oc-facho/facho/fe/form.py
bit4bit@riseup.net b451c90dd6 verificacion de cufe segun resolucion
FossilOrigin-Name: c7b12b74accfb0a8e3abea0b74d2734f4911cfc7594b5294d748d81f9c43fb03
2020-05-27 18:22:38 +00:00

318 lines
12 KiB
Python

# 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
from dataclasses import dataclass
from datetime import datetime
from .data import dian
from . import fe
@dataclass
class Party:
name: str
ident: str
responsability_code: str
organization_code: str
phone: str = ''
address: str = ''
email: str = ''
legal_name: str = ''
legal_company_ident: str = ''
legal_address: str = ''
@dataclass
class TaxSubTotal:
percent: float
tax_scheme_ident: str = '01'
tax_amount: float = 0.0
taxable_amount: float = 0.0
def calculate(self, invline):
self.tax_amount = invline.total_amount * (self.percent / 100)
self.taxable_amount = invline.total_amount
@dataclass
class TaxTotal:
subtotals: list
tax_amount: float = 0.0
taxable_amount: float = 0.0
def calculate(self, invline):
for subtax in self.subtotals:
subtax.calculate(invline)
self.tax_amount += subtax.tax_amount
self.taxable_amount += subtax.taxable_amount
@dataclass
class InvoiceLine:
# RESOLUCION 0004: pagina 155
quantity: int
description: str
item_ident: int
price_amount: float
tax: TaxTotal
@property
def total_amount(self):
return self.quantity * self.price_amount
@property
def total_tax_inclusive_amount(self):
return self.tax.taxable_amount + self.tax.tax_amount
@property
def total_tax_exclusive_amount(self):
return self.tax.taxable_amount
def calculate(self):
self.tax.calculate(self)
@dataclass
class LegalMonetaryTotal:
line_extension_amount: float = 0.0
tax_exclusive_amount: float = 0.0
tax_inclusive_amount: float = 0.0
charge_total_amount: float = 0.0
payable_amount: float = 0.0
class Invoice:
def __init__(self):
self.invoice_period_start = None
self.invoice_period_end = None
self.invoice_issue = None
self.invoice_ident = None
self.invoice_cufe = None
self.invoice_legal_monetary_total = LegalMonetaryTotal(0, 0, 0, 0, 0)
self.invoice_customer = None
self.invoice_supplier = None
self.invoice_lines = []
def set_period(self, startdate, enddate):
self.invoice_period_start = startdate
self.invoice_period_end = enddate
def set_issue(self, dtime: datetime):
self.invoice_issue = dtime
def set_ident(self, ident: str):
self.invoice_ident = ident
def set_supplier(self, party: Party):
self.invoice_supplier = party
def set_customer(self, party: Party):
self.invoice_customer = party
def add_invoice_line(self, line: InvoiceLine):
self.invoice_lines.append(line)
def validate(self, validator):
validator.validate_customer(self.invoice_customer)
validator.validate_supplier(self.invoice_supplier)
for invline in self.invoice_lines:
validator.validate_invoice_line(self, invline)
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
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.payable_amount = self.invoice_legal_monetary_total.tax_exclusive_amount \
# + self.invoice_legal_monetary_total.line_extension_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):
for invline in self.invoice_lines:
invline.calculate()
self._calculate_legal_monetary_total()
class DianResolucion0001Validator:
def __init__(self):
self.errors = []
def _validate_party(self, party):
try:
dian.TipoResponsabilidad[party.responsability_code]
except KeyError:
self.errors.append(('responsability_code', 'not found'))
try:
dian.TipoOrganizacion[party.organization_code]
except KeyError:
self.errors.append(('organization_code', 'not found'))
def validate_customer(self, customer):
self._validate_party(customer)
def validate_supplier(self, supplier):
self._validate_party(supplier)
def validate_invoice_line(self, invoice, line):
pass
def valid(self):
return not self.errors
class DIANInvoiceXML(fe.FeXML):
AMBIENTE_PRUEBAS = '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')
self.tipo_ambiente = tipo_ambiente
self.clave_tecnica = clave_tecnica
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
en caso de fallar validacion retorna None"""
fexml = self
invoice.calculate()
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:UUID[schemaName="CUFE-SHA384"]', cufe)
fexml.set_element('/fe:Invoice/cbc:IssueDate', self.issue_date(invoice.invoice_issue))
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:EndDate', invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_element('/fe:Invoice/cbc:LineCountNumeric', len(invoice.invoice_lines))
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyIdentification/cbc:ID',
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',
invoice.invoice_supplier.responsability_code)
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/cbc:AdditionalAccountID',
invoice.invoice_supplier.organization_code)
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/cac:PartyName/cbc:Name',
invoice.invoice_supplier.name)
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PartyLegalEntity/cbc:RegistrationName',
invoice.invoice_supplier.legal_name)
fexml.set_element('/fe:Invoice/fe:AccountingSupplierParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
invoice.invoice_supplier.address)
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyIdentification/cbc:ID',
invoice.invoice_customer.ident)
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PartyTaxScheme/cbc:TaxLevelCode',
invoice.invoice_customer.responsability_code)
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/cbc:AdditionalAccountID',
invoice.invoice_customer.organization_code)
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/cac:PartyName/cbc: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',
invoice.invoice_customer.legal_name)
fexml.set_element('/fe:Invoice/fe:AccountingCustomerParty/fe:Party/fe:PhysicalLocation/fe:Address/cac:AddressLine/cbc:Line',
invoice.invoice_customer.address)
fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:LineExtensionAmount',
invoice.invoice_legal_monetary_total.line_extension_amount,
currencyID='COP')
fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:TaxExclusiveAmount',
invoice.invoice_legal_monetary_total.tax_exclusive_amount,
currencyID='COP')
fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:TaxInclusiveAmount',
invoice.invoice_legal_monetary_total.tax_inclusive_amount,
currencyID='COP')
fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:ChargeTotalAmount',
invoice.invoice_legal_monetary_total.charge_total_amount,
currencyID='COP')
fexml.set_element('/fe:Invoice/fe:LegalMonetaryTotal/cbc:PayableAmount',
invoice.invoice_legal_monetary_total.payable_amount,
currencyID='COP')
fexml.set_element('/fe:Invoice/cbc:LineCountNumeric', len(invoice.invoice_lines))
next_append = False
for index, invoice_line in enumerate(invoice.invoice_lines):
line = fexml.fragment('/fe:Invoice/fe:InvoiceLine', append=next_append)
next_append = True
line.set_element('/fe:InvoiceLine/cbc:ID', index + 1)
line.set_element('/fe:InvoiceLine/cbc:InvoicedQuantity', invoice_line.quantity, unitCode = 'NAR')
line.set_element('/fe:InvoiceLine/cbc:LineExtensionAmount', invoice_line.total_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)
return fexml
def issue_time(self, datetime_):
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
FecFac = self.issue_date(invoice.invoice_issue)
HoraFac = self.issue_time(invoice.invoice_issue)
ValorBruto = invoice.invoice_legal_monetary_total.line_extension_amount
ValorTotalPagar = invoice.invoice_legal_monetary_total.payable_amount
ValorImpuestoPara = {}
ValorImpuesto1 = 0.0
CodImpuesto1 = 1
ValorImpuesto2 = 0.0
CodImpuesto2 = 4
ValorImpuesto3 = 0.0
CodImpuesto3 = 3
for invoice_line in invoice.invoice_lines:
for subtotal in invoice_line.tax.subtotals:
# TODO cual es la naturaleza de tax_scheme_ident?
codigo_impuesto = int(subtotal.tax_scheme_ident)
ValorImpuestoPara.setdefault(codigo_impuesto, 0.0)
ValorImpuestoPara[codigo_impuesto] += subtotal.tax_amount
NitOFE = invoice.invoice_supplier.ident
NumAdq = invoice.invoice_customer.ident
TipoAmb = self._tipo_ambiente()
ClTec = str(self.clave_tecnica)
formatVars = [
'%s' % NumFac,
'%s' % FecFac,
'%s' % HoraFac,
'%.02f' % ValorBruto,
'%02d' % CodImpuesto1,
'%.02f' % ValorImpuestoPara.get(CodImpuesto1, 0.0),
'%02d' % CodImpuesto2,
'%.02f' % ValorImpuestoPara.get(CodImpuesto2, 0.0),
'%02d' % CodImpuesto3,
'%.02f' % ValorImpuestoPara.get(CodImpuesto3, 0.0),
'%.02f' % ValorTotalPagar,
'%s' % NitOFE,
'%s' % NumAdq,
'%s' % ClTec,
'%d' % TipoAmb,
]
cufe = "".join(formatVars)
# crear hash...
h = hashlib.sha384()
h.update(cufe.encode('utf-8'))
return h.hexdigest()