verificacion de cufe segun resolucion
FossilOrigin-Name: c7b12b74accfb0a8e3abea0b74d2734f4911cfc7594b5294d748d81f9c43fb03
This commit is contained in:
parent
bafd58124d
commit
b451c90dd6
102
facho/fe/form.py
102
facho/fe/form.py
@ -54,6 +54,7 @@ class TaxTotal:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InvoiceLine:
|
class InvoiceLine:
|
||||||
|
# RESOLUCION 0004: pagina 155
|
||||||
quantity: int
|
quantity: int
|
||||||
description: str
|
description: str
|
||||||
item_ident: int
|
item_ident: int
|
||||||
@ -66,11 +67,11 @@ class InvoiceLine:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def total_tax_inclusive_amount(self):
|
def total_tax_inclusive_amount(self):
|
||||||
return self.tax.taxable_amount
|
return self.tax.taxable_amount + self.tax.tax_amount
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_tax_exclusive_amount(self):
|
def total_tax_exclusive_amount(self):
|
||||||
return self.tax.tax_amount
|
return self.tax.taxable_amount
|
||||||
|
|
||||||
def calculate(self):
|
def calculate(self):
|
||||||
self.tax.calculate(self)
|
self.tax.calculate(self)
|
||||||
@ -125,17 +126,18 @@ class Invoice:
|
|||||||
def _calculate_legal_monetary_total(self):
|
def _calculate_legal_monetary_total(self):
|
||||||
for invline in self.invoice_lines:
|
for invline in self.invoice_lines:
|
||||||
self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount
|
self.invoice_legal_monetary_total.line_extension_amount += invline.total_amount
|
||||||
self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_amount
|
self.invoice_legal_monetary_total.tax_exclusive_amount += invline.total_tax_exclusive_amount
|
||||||
|
self.invoice_legal_monetary_total.tax_inclusive_amount += invline.total_tax_inclusive_amount
|
||||||
self.invoice_legal_monetary_total.charge_total_amount += invline.total_amount
|
self.invoice_legal_monetary_total.charge_total_amount += invline.total_amount
|
||||||
|
#self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \
|
||||||
self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \
|
# + self.invoice_legal_monetary_total.line_extension_amount \
|
||||||
+ self.invoice_legal_monetary_total.line_extension_amount \
|
# + self.invoice_legal_monetary_total.tax_inclusive_amount
|
||||||
+ self.invoice_legal_monetary_total.tax_inclusive_amount
|
self.invoice_legal_monetary_total.payable_amount = self.invoice_legal_monetary_total.tax_inclusive_amount
|
||||||
|
|
||||||
def calculate(self):
|
def calculate(self):
|
||||||
self._calculate_legal_monetary_total()
|
|
||||||
for invline in self.invoice_lines:
|
for invline in self.invoice_lines:
|
||||||
invline.calculate()
|
invline.calculate()
|
||||||
|
self._calculate_legal_monetary_total()
|
||||||
|
|
||||||
|
|
||||||
class DianResolucion0001Validator:
|
class DianResolucion0001Validator:
|
||||||
@ -168,24 +170,32 @@ class DianResolucion0001Validator:
|
|||||||
|
|
||||||
|
|
||||||
class DIANInvoiceXML(fe.FeXML):
|
class DIANInvoiceXML(fe.FeXML):
|
||||||
|
AMBIENTE_PRUEBAS = 'Pruebas'
|
||||||
def __init__(self, invoice, TipoAmbiente = 'Pruebas'):
|
AMBIENTE_PRODUCCION = 'Producción'
|
||||||
|
|
||||||
|
def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS, clave_tecnica = ''):
|
||||||
super().__init__('Invoice', 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
super().__init__('Invoice', 'http://www.dian.gov.co/contratos/facturaelectronica/v1')
|
||||||
self.attach_invoice(invoice, TipoAmbiente)
|
self.tipo_ambiente = tipo_ambiente
|
||||||
|
self.clave_tecnica = clave_tecnica
|
||||||
def attach_invoice(self, invoice, TipoAmbiente):
|
self.attach_invoice(invoice)
|
||||||
|
|
||||||
|
def _tipo_ambiente(self):
|
||||||
|
return int(dian.TipoAmbiente[self.tipo_ambiente]['code'])
|
||||||
|
|
||||||
|
def attach_invoice(self, invoice):
|
||||||
"""adiciona etiquetas a FEXML y retorna FEXML
|
"""adiciona etiquetas a FEXML y retorna FEXML
|
||||||
en caso de fallar validacion retorna None"""
|
en caso de fallar validacion retorna None"""
|
||||||
fexml = self
|
fexml = self
|
||||||
|
|
||||||
invoice.calculate()
|
invoice.calculate()
|
||||||
|
|
||||||
cufe = self._generate_cufe(invoice, TipoAmbiente)
|
cufe = self._generate_cufe(invoice)
|
||||||
|
|
||||||
|
fexml.set_element('/fe:Invoice/cbc:ProfileExecutionID', self._tipo_ambiente())
|
||||||
fexml.set_element('/fe:Invoice/cbc:ID', invoice.invoice_ident)
|
fexml.set_element('/fe:Invoice/cbc:ID', invoice.invoice_ident)
|
||||||
fexml.set_element('/fe:Invoice/cbc:UUID[schemaName="CUFE-SHA384"]', cufe)
|
fexml.set_element('/fe:Invoice/cbc:UUID[schemaName="CUFE-SHA384"]', cufe)
|
||||||
fexml.set_element('/fe:Invoice/cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
|
fexml.set_element('/fe:Invoice/cbc:IssueDate', self.issue_date(invoice.invoice_issue))
|
||||||
fexml.set_element('/fe:Invoice/cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S'))
|
fexml.set_element('/fe:Invoice/cbc:IssueTime', self.issue_time(invoice.invoice_issue))
|
||||||
fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:StartDate', invoice.invoice_period_start.strftime('%Y-%m-%d'))
|
fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:StartDate', invoice.invoice_period_start.strftime('%Y-%m-%d'))
|
||||||
fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:EndDate', invoice.invoice_period_end.strftime('%Y-%m-%d'))
|
fexml.set_element('/fe:Invoice/cac:InvoicePeriod/cbc:EndDate', invoice.invoice_period_end.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
@ -193,6 +203,8 @@ class DIANInvoiceXML(fe.FeXML):
|
|||||||
|
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyIdentification/cbc:ID',
|
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyIdentification/cbc:ID',
|
||||||
invoice.invoice_supplier.ident)
|
invoice.invoice_supplier.ident)
|
||||||
|
fexml.set_element('/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
|
||||||
|
invoice.invoice_supplier.ident)
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode',
|
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode',
|
||||||
invoice.invoice_supplier.responsability_code)
|
invoice.invoice_supplier.responsability_code)
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/cbc:AdditionalAccountID',
|
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/cbc:AdditionalAccountID',
|
||||||
@ -211,6 +223,8 @@ class DIANInvoiceXML(fe.FeXML):
|
|||||||
invoice.invoice_customer.organization_code)
|
invoice.invoice_customer.organization_code)
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyName/cbc:Name',
|
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyName/cbc:Name',
|
||||||
invoice.invoice_customer.name)
|
invoice.invoice_customer.name)
|
||||||
|
fexml.set_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID',
|
||||||
|
invoice.invoice_customer.ident)
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName',
|
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName',
|
||||||
invoice.invoice_customer.legal_name)
|
invoice.invoice_customer.legal_name)
|
||||||
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
|
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
|
||||||
@ -232,6 +246,7 @@ class DIANInvoiceXML(fe.FeXML):
|
|||||||
invoice.invoice_legal_monetary_total.payable_amount,
|
invoice.invoice_legal_monetary_total.payable_amount,
|
||||||
currencyID='COP')
|
currencyID='COP')
|
||||||
|
|
||||||
|
fexml.set_element('/fe:Invoice/cbc:LineCountNumeric', len(invoice.invoice_lines))
|
||||||
next_append = False
|
next_append = False
|
||||||
for index, invoice_line in enumerate(invoice.invoice_lines):
|
for index, invoice_line in enumerate(invoice.invoice_lines):
|
||||||
line = fexml.fragment('/fe:Invoice/fe:InvoiceLine', append=next_append)
|
line = fexml.fragment('/fe:Invoice/fe:InvoiceLine', append=next_append)
|
||||||
@ -243,13 +258,18 @@ class DIANInvoiceXML(fe.FeXML):
|
|||||||
line.set_element('/fe:InvoiceLine/fe:Price/cbc:PriceAmount', invoice_line.price_amount, currencyID="COP")
|
line.set_element('/fe:InvoiceLine/fe:Price/cbc:PriceAmount', invoice_line.price_amount, currencyID="COP")
|
||||||
line.set_element('/fe:InvoiceLine/fe:Item/cbc:Description', invoice_line.description)
|
line.set_element('/fe:InvoiceLine/fe:Item/cbc:Description', invoice_line.description)
|
||||||
|
|
||||||
|
|
||||||
return fexml
|
return fexml
|
||||||
|
|
||||||
|
def issue_time(self, datetime_):
|
||||||
def _generate_cufe(self, invoice, TipoAmbiente = 'Pruebas'):
|
return datetime_.strftime('%H:%M:%S%z')
|
||||||
|
def issue_date(self, datetime_):
|
||||||
|
return datetime_.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
def _generate_cufe(self, invoice):
|
||||||
NumFac = invoice.invoice_ident
|
NumFac = invoice.invoice_ident
|
||||||
FecFac = invoice.invoice_issue.strftime('%Y-%m-%d')
|
FecFac = self.issue_date(invoice.invoice_issue)
|
||||||
HoraFac = invoice.invoice_issue.strftime('%H:%H:%S')
|
HoraFac = self.issue_time(invoice.invoice_issue)
|
||||||
ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount
|
ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount
|
||||||
ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount
|
ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount
|
||||||
ValorImpuestoPara = {}
|
ValorImpuestoPara = {}
|
||||||
@ -268,25 +288,27 @@ class DIANInvoiceXML(fe.FeXML):
|
|||||||
|
|
||||||
NitOFE = invoice.invoice_supplier.ident
|
NitOFE = invoice.invoice_supplier.ident
|
||||||
NumAdq = invoice.invoice_customer.ident
|
NumAdq = invoice.invoice_customer.ident
|
||||||
TipoAmb = int(dian.TipoAmbiente[TipoAmbiente]['code'])
|
TipoAmb = self._tipo_ambiente()
|
||||||
|
ClTec = str(self.clave_tecnica)
|
||||||
formatVars = {
|
|
||||||
'%s': NumFac,
|
formatVars = [
|
||||||
'%s': FecFac,
|
'%s' % NumFac,
|
||||||
'%.02f': HoraFac,
|
'%s' % FecFac,
|
||||||
'%.02f': ValorBruto,
|
'%s' % HoraFac,
|
||||||
'%.02f': ValorTotalPagar,
|
'%.02f' % ValorBruto,
|
||||||
'%.02f': ValorImpuestoPara.get(CodImpuesto1, 0.0),
|
'%02d' % CodImpuesto1,
|
||||||
'%02d': CodImpuesto1,
|
'%.02f' % ValorImpuestoPara.get(CodImpuesto1, 0.0),
|
||||||
'%.02f': ValorImpuestoPara.get(CodImpuesto2, 0.0),
|
'%02d' % CodImpuesto2,
|
||||||
'%02d': CodImpuesto2,
|
'%.02f' % ValorImpuestoPara.get(CodImpuesto2, 0.0),
|
||||||
'%.02f': ValorImpuestoPara.get(CodImpuesto3, 0.0),
|
'%02d' % CodImpuesto3,
|
||||||
'%02d': CodImpuesto3,
|
'%.02f' % ValorImpuestoPara.get(CodImpuesto3, 0.0),
|
||||||
'%s': NitOFE,
|
'%.02f' % ValorTotalPagar,
|
||||||
'%s': NumAdq,
|
'%s' % NitOFE,
|
||||||
'%d': TipoAmb,
|
'%s' % NumAdq,
|
||||||
}
|
'%s' % ClTec,
|
||||||
cufe = "".join(formatVars.keys()) % tuple(formatVars.values())
|
'%d' % TipoAmb,
|
||||||
|
]
|
||||||
|
cufe = "".join(formatVars)
|
||||||
|
|
||||||
# crear hash...
|
# crear hash...
|
||||||
h = hashlib.sha384()
|
h = hashlib.sha384()
|
||||||
|
@ -14,6 +14,25 @@ import facho.fe.form as form
|
|||||||
from facho import fe
|
from facho import fe
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_invoice_without_lines():
|
||||||
|
inv = form.Invoice()
|
||||||
|
inv.set_period(datetime.now(), datetime.now())
|
||||||
|
inv.set_issue(datetime.now())
|
||||||
|
inv.set_ident('ABC123')
|
||||||
|
inv.set_supplier(form.Party(
|
||||||
|
name = 'facho-supplier',
|
||||||
|
ident = 123,
|
||||||
|
responsability_code = 'No aplica',
|
||||||
|
organization_code = 'Persona Natural'
|
||||||
|
))
|
||||||
|
inv.set_customer(form.Party(
|
||||||
|
name = 'facho-customer',
|
||||||
|
ident = 321,
|
||||||
|
responsability_code = 'No aplica',
|
||||||
|
organization_code = 'Persona Natural'
|
||||||
|
))
|
||||||
|
return inv
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def simple_invoice():
|
def simple_invoice():
|
||||||
@ -110,7 +129,70 @@ def test_invoicesimple_zip(simple_invoice):
|
|||||||
|
|
||||||
def test_bug_cbcid_empty_on_invoice_line(simple_invoice):
|
def test_bug_cbcid_empty_on_invoice_line(simple_invoice):
|
||||||
xml_invoice = form.DIANInvoiceXML(simple_invoice)
|
xml_invoice = form.DIANInvoiceXML(simple_invoice)
|
||||||
print(str(xml_invoice))
|
|
||||||
|
|
||||||
cbc_id = xml_invoice.get_element_text('/fe:Invoice/fe:InvoiceLine[1]/cbc:ID', format_=int)
|
cbc_id = xml_invoice.get_element_text('/fe:Invoice/fe:InvoiceLine[1]/cbc:ID', format_=int)
|
||||||
assert cbc_id == 1
|
assert cbc_id == 1
|
||||||
|
|
||||||
|
def test_invoice_line_count_numeric(simple_invoice):
|
||||||
|
xml_invoice = form.DIANInvoiceXML(simple_invoice)
|
||||||
|
count = xml_invoice.get_element_text('/fe:Invoice/cbc:LineCountNumeric', format_=int)
|
||||||
|
assert count == len(simple_invoice.invoice_lines)
|
||||||
|
|
||||||
|
def test_invoice_profileexecutionid(simple_invoice):
|
||||||
|
xml_invoice = form.DIANInvoiceXML(simple_invoice)
|
||||||
|
id_ = xml_invoice.get_element_text('/fe:Invoice/cbc:ProfileExecutionID', format_=int)
|
||||||
|
assert id_ == 2
|
||||||
|
|
||||||
|
def test_invoice_totals(simple_invoice_without_lines):
|
||||||
|
simple_invoice = simple_invoice_without_lines
|
||||||
|
simple_invoice.invoice_ident = '323200000129'
|
||||||
|
simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
|
||||||
|
simple_invoice.invoice_supplier.ident = '700085371'
|
||||||
|
simple_invoice.invoice_customer.ident = '800199436'
|
||||||
|
simple_invoice.add_invoice_line(form.InvoiceLine(
|
||||||
|
quantity = 1,
|
||||||
|
description = 'producto',
|
||||||
|
item_ident = 9999,
|
||||||
|
price_amount = 1_500_000,
|
||||||
|
tax = form.TaxTotal(
|
||||||
|
subtotals = [
|
||||||
|
form.TaxSubTotal(
|
||||||
|
tax_scheme_ident = '01',
|
||||||
|
percent = 19.0
|
||||||
|
)])
|
||||||
|
))
|
||||||
|
simple_invoice.calculate()
|
||||||
|
assert 1 == len(simple_invoice.invoice_lines)
|
||||||
|
assert 1_500_000 == simple_invoice.invoice_legal_monetary_total.line_extension_amount
|
||||||
|
assert 1_785_000 == simple_invoice.invoice_legal_monetary_total.payable_amount
|
||||||
|
|
||||||
|
def test_invoice_cufe(simple_invoice_without_lines):
|
||||||
|
simple_invoice = simple_invoice_without_lines
|
||||||
|
simple_invoice.invoice_ident = '323200000129'
|
||||||
|
simple_invoice.invoice_issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z')
|
||||||
|
simple_invoice.invoice_supplier.ident = '700085371'
|
||||||
|
simple_invoice.invoice_customer.ident = '800199436'
|
||||||
|
simple_invoice.add_invoice_line(form.InvoiceLine(
|
||||||
|
quantity = 1,
|
||||||
|
description = 'producto',
|
||||||
|
item_ident = 9999,
|
||||||
|
price_amount = 1_500_000,
|
||||||
|
tax = form.TaxTotal(
|
||||||
|
subtotals = [
|
||||||
|
form.TaxSubTotal(
|
||||||
|
tax_scheme_ident = '01',
|
||||||
|
percent = 19.0
|
||||||
|
)])
|
||||||
|
))
|
||||||
|
|
||||||
|
class FakeDIANInvoiceXML(form.DIANInvoiceXML):
|
||||||
|
def issue_time(self, datetime_):
|
||||||
|
return '10:53:10-05:00'
|
||||||
|
def issue_date(self, datetime_):
|
||||||
|
return '2019-01-16'
|
||||||
|
|
||||||
|
xml_invoice = FakeDIANInvoiceXML(simple_invoice,
|
||||||
|
tipo_ambiente = form.DIANInvoiceXML.AMBIENTE_PRODUCCION,
|
||||||
|
clave_tecnica = '693ff6f2a553c3646a063436fd4dd9ded0311471')
|
||||||
|
cufe = xml_invoice.get_element_text('/fe:Invoice/cbc:UUID')
|
||||||
|
|
||||||
|
assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4'
|
||||||
|
Loading…
Reference in New Issue
Block a user