diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index 232fe35..026eef8 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -1,12 +1,14 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -import hashlib -from functools import reduce -import copy + +# import hashlib +# from functools import reduce +# import copy + import dataclasses from dataclasses import dataclass, field from datetime import datetime, date -from collections import defaultdict +# from collections import defaultdict import decimal from decimal import Decimal import typing @@ -14,9 +16,11 @@ from ..data.dian import codelist DECIMAL_PRECISION = 6 + class AmountCurrencyError(TypeError): pass + @dataclass class Currency: code: str @@ -27,6 +31,7 @@ class Currency: def __str__(self): return self.code + class Collection: def __init__(self, array): @@ -43,6 +48,7 @@ class Collection: def sum(self): return sum(self.array) + class AmountCollection(Collection): def sum(self): @@ -51,10 +57,13 @@ class AmountCollection(Collection): total += v return total -class Amount: - def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')): - #DIAN 1.7.-2020: 1.2.3.1 +class Amount: + def __init__( + self, amount: typing.Union[int, float, str, "Amount"], + currency: Currency = Currency('COP')): + + # DIAN 1.7.-2020: 1.2.3.1 if isinstance(amount, Amount): if amount < Amount(0.0): raise ValueError('amount must be positive >= 0') @@ -65,14 +74,16 @@ class Amount: if float(amount) < 0: raise ValueError('amount must be positive >= 0') - self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, - #DIAN 1.7.-2020: 1.2.1.1 - rounding=decimal.ROUND_HALF_EVEN )) + self.amount = Decimal( + amount, decimal.Context( + prec=DECIMAL_PRECISION, + # DIAN 1.7.-2020: 1.2.1.1 + rounding=decimal.ROUND_HALF_EVEN)) self.currency = currency def fromNumber(self, val): return Amount(val, currency=self.currency) - + def round(self, prec): return Amount(round(self.amount, prec), currency=self.currency) @@ -90,7 +101,8 @@ class Amount: def __eq__(self, other): if not self.is_same_currency(other): raise AmountCurrencyError() - return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) + return round(self.amount, DECIMAL_PRECISION) == round( + other.amount, DECIMAL_PRECISION) def _cast(self, val): if type(val) in [int, float]: @@ -98,7 +110,7 @@ class Amount: if isinstance(val, Amount): return val raise TypeError("cant cast to amount") - + def __add__(self, rother): other = self._cast(rother) if not self.is_same_currency(other): @@ -122,14 +134,14 @@ class Amount: def truncate_as_string(self, prec): parts = str(self.float()).split('.', 1) - return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0')) + return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec, '0')) def float(self): return float(round(self.amount, DECIMAL_PRECISION)) - + class Quantity: - + def __init__(self, val, code): if type(val) not in [float, int]: raise ValueError('val expected int or float') @@ -151,6 +163,7 @@ class Quantity: def __repr__(self): return str(self) + @dataclass class Item: scheme_name: str @@ -175,9 +188,9 @@ class UNSPSCItem(Item): description=description, scheme_name='UNSPSC', scheme_id='001', - scheme_agency_id='10') + scheme_agency_id='10') + - @dataclass class Country: code: str @@ -188,6 +201,7 @@ class Country: raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Paises[self.code]['name'] + @dataclass class CountrySubentity: code: str @@ -198,6 +212,7 @@ class CountrySubentity: raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Departamento[self.code]['name'] + @dataclass class City: code: str @@ -208,19 +223,23 @@ class City: raise ValueError("code [%s] not found" % (self.code)) self.name = codelist.Municipio[self.code]['name'] + @dataclass class PostalZone: code: str = '' - + + @dataclass class Address: name: str street: str = '' city: City = field(default_factory=lambda: City('05001')) country: Country = field(default_factory=lambda: Country('CO')) - countrysubentity: CountrySubentity = field(default_factory=lambda: CountrySubentity('05')) + countrysubentity: CountrySubentity = field( + default_factory=lambda: CountrySubentity('05')) postalzone: PostalZone = field(default_factory=lambda: PostalZone('')) + @dataclass class PartyIdentification: number: str @@ -240,6 +259,7 @@ class PartyIdentification: if self.type_fiscal not in codelist.TipoIdFiscal: raise ValueError("type_fiscal [%s] not found" % (self.type_fiscal)) + @dataclass class Responsability: codes: list @@ -269,6 +289,7 @@ class TaxScheme: raise ValueError("code not found") self.name = codelist.TipoImpuesto[self.code]['name'] + @dataclass class Party: name: str @@ -334,6 +355,7 @@ class TaxTotalOmit(TaxTotal): def calculate(self, invline): pass + @dataclass class WithholdingTaxSubTotal: percent: float @@ -344,6 +366,7 @@ class WithholdingTaxSubTotal: if self.percent is not None: self.tax_amount = invline.total_amount * Amount(self.percent / 100) + @dataclass class WithholdingTaxTotal: subtotals: list @@ -356,7 +379,8 @@ class WithholdingTaxTotal: for subtax in self.subtotals: subtax.calculate(invline) self.tax_amount += subtax.tax_amount - + + class WithholdingTaxTotalOmit(WithholdingTaxTotal): def __init__(self): super().__init__([]) @@ -364,6 +388,7 @@ class WithholdingTaxTotalOmit(WithholdingTaxTotal): def calculate(self, invline): pass + @dataclass class Price: amount: Amount @@ -379,6 +404,7 @@ class Price: self.amount *= self.quantity + @dataclass class PaymentMean: DEBIT = '01' @@ -396,15 +422,17 @@ class PaymentMean: @dataclass class PrePaidPayment: - #DIAN 1.7.-2020: FBD03 + # DIAN 1.7.-2020: FBD03 paid_amount: Amount = field(default_factory=lambda: Amount(0.0)) + @dataclass class BillingResponse: id: str code: str description: str + class SupportDocumentCreditNoteResponse(BillingResponse): """ ReferenceID: Identifica la sección del Documento @@ -414,13 +442,13 @@ class SupportDocumentCreditNoteResponse(BillingResponse): """ - @dataclass class BillingReference: ident: str uuid: str date: date + class CreditNoteDocumentReference(BillingReference): """ ident: Prefijo + Numero de la factura relacionada @@ -428,6 +456,7 @@ class CreditNoteDocumentReference(BillingReference): date: fecha de emision de la factura relacionada """ + class DebitNoteDocumentReference(BillingReference): """ ident: Prefijo + Numero de la factura relacionada @@ -435,6 +464,7 @@ class DebitNoteDocumentReference(BillingReference): date: fecha de emision de la factura relacionada """ + class InvoiceDocumentReference(BillingReference): """ ident: Prefijo + Numero de la nota credito relacionada @@ -442,6 +472,7 @@ class InvoiceDocumentReference(BillingReference): date: fecha de emision de la nota credito relacionada """ + @dataclass class AllowanceChargeReason: code: str @@ -468,10 +499,12 @@ class AllowanceCharge: default_factory=lambda: Amount(1.0)) def isCharge(self): - return self.charge_indicator == True + charge_indicator = self.charge_indicator is True + return charge_indicator def isDiscount(self): - return self.charge_indicator == False + charge_indicator = self.charge_indicator is False + return charge_indicator def asCharge(self): self.charge_indicator = True @@ -485,11 +518,13 @@ class AllowanceCharge: def set_base_amount(self, amount): self.base_amount = amount + class AllowanceChargeAsDiscount(AllowanceCharge): def __init__(self, amount: Amount = Amount(0.0)): self.charge_indicator = False self.amount = amount + @dataclass class InvoiceLine: # RESOLUCION 0004: pagina 155 @@ -503,7 +538,8 @@ class InvoiceLine: # de subtotal tax: typing.Optional[TaxTotal] withholding: typing.Optional[WithholdingTaxTotal] - allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list) + allowance_charge: typing.List[AllowanceCharge] = dataclasses.field( + default_factory=list) def add_allowance_charge(self, charge): if not isinstance(charge, AllowanceCharge): @@ -514,7 +550,7 @@ class InvoiceLine: @property def total_amount_without_charge(self): return (self.quantity * self.price.amount) - + @property def total_amount(self): charge = AmountCollection(self.allowance_charge)\ @@ -568,6 +604,7 @@ class InvoiceLine: if self.withholding is None: self.withholding = WithholdingTaxTotalOmit() + @dataclass class LegalMonetaryTotal: line_extension_amount: Amount = field(default_factory=lambda: Amount(0.0)) @@ -579,7 +616,7 @@ class LegalMonetaryTotal: prepaid_amount: Amount = field(default_factory=lambda: Amount(0.0)) def calculate(self): - #DIAN 1.7.-2020: FAU14 + # DIAN 1.7.-2020: FAU14 self.payable_amount = \ self.tax_inclusive_amount \ + self.allowance_total_amount \ @@ -587,26 +624,29 @@ class LegalMonetaryTotal: - self.prepaid_amount - class NationalSalesInvoiceDocumentType(str): def __str__(self): # 6.1.3 return '01' + class CreditNoteDocumentType(str): def __str__(self): # 6.1.3 return '91' + class DebitNoteDocumentType(str): def __str__(self): # 6.1.3 return '92' + class CreditNoteSupportDocumentType(str): def __str__(self): return '95' + class Invoice: def __init__(self, type_code: str): if str(type_code) not in codelist.TipoDocumento: @@ -652,7 +692,8 @@ class Invoice: if len(prefix) <= 4: self.invoice_ident_prefix = prefix else: - raise ValueError('ident prefix failed to get, expected 0 to 4 chars') + raise ValueError( + 'ident prefix failed to get, expected 0 to 4 chars') def set_ident(self, ident: str): """ @@ -683,7 +724,7 @@ class Invoice: def _get_codelist_tipo_operacion(self): return codelist.TipoOperacionF - + def set_operation_type(self, operation): if operation not in self._get_codelist_tipo_operacion(): raise ValueError("operation not found") @@ -705,7 +746,6 @@ class Invoice: def set_discrepancy_response(self, billing_response: BillingResponse): self.invoice_discrepancy_response = billing_response - def accept(self, visitor): visitor.visit_payment_mean(self.invoice_payment_mean) visitor.visit_customer(self.invoice_customer) @@ -717,29 +757,34 @@ class Invoice: def _calculate_legal_monetary_total(self): for invline in self.invoice_lines: - self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount - self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount - #DIAN 1.7.-2020: FAU6 - self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount + self.invoice_legal_monetary_total.line_extension_amount +=\ + invline.total_amount + self.invoice_legal_monetary_total.tax_exclusive_amount +=\ + invline.total_tax_exclusive_amount + # DIAN 1.7.-2020: FAU6 + self.invoice_legal_monetary_total.tax_inclusive_amount +=\ + invline.total_tax_inclusive_amount - #DIAN 1.7.-2020: FAU08 - self.invoice_legal_monetary_total.allowance_total_amount = AmountCollection(self.invoice_allowance_charge)\ + # DIAN 1.7.-2020: FAU08 + self.invoice_legal_monetary_total.allowance_total_amount =\ + AmountCollection(self.invoice_allowance_charge)\ .filter(lambda charge: charge.isDiscount())\ .map(lambda charge: charge.amount)\ .sum() - #DIAN 1.7.-2020: FAU10 - self.invoice_legal_monetary_total.charge_total_amount = AmountCollection(self.invoice_allowance_charge)\ + # DIAN 1.7.-2020: FAU10 + self.invoice_legal_monetary_total.charge_total_amount =\ + AmountCollection(self.invoice_allowance_charge)\ .filter(lambda charge: charge.isCharge())\ .map(lambda charge: charge.amount)\ .sum() - #DIAN 1.7.-2020: FAU12 - self.invoice_legal_monetary_total.prepaid_amount = AmountCollection(self.invoice_prepaid_payment)\ - .map(lambda paid: paid.paid_amount)\ - .sum() + # DIAN 1.7.-2020: FAU12 + self.invoice_legal_monetary_total.prepaid_amount = AmountCollection( + self.invoice_prepaid_payment).map( + lambda paid: paid.paid_amount).sum() - #DIAN 1.7.-2020: FAU14 + # DIAN 1.7.-2020: FAU14 self.invoice_legal_monetary_total.calculate() def _refresh_charges_base_amount(self): @@ -747,18 +792,21 @@ class Invoice: for invline in self.invoice_lines: if invline.allowance_charge: # TODO actualmente solo uno de los cargos es permitido - raise ValueError('allowance charge in invoice exclude invoice line') - + raise ValueError( + 'allowance charge in invoice exclude invoice line') + # cargos a nivel de factura for charge in self.invoice_allowance_charge: - charge.set_base_amount(self.invoice_legal_monetary_total.line_extension_amount) - + charge.set_base_amount( + self.invoice_legal_monetary_total.line_extension_amount) + def calculate(self): for invline in self.invoice_lines: invline.calculate() self._calculate_legal_monetary_total() self._refresh_charges_base_amount() + class NationalSalesInvoice(Invoice): def __init__(self): super().__init__(NationalSalesInvoiceDocumentType()) @@ -774,7 +822,7 @@ class CreditNote(Invoice): def _get_codelist_tipo_operacion(self): return codelist.TipoOperacionNC - + def _check_ident_prefix(self, prefix): if len(prefix) != 6: raise ValueError('prefix must be 6 length') @@ -803,12 +851,15 @@ class DebitNote(Invoice): if not self.invoice_ident_prefix: self.invoice_ident_prefix = self.invoice_ident[0:6] + class SupportDocument(Invoice): pass + class SupportDocumentCreditNote(SupportDocument): - def __init__(self, invoice_document_reference: BillingReference, - invoice_discrepancy_response: BillingResponse): + def __init__( + self, invoice_document_reference: BillingReference, + invoice_discrepancy_response: BillingResponse): super().__init__(CreditNoteSupportDocumentType()) if not isinstance(invoice_document_reference, BillingReference):