Compare commits
	
		
			16 Commits
		
	
	
		
			quantity-f
			...
			v0.2.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 50b1a13c0a | ||
|  | 4e68025e48 | ||
|  | ead21bd4f2 | ||
|  | 5e79850686 | ||
|  | 988d01daf7 | ||
|  | 28a963b76a | ||
|  | af99a7593a | ||
|  | d02c0b13b8 | ||
|  | 79209964e0 | ||
|  | 38f4c5ae45 | ||
|  | 1143b26988 | ||
|  | e571009945 | ||
|  | 48619106c5 | ||
|  | bcf5120d82 | ||
|  | f648188834 | ||
|  | 67156ec9a6 | 
							
								
								
									
										20
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.rst
									
									
									
									
									
								
							| @@ -2,8 +2,6 @@ | ||||
| facho | ||||
| ===== | ||||
|  | ||||
| !!INESTABLE NO RECOMENDAMOS USO PARA PRODUCION!! | ||||
|  | ||||
| Libreria para facturacion electronica colombia. | ||||
|  | ||||
| - facho/facho.py: abstracion para manipulacion del XML | ||||
| @@ -24,7 +22,7 @@ usando pip:: | ||||
| CLI | ||||
| === | ||||
|  | ||||
| tambien se provee linea de comandos **facho** para firmado y envio de documentos:: | ||||
| tambien se provee linea de comandos **facho** para generacion, firmado y envio de documentos:: | ||||
|   facho --help | ||||
|  | ||||
| CONTRIBUIR | ||||
| @@ -32,17 +30,13 @@ CONTRIBUIR | ||||
|  | ||||
| ver **CONTRIBUTING.rst** | ||||
|  | ||||
| USO | ||||
| === | ||||
|  | ||||
| ver **USAGE.rst** | ||||
|  | ||||
|  | ||||
| DIAN HABILITACION | ||||
| ================= | ||||
|  | ||||
| guia oficial actualizada al 2020-04-20: https://www.dian.gov.co/fizcalizacioncontrol/herramienconsulta/FacturaElectronica/Facturaci%C3%B3n_Gratuita_DIAN/Documents/Guia_usuario_08052019.pdf#search=numeracion | ||||
|  | ||||
|  | ||||
| ERROR X509SerialNumber | ||||
| ====================== | ||||
|  | ||||
|  | ||||
| lxml.etree.DocumentInvalid: Element '{http://www.w3.org/2000/09/xmldsig#}X509SerialNumber': '632837201711293159666920255411738137494572618415' is not a valid value of the atomic type 'xs:integer' | ||||
|  | ||||
| Actualmente el xmlschema usado por xmlsig para el campo X509SerialNumber es tipo | ||||
| integer ahi que parchar manualmente a tipo string, en el archivo site-packages/xmlsig/data/xmldsig-core-schema.xsd. | ||||
|   | ||||
							
								
								
									
										24
									
								
								USAGE.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								USAGE.rst
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| uso de la libreria | ||||
| ================== | ||||
|  | ||||
| **facho** es tanto una libreria para modelar y generar los documentos xml requeridos para la facturacion, | ||||
| asi como una herramienta de **consola** para facilitar algunas actividades como: generaciones de xml | ||||
| apartir de una especificacion en python, comprimir y enviar archivos según el SOAP vigente. | ||||
|  | ||||
| **facho** es diseñado para ser usado en conjunto con el documento **docs/DIAN/Anexo_Tecnico_Factura_Electronica_Vr1_7_2020.pdf**, ya que en gran medida sigue la terminologia presente en este. | ||||
|  | ||||
|  | ||||
| Para ejemplos ver **examples/** . | ||||
|  | ||||
| En terminos generales seria modelar la factura usando **facho/fe/form.py**, instanciar las extensiones requeridas ver **facho/fe/fe.py** y | ||||
| una vez generado el objeto invoice y las extensiones requeridas se procede a crear el XML, ejemplo: | ||||
|  | ||||
| ~~~python | ||||
| .... | ||||
| xml = form_xml.DIANInvoiceXML(invoice) | ||||
| extensions = module.extensions(invoice) | ||||
| for extension in extensions: | ||||
|   xml.add_extension(extension) | ||||
|  | ||||
| form_xml.DIANWriteSigned(xml, "factura.xml", "llave privada", "frase") | ||||
| ~~~ | ||||
							
								
								
									
										117
									
								
								examples/generate-invoice-from-cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								examples/generate-invoice-from-cli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| # este archivo es un ejemplo para le generacion | ||||
| # una factura de venta nacional usando el comando **facho**. | ||||
| # | ||||
| # ejemplo: facho generate-invoice generate-invoice-from-cli.py | ||||
| # | ||||
| # importar libreria de modelos | ||||
| from facho.fe import form | ||||
| from facho.fe import form_xml | ||||
|  | ||||
| # importar libreria extensiones xml para cumplir decreto | ||||
| from facho.fe import fe | ||||
|  | ||||
| # importar otras necesarias | ||||
| from datetime import datetime, date | ||||
|  | ||||
| # Datos del fomulario del SET de pruebas | ||||
| INVOICE_AUTHORIZATION = '181360000001' #Número suministrado por la Dian en el momento de la creación del SET de Pruebas   | ||||
| ID_SOFTWARE = '57bcb6d1-c591-5a90-b80a-cb030ec91440' #Id suministrado por la Dian en el momento de la creación del SET de Pruebas   | ||||
| PIN = '19642' #Número creado por la empresa para poder crear el SET de pruebas | ||||
| CLAVE_TECNICA = 'fc9eac422eba16e21ffd8c5f94b3f30a6e38162d' ##Id suministrado por la Dian en el momento de la creación del SET de Pruebas   | ||||
|  | ||||
|  | ||||
| # callback que retonar las extensiones XML necesarias | ||||
| # para que el documento final XML cumpla el decreto. | ||||
| # | ||||
| # muchos de los valores usados son obtenidos | ||||
| # del servicio web de la DIAN. | ||||
| def extensions(inv): | ||||
|     security_code = fe.DianXMLExtensionSoftwareSecurityCode(ID_SOFTWARE, PIN, inv.invoice_ident) | ||||
|     authorization_provider = fe.DianXMLExtensionAuthorizationProvider() | ||||
|     cufe = fe.DianXMLExtensionCUFE(inv, CLAVE_TECNICA, fe.AMBIENTE_PRUEBAS) | ||||
|     software_provider = fe.DianXMLExtensionSoftwareProvider('nit_empresa', 'dígito_verificación', ID_SOFTWARE) | ||||
|     inv_authorization = fe.DianXMLExtensionInvoiceAuthorization(INVOICE_AUTHORIZATION, | ||||
|                                                                 datetime(2019, 1, 19),#Datos toamdos de  | ||||
|                                                                 datetime(2030, 1, 19),#la configuración  | ||||
|                                                                 'SETP', 990000000, 995000000)#del SET de pruebas | ||||
|     return [security_code, authorization_provider, cufe, software_provider, inv_authorization] | ||||
|  | ||||
| def invoice(): | ||||
|     # factura de venta nacional | ||||
|     inv = form.Invoice('01') | ||||
|     # asignar periodo de facturacion | ||||
|     inv.set_period(datetime.now(), datetime.now()) | ||||
|     # asignar fecha de emision de la factura | ||||
|     inv.set_issue(datetime.now()) | ||||
|     # asignar prefijo y numero del documento | ||||
|     inv.set_ident('SETP990000008') | ||||
|     # asignar tipo de operacion ver DIAN:6.1.5 | ||||
|     inv.set_operation_type('10') | ||||
|     inv.set_supplier(form.Party( | ||||
|         legal_name = 'Nombre registrado de la empresa', | ||||
|         name = 'Nombre comercial o él mismo nombre registrado', | ||||
|         ident = form.PartyIdentification('nit_empresa', 'digito_verificación', '31'), | ||||
|         # obligaciones del contribuyente ver DIAN:FAK26 | ||||
|         responsability_code = form.Responsability(['O-07', 'O-14', 'O-48']), | ||||
|         # ver DIAN:FAJ28 | ||||
|         responsability_regime_code = '48', | ||||
|         # tipo de organizacion juridica ver DIAN:6.2.3 | ||||
|         organization_code = '1', | ||||
|         email = "correoempresa@correoempresa.correo", | ||||
|         address = form.Address( | ||||
|             '', '', form.City('05001', 'Medellín'), | ||||
|             form.Country('CO', 'Colombia'), | ||||
|             form.CountrySubentity('05', 'Antioquia')), | ||||
|     )) | ||||
|     #Tercero a quien se le factura | ||||
|     inv.set_customer(form.Party( | ||||
|         legal_name = 'consumidor final', | ||||
|         name = 'consumidor final', | ||||
|         ident = form.PartyIdentification('222222222222', '', '13'), | ||||
|         responsability_code = form.Responsability(['R-99-PN']), | ||||
|         responsability_regime_code = '49', | ||||
|         organization_code = '2', | ||||
|         email = "consumidor_final0final.final", | ||||
|         address = form.Address( | ||||
|             '', '', form.City('05001', 'Medellín'), | ||||
|             form.Country('CO', 'Colombia'), | ||||
|             form.CountrySubentity('05', 'Antioquia')), | ||||
| 		#tax_scheme = form.TaxScheme('01', 'IVA') | ||||
|     )) | ||||
|     # asignar metodo de pago     | ||||
|     inv.set_payment_mean(form.PaymentMean( | ||||
|         # metodo de pago ver DIAN:3.4.1 | ||||
|         id = '1', | ||||
|         # codigo correspondiente al medio de pago ver DIAN:3.4.2 | ||||
|         code = '20', | ||||
|         # fecha de vencimiento de la factura         | ||||
|         due_at = datetime.now(), | ||||
|         # identificador numerico | ||||
|         payment_id = '2' | ||||
|     )) | ||||
|     # adicionar una linea al documento | ||||
|     inv.add_invoice_line(form.InvoiceLine( | ||||
|         quantity = form.Quantity(int(20.5), '94'), | ||||
|         # item general de codigo 999 | ||||
|         description = 'productO3', | ||||
|         item = form.StandardItem('test', 9999), | ||||
|         price = form.Price( | ||||
|             # precio base del item (sin iva) | ||||
|             amount = form.Amount(200.00), | ||||
|             # ver DIAN:6.3.5.1 | ||||
|             type_code = '01', | ||||
|             type = 'x' | ||||
|         ), | ||||
|         tax = form.TaxTotal( | ||||
|             subtotals = [ | ||||
|                 form.TaxSubTotal( | ||||
|                     percent = 19.00, | ||||
|                     scheme=form.TaxScheme('01') | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     )) | ||||
|     return inv | ||||
|  | ||||
| def document_xml(): | ||||
|     return form_xml.DIANInvoiceXML | ||||
| @@ -1,74 +0,0 @@ | ||||
| import facho.fe.form as form | ||||
| from facho.fe import fe | ||||
| from datetime import datetime | ||||
|  | ||||
| def extensions(inv): | ||||
|     nit = form.PartyIdentification('nit', '5', '31') | ||||
|     security_code = fe.DianXMLExtensionSoftwareSecurityCode('id software', 'pin', inv.invoice_ident) | ||||
|     authorization_provider = fe.DianXMLExtensionAuthorizationProvider() | ||||
|     cufe = fe.DianXMLExtensionCUFE(inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS, | ||||
|                                    'clave tecnica') | ||||
|     software_provider = fe.DianXMLExtensionSoftwareProvider(nit, nit.dv, 'id software') | ||||
|     inv_authorization = fe.DianXMLExtensionInvoiceAuthorization('invoice autorization', | ||||
|                                                                 datetime(2019, 1, 19), | ||||
|                                                                 datetime(2030, 1, 19), | ||||
|                                                                 'SETP', 990000001, 995000000) | ||||
|     return [security_code, authorization_provider, cufe, software_provider, inv_authorization] | ||||
|  | ||||
|  | ||||
| def invoice(): | ||||
|     inv = form.Invoice() | ||||
|     inv.set_period(datetime.now(), datetime.now()) | ||||
|     inv.set_issue(datetime.now()) | ||||
|     inv.set_ident('SETP990003033') | ||||
|     inv.set_operation_type('10') | ||||
|     inv.set_supplier(form.Party( | ||||
|         legal_name = 'FACHO SOS', | ||||
|         name = 'FACHO SOS', | ||||
|         ident = form.PartyIdentification('900579212', '5', '31'), | ||||
|         responsability_code = form.Responsability(['O-07', 'O-09', 'O-14', 'O-48']), | ||||
|         responsability_regime_code = '48', | ||||
|         organization_code = '1', | ||||
|         email = "sdds@sd.com", | ||||
|         address = form.Address( | ||||
|             '', '', form.City('05001', 'Medellín'), | ||||
|             form.Country('CO', 'Colombia'), | ||||
|             form.CountrySubentity('05', 'Antioquia')) | ||||
|     )) | ||||
|     inv.set_customer(form.Party( | ||||
|         legal_name = 'facho-customer', | ||||
|         name = 'facho-customer', | ||||
|         ident = form.PartyIdentification('999999999', '', '13'), | ||||
|         responsability_code = form.Responsability(['R-99-PN']), | ||||
|         responsability_regime_code = '49', | ||||
|         organization_code = '2', | ||||
|         email = "sdds@sd.com", | ||||
|         address = form.Address( | ||||
|             '', '', form.City('05001', 'Medellín'), | ||||
|             form.Country('CO', 'Colombia'), | ||||
|             form.CountrySubentity('05', 'Antioquia')) | ||||
|     )) | ||||
|     inv.set_payment_mean(form.PaymentMean( | ||||
|         id = '1', | ||||
|         code = '10', | ||||
|         due_at = datetime.now(), | ||||
|         payment_id = '1' | ||||
|     )) | ||||
|     inv.add_invoice_line(form.InvoiceLine( | ||||
|         quantity = form.Quantity(1, '94'), | ||||
|         description = 'producto facho', | ||||
|         item = form.StandardItem('test', 9999), | ||||
|         price = form.Price( | ||||
|             amount = form.Amount(100.00), | ||||
|             type_code = '01', | ||||
|             type = 'x' | ||||
|         ), | ||||
|         tax = form.TaxTotal( | ||||
|             subtotals = [ | ||||
|                 form.TaxSubTotal( | ||||
|                     percent = 19.00, | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     )) | ||||
|     return inv | ||||
							
								
								
									
										114
									
								
								examples/use-as-lib.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								examples/use-as-lib.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| # importar libreria de modelos | ||||
| import facho.fe.form as form | ||||
| import facho.fe.form_xml | ||||
|  | ||||
| PRIVATE_KEY_PATH='ruta a mi llave privada' | ||||
| PRIVATE_PASSPHRASE='clave de la llave privada' | ||||
|  | ||||
| # consultar las extensiones necesarias | ||||
| def extensions(inv): | ||||
|     security_code = fe.DianXMLExtensionSoftwareSecurityCode('id software', 'pin', inv.invoice_ident) | ||||
|     authorization_provider = fe.DianXMLExtensionAuthorizationProvider() | ||||
|     cufe = fe.DianXMLExtensionCUFE(inv, fe.DianXMLExtensionCUFE.AMBIENTE_PRUEBAS, | ||||
|                                    'clave tecnica') | ||||
|     nit = form.PartyIdentification('nit', '5', '31') | ||||
|     software_provider = fe.DianXMLExtensionSoftwareProvider(nit, nit.dv, 'id software') | ||||
|     inv_authorization = fe.DianXMLExtensionInvoiceAuthorization('invoice autorization', | ||||
|                                                                 datetime(2019, 1, 19), | ||||
|                                                                 datetime(2030, 1, 19), | ||||
|                                                                 'SETP', 990000001, 995000000) | ||||
|     return [security_code, authorization_provider, cufe, software_provider, inv_authorization] | ||||
|  | ||||
| # generar documento desde modelo a ruta indicada | ||||
| def generate_document(invoice, filepath): | ||||
|     xml = form_xml.DIANInvoiceXML(invoice) | ||||
|     for extension in extensions(invoice): | ||||
|         xml.add_extension(extension) | ||||
|     form_xml.utils.DIANWriteSigned(xml, filepath, PRIVATE_KEY_PATH, PRIVATE_PASSPHRASE, True) | ||||
|  | ||||
| # Modelars las facturas | ||||
| # ... | ||||
|  | ||||
| # factura de venta nacional | ||||
| inv = form.NationalSalesInvoice() | ||||
| # asignar periodo de facturacion | ||||
| inv.set_period(datetime.now(), datetime.now()) | ||||
| # asignar fecha de emision de la factura | ||||
| inv.set_issue(datetime.now()) | ||||
| # asignar prefijo y numero del documento | ||||
| inv.set_ident('SETP990003033') | ||||
| # asignar tipo de operacion ver DIAN:6.1.5 | ||||
| inv.set_operation_type('10') | ||||
| # asignar proveedor | ||||
| inv.set_supplier(form.Party( | ||||
|     legal_name = 'FACHO SOS', | ||||
|     name = 'FACHO SOS', | ||||
|     ident = form.PartyIdentification('900579212', '5', '31'), | ||||
|     # obligaciones del contribuyente ver DIAN:FAK26 | ||||
|     responsability_code = form.Responsability(['O-07', 'O-09', 'O-14', 'O-48']), | ||||
|     # ver DIAN:FAJ28 | ||||
|     responsability_regime_code = '48', | ||||
|     # tipo de organizacion juridica ver DIAN:6.2.3 | ||||
|     organization_code = '1', | ||||
|     email = "sdds@sd.com", | ||||
|     address = form.Address( | ||||
|         name = '', | ||||
|         street = '', | ||||
|         city = form.City('05001', 'Medellín'), | ||||
|         country = form.Country('CO', 'Colombia'), | ||||
|         countrysubentity = form.CountrySubentity('05', 'Antioquia')) | ||||
| )) | ||||
| inv.set_customer(form.Party( | ||||
|     legal_name = 'facho-customer', | ||||
|     name = 'facho-customer', | ||||
|     ident = form.PartyIdentification('999999999', '', '13'), | ||||
|     responsability_code = form.Responsability(['R-99-PN']), | ||||
|     responsability_regime_code = '49', | ||||
|     organization_code = '2', | ||||
|     email = "sdds@sd.com", | ||||
|     address = form.Address( | ||||
|         name = '', | ||||
|         street = '', | ||||
|         city = form.City('05001', 'Medellín'), | ||||
|         country = form.Country('CO', 'Colombia'), | ||||
|         countrysubentity = form.CountrySubentity('05', 'Antioquia')) | ||||
| )) | ||||
| # asignar metodo de pago | ||||
| inv.set_payment_mean(form.PaymentMean( | ||||
|     # metodo de pago ver DIAN:3.4.1 | ||||
|     id = '1', | ||||
|     # codigo correspondiente al medio de pago ver DIAN:3.4.2 | ||||
|     code = '10', | ||||
|     # fecha de vencimiento de la factura | ||||
|     due_at = datetime.now(), | ||||
|      | ||||
|     # identificador numerico | ||||
|     payment_id = '1' | ||||
| )) | ||||
| # adicionar una linea al documento | ||||
| inv.add_invoice_line(form.InvoiceLine( | ||||
|     quantity = form.Quantity(1, '94'), | ||||
|     description = 'producto facho', | ||||
|     # item general de codigo 999 | ||||
|     item = form.StandardItem('test', 9999), | ||||
|     price = form.Price( | ||||
|         # precio base del tiem | ||||
|         amount = form.Amount(100.00), | ||||
|         # ver DIAN:6.3.5.1 | ||||
|         type_code = '01', | ||||
|         type = 'x' | ||||
|     ), | ||||
|     tax = form.TaxTotal( | ||||
|         subtotals = [ | ||||
|             form.TaxSubTotal( | ||||
|                 percent = 19.00, | ||||
|             ) | ||||
|         ] | ||||
|     ) | ||||
| )) | ||||
|  | ||||
| # refrescar valores de la factura | ||||
| inv.calculate() | ||||
|  | ||||
| # generar el documento xml en la ruta indicada | ||||
| generate_document(inv, 'ruta a donde guardar el .xml') | ||||
| @@ -129,6 +129,11 @@ class FachoXML: | ||||
|         self.xpath_for = {} | ||||
|         self.extensions = [] | ||||
|  | ||||
|     @classmethod | ||||
|     def from_string(cls, document: str, namespaces: dict() = []) -> 'FachoXML': | ||||
|         xml = LXMLBuilder.from_string(document) | ||||
|         return FachoXML(xml, nsmap=namespaces) | ||||
|  | ||||
|     def append_element(self, elem, new_elem): | ||||
|         #elem = self.find_or_create_element(xpath, append=append) | ||||
|         #self.builder.append(elem, new_elem) | ||||
|   | ||||
| @@ -138,10 +138,16 @@ class GetStatusResponse: | ||||
|  | ||||
|     @classmethod | ||||
|     def fromdict(cls, data): | ||||
|         if data['ErrorMessage']: | ||||
|             error_message = data['ErrorMessage']['string'] | ||||
|         else: | ||||
|             error_message = None | ||||
|              | ||||
|         return cls(data['IsValid'], | ||||
|                    data['StatusDescription'], | ||||
|                    data['StatusCode'], | ||||
|                    data['ErrorMessage']['string']) | ||||
|                    error_message) | ||||
|                     | ||||
|  | ||||
| @dataclass | ||||
| class GetStatus(SOAPService): | ||||
|   | ||||
| @@ -14,6 +14,7 @@ from contextlib import contextmanager | ||||
| from .data.dian import codelist | ||||
| from . import form | ||||
| from collections import defaultdict | ||||
| from pathlib import Path | ||||
|  | ||||
| AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] | ||||
| AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['code'] | ||||
| @@ -25,8 +26,9 @@ SCHEME_AGENCY_ATTRS = { | ||||
| } | ||||
|  | ||||
|  | ||||
| pwd = Path(__file__).parent | ||||
| # RESOLUCION 0001: pagina 516 | ||||
| POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' | ||||
| POLICY_ID = 'file://'+str(pwd)+'/data/dian/politicadefirmav2.pdf' | ||||
| POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.' | ||||
|  | ||||
|  | ||||
| @@ -49,8 +51,7 @@ NAMESPACES = { | ||||
| } | ||||
|  | ||||
| def fe_from_string(document: str) -> FachoXML: | ||||
|     xml = LXMLBuilder.from_string(document) | ||||
|     return FachoXML(xml, nsmap=NAMESPACES) | ||||
|     return FeXML.from_string(document) | ||||
|  | ||||
| from contextlib import contextmanager | ||||
| @contextmanager | ||||
| @@ -69,6 +70,7 @@ def mock_xades_policy(): | ||||
|         mock.return_value = UrllibPolicyMock() | ||||
|         yield | ||||
|  | ||||
|          | ||||
| class FeXML(FachoXML): | ||||
|  | ||||
|     def __init__(self, root, namespace): | ||||
| @@ -79,6 +81,10 @@ class FeXML(FachoXML): | ||||
|         self._cn = root.rstrip('/') | ||||
|         #self.find_or_create_element(self._cn) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_string(cls, document: str) -> 'FeXML': | ||||
|         return super().from_string(document, namespaces=NAMESPACES) | ||||
|      | ||||
|     def tostring(self, **kw): | ||||
|         return super().tostring(**kw)\ | ||||
|             .replace("fe:", "")\ | ||||
| @@ -113,7 +119,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension): | ||||
|         fachoxml.set_element('./cbc:UUID', cufe, | ||||
|                              schemeID=self.tipo_ambiente, | ||||
|                              schemeName=self.schemeName()) | ||||
|         fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1') | ||||
|         #DIAN 1.8.-2021: FAD03 | ||||
|         fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') | ||||
|         fachoxml.set_element('./cbc:ProfileExecutionID', self._tipo_ambiente_int()) | ||||
|         #DIAN 1.7.-2020: FAB36 | ||||
|         fachoxml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode', | ||||
| @@ -185,14 +192,14 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE): | ||||
|             '%s' % build_vars['NumFac'], | ||||
|             '%s' % build_vars['FecFac'], | ||||
|             '%s' % build_vars['HoraFac'], | ||||
|             form.Amount(build_vars['ValorBruto']).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorBruto']).truncate_as_string(2), | ||||
|             CodImpuesto1, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).format('%.02f'), | ||||
|             build_vars['ValorImpuestoPara'].get(CodImpuesto1, form.Amount(0.0)).truncate_as_string(2), | ||||
|             CodImpuesto2, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).format('%.02f'), | ||||
|             build_vars['ValorImpuestoPara'].get(CodImpuesto2, form.Amount(0.0)).truncate_as_string(2), | ||||
|             CodImpuesto3, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorTotalPagar']).format('%.02f'), | ||||
|             build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2), | ||||
|             build_vars['ValorTotalPagar'].truncate_as_string(2), | ||||
|             '%s' % build_vars['NitOFE'], | ||||
|             '%s' % build_vars['NumAdq'], | ||||
|             '%s' % build_vars['ClTec'], | ||||
| @@ -222,14 +229,14 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE): | ||||
|             '%s' % build_vars['NumFac'], | ||||
|             '%s' % build_vars['FecFac'], | ||||
|             '%s' % build_vars['HoraFac'], | ||||
|             form.Amount(build_vars['ValorBruto']).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorBruto']).truncate_as_string(2), | ||||
|             CodImpuesto1, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto1, 0.0)).truncate_as_string(2), | ||||
|             CodImpuesto2, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto2, 0.0)).truncate_as_string(2), | ||||
|             CodImpuesto3, | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorTotalPagar']).format('%.02f'), | ||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2), | ||||
|             form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2), | ||||
|             '%s' % build_vars['NitOFE'], | ||||
|             '%s' % build_vars['NumAdq'], | ||||
|             '%s' % build_vars['Software-PIN'], | ||||
| @@ -277,16 +284,24 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension): | ||||
| class DianXMLExtensionSigner: | ||||
|  | ||||
|     def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False): | ||||
|         self._pkcs12_path = pkcs12_path | ||||
|         self._pkcs12_data = open(pkcs12_path, 'rb').read() | ||||
|         self._passphrase = None | ||||
|         self._mockpolicy = mockpolicy | ||||
|         if passphrase: | ||||
|             self._passphrase = passphrase.encode('utf-8') | ||||
|  | ||||
|     @classmethod | ||||
|     def from_pkcs12(self, filepath, password=None): | ||||
|         p12 = OpenSSL.crypto.load_pkcs12(open(filepath, 'rb').read(), password) | ||||
|  | ||||
|     def from_bytes(cls, data, passphrase=None, mockpolicy=False): | ||||
|         self = cls.__new__(cls) | ||||
|          | ||||
|         self._pkcs12_data = data | ||||
|         self._passphrase = None | ||||
|         self._mockpolicy = mockpolicy | ||||
|         if passphrase: | ||||
|             self._passphrase = passphrase.encode('utf-8') | ||||
|              | ||||
|         return self | ||||
|      | ||||
|     def sign_xml_string(self, document): | ||||
|         xml = LXMLBuilder.from_string(document) | ||||
|         signature = self.sign_xml_element(xml) | ||||
| @@ -341,7 +356,7 @@ class DianXMLExtensionSigner: | ||||
|             POLICY_NAME, | ||||
|             xmlsig.constants.TransformSha256) | ||||
|         ctx = xades.XAdESContext(policy) | ||||
|         ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(open(self._pkcs12_path, 'rb').read(), | ||||
|         ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(self._pkcs12_data, | ||||
|                                                    self._passphrase)) | ||||
|  | ||||
|         if self._mockpolicy: | ||||
| @@ -360,6 +375,7 @@ class DianXMLExtensionSigner: | ||||
|         extcontent = fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') | ||||
|         fachoxml.append_element(extcontent, signature) | ||||
|  | ||||
|          | ||||
| class DianXMLExtensionAuthorizationProvider(FachoXMLExtension): | ||||
|     # RESOLUCION 0004: pagina 176 | ||||
|  | ||||
| @@ -429,7 +445,7 @@ class DianZIP: | ||||
|     MAX_FILES = 50 | ||||
|  | ||||
|     def __init__(self, file_like): | ||||
|         self.zipfile = zipfile.ZipFile(file_like, mode='w') | ||||
|         self.zipfile = zipfile.ZipFile(file_like, mode='w', compression=zipfile.ZIP_DEFLATED) | ||||
|         self.num_files = 0 | ||||
|  | ||||
|     def add_invoice_xml(self, name, xml_data): | ||||
| @@ -452,8 +468,8 @@ class DianZIP: | ||||
|  | ||||
| class DianXMLExtensionSignerVerifier: | ||||
|  | ||||
|     def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False): | ||||
|         self._pkcs12_path = pkcs12_path | ||||
|     def __init__(self, pkcs12_path_or_bytes, passphrase=None, mockpolicy=False): | ||||
|         self._pkcs12_path_or_bytes = pkcs12_path_or_bytes | ||||
|         self._passphrase = None | ||||
|         self._mockpolicy = mockpolicy | ||||
|         if passphrase: | ||||
| @@ -470,7 +486,12 @@ class DianXMLExtensionSignerVerifier: | ||||
|         fachoxml.root.append(signature) | ||||
|  | ||||
|         ctx = xades.XAdESContext() | ||||
|         ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(open(self._pkcs12_path, 'rb').read(), | ||||
|  | ||||
|         pkcs12_data = self._pkcs12_path_or_bytes | ||||
|         if isinstance(self._pkcs12_path_or_bytes, str): | ||||
|             pkcs12_data = open(self._pkcs12_path_or_bytes, 'rb').read() | ||||
|  | ||||
|         ctx.load_pkcs12(OpenSSL.crypto.load_pkcs12(pkcs12_data, | ||||
|                                                    self._passphrase)) | ||||
|  | ||||
|         try: | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| import hashlib | ||||
| from functools import reduce | ||||
| import copy | ||||
| import dataclasses | ||||
| from dataclasses import dataclass | ||||
| from datetime import datetime, date | ||||
| from collections import defaultdict | ||||
| @@ -53,7 +54,7 @@ class AmountCollection(Collection): | ||||
|         return total | ||||
|  | ||||
| class Amount: | ||||
|     def __init__(self, amount: int or float or Amount, currency: Currency = Currency('COP')): | ||||
|     def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')): | ||||
|  | ||||
|         #DIAN 1.7.-2020: 1.2.3.1 | ||||
|         if isinstance(amount, Amount): | ||||
| @@ -63,7 +64,7 @@ class Amount: | ||||
|             self.amount = amount.amount | ||||
|             self.currency = amount.currency | ||||
|         else: | ||||
|             if amount < 0: | ||||
|             if float(amount) < 0: | ||||
|                 raise ValueError('amount must be positive >= 0') | ||||
|  | ||||
|             self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, | ||||
| @@ -71,12 +72,17 @@ class Amount: | ||||
|                                                           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) | ||||
|  | ||||
|     def __round__(self, prec): | ||||
|         return round(self.amount, prec) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return '%.06f' % self.amount | ||||
|         return str(self.float()) | ||||
|  | ||||
|     def __lt__(self, other): | ||||
|         if not self.is_same_currency(other): | ||||
| @@ -88,17 +94,27 @@ class Amount: | ||||
|             raise AmountCurrencyError() | ||||
|         return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) | ||||
|  | ||||
|     def __add__(self, other): | ||||
|     def _cast(self, val): | ||||
|         if type(val) in [int, float]: | ||||
|             return self.fromNumber(val) | ||||
|         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): | ||||
|             raise AmountCurrencyError() | ||||
|         return Amount(self.amount + other.amount, self.currency) | ||||
|  | ||||
|     def __sub__(self, other): | ||||
|     def __sub__(self, rother): | ||||
|         other = self._cast(rother) | ||||
|         if not self.is_same_currency(other): | ||||
|             raise AmountCurrencyError() | ||||
|         return Amount(self.amount - other.amount, self.currency) | ||||
|  | ||||
|     def __mul__(self, other): | ||||
|     def __mul__(self, rother): | ||||
|         other = self._cast(rother) | ||||
|         if not self.is_same_currency(other): | ||||
|             raise AmountCurrencyError() | ||||
|         return Amount(self.amount * other.amount, self.currency) | ||||
| @@ -106,8 +122,9 @@ class Amount: | ||||
|     def is_same_currency(self, other): | ||||
|         return self.currency == other.currency | ||||
|  | ||||
|     def format(self, formatter): | ||||
|         return formatter % self.float() | ||||
|     def truncate_as_string(self, prec): | ||||
|         parts = str(self.float()).split('.', 1) | ||||
|         return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0')) | ||||
|  | ||||
|     def float(self): | ||||
|         return float(round(self.amount, DECIMAL_PRECISION)) | ||||
| @@ -116,27 +133,25 @@ class Amount: | ||||
| class Quantity: | ||||
|      | ||||
|     def __init__(self, val, code): | ||||
|         if not isinstance(val, int): | ||||
|             raise ValueError('val expected int') | ||||
|         if type(val) not in [float, int]: | ||||
|             raise ValueError('val expected int or float') | ||||
|         if code not in codelist.UnidadesMedida: | ||||
|             raise ValueError("code [%s] not found" % (code)) | ||||
|  | ||||
|         self.value = val | ||||
|         self.value = Amount(val) | ||||
|         self.code = code | ||||
|  | ||||
|     def __mul__(self, other): | ||||
|         if isinstance(other, Amount): | ||||
|             return Amount(self.value) * other | ||||
|         return self.value * other | ||||
|  | ||||
|     def __lt__(self, other): | ||||
|         if isinstance(other, Amount): | ||||
|             return Amount(self.value) < other | ||||
|         return self.value < other | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.value) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return str(self) | ||||
|  | ||||
| @dataclass | ||||
| class Item: | ||||
| @@ -323,11 +338,15 @@ class Price: | ||||
|     amount: Amount | ||||
|     type_code: str | ||||
|     type: str | ||||
|     quantity: int = 1 | ||||
|  | ||||
|     def __post_init__(self): | ||||
|         if self.type_code not in codelist.CodigoPrecioReferencia: | ||||
|             raise ValueError("type_code [%s] not found" % (self.type_code)) | ||||
|         if not isinstance(self.quantity, int): | ||||
|             raise ValueError("quantity must be int") | ||||
|  | ||||
|         self.amount *= self.quantity | ||||
|  | ||||
| @dataclass | ||||
| class PaymentMean: | ||||
| @@ -378,6 +397,52 @@ class InvoiceDocumentReference(BillingReference): | ||||
|     date: fecha de emision de la nota credito relacionada | ||||
|     """ | ||||
|  | ||||
| @dataclass | ||||
| class AllowanceChargeReason: | ||||
|     code: str | ||||
|     reason: str | ||||
|  | ||||
|     def __post_init__(self): | ||||
|         if self.code not in codelist.CodigoDescuento: | ||||
|             raise ValueError("code [%s] not found" % (self.code)) | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class AllowanceCharge: | ||||
|     #DIAN 1.7.-2020: FAQ03 | ||||
|     charge_indicator: bool = True | ||||
|     amount: Amount = Amount(0.0) | ||||
|     reason: AllowanceChargeReason = None | ||||
|  | ||||
|     #Valor Base para calcular el descuento o el cargo | ||||
|     base_amount: typing.Optional[Amount] = Amount(0.0) | ||||
|      | ||||
|     # Porcentaje: Porcentaje que aplicar. | ||||
|     multiplier_factor_numeric: Amount = Amount(1.0) | ||||
|      | ||||
|     def isCharge(self): | ||||
|         return self.charge_indicator == True | ||||
|  | ||||
|     def isDiscount(self): | ||||
|         return self.charge_indicator == False | ||||
|  | ||||
|     def asCharge(self): | ||||
|         self.charge_indicator = True | ||||
|  | ||||
|     def asDiscount(self): | ||||
|         self.charge_indicator = False | ||||
|  | ||||
|     def hasReason(self): | ||||
|         return self.reason is not None | ||||
|  | ||||
|     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 | ||||
| @@ -391,9 +456,31 @@ class InvoiceLine: | ||||
|     # de subtotal | ||||
|     tax: typing.Optional[TaxTotal] | ||||
|  | ||||
|     allowance_charge: typing.List[AllowanceCharge] = dataclasses.field(default_factory=list) | ||||
|  | ||||
|     def add_allowance_charge(self, charge): | ||||
|         if not isinstance(charge, AllowanceCharge): | ||||
|             raise TypeError('charge invalid type expected AllowanceCharge') | ||||
|         charge.set_base_amount(self.total_amount_without_charge) | ||||
|         self.allowance_charge.add(charge) | ||||
|  | ||||
|     @property | ||||
|     def total_amount_without_charge(self): | ||||
|         return (self.quantity * self.price.amount) | ||||
|      | ||||
|     @property | ||||
|     def total_amount(self): | ||||
|         return self.quantity * self.price.amount | ||||
|         charge = AmountCollection(self.allowance_charge)\ | ||||
|             .filter(lambda charge: charge.isCharge())\ | ||||
|             .map(lambda charge: charge.amount)\ | ||||
|             .sum() | ||||
|  | ||||
|         discount = AmountCollection(self.allowance_charge)\ | ||||
|             .filter(lambda charge: charge.isDiscount())\ | ||||
|             .map(lambda charge: charge.amount)\ | ||||
|             .sum() | ||||
|  | ||||
|         return self.total_amount_without_charge + charge - discount | ||||
|  | ||||
|     @property | ||||
|     def total_tax_inclusive_amount(self): | ||||
| @@ -441,37 +528,6 @@ class LegalMonetaryTotal: | ||||
|             - self.prepaid_amount | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class AllowanceChargeReason: | ||||
|     code: str | ||||
|     reason: str | ||||
|  | ||||
|     def __post_init__(self): | ||||
|         if self.code not in codelist.CodigoDescuento: | ||||
|             raise ValueError("code [%s] not found" % (self.code)) | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class AllowanceCharge: | ||||
|     #DIAN 1.7.-2020: FAQ03 | ||||
|     charge_indicator: bool = True | ||||
|     amount: Amount = Amount(0.0) | ||||
|     reason: AllowanceChargeReason = None | ||||
|  | ||||
|     def isCharge(self): | ||||
|         return self.charge_indicator == True | ||||
|  | ||||
|     def isDiscount(self): | ||||
|         return self.charge_indicator == False | ||||
|  | ||||
|     def asCharge(self): | ||||
|         self.charge_indicator = True | ||||
|  | ||||
|     def asDiscount(self): | ||||
|         self.charge_indicator = False | ||||
|  | ||||
|     def hasReason(self): | ||||
|         return self.reason is not None | ||||
|  | ||||
| class NationalSalesInvoiceDocumentType(str): | ||||
|     def __str__(self): | ||||
| @@ -570,7 +626,7 @@ class Invoice: | ||||
|  | ||||
|         self.invoice_operation_type = operation | ||||
|  | ||||
|     def add_allownace_charge(self, charge: AllowanceCharge): | ||||
|     def add_allowance_charge(self, charge: AllowanceCharge): | ||||
|         self.invoice_allowance_charge.append(charge) | ||||
|  | ||||
|     def add_invoice_line(self, line: InvoiceLine): | ||||
| @@ -618,11 +674,22 @@ class Invoice: | ||||
|         #DIAN 1.7.-2020: FAU14 | ||||
|         self.invoice_legal_monetary_total.calculate() | ||||
|  | ||||
|     def _refresh_charges_base_amount(self): | ||||
|         if self.invoice_allowance_charge: | ||||
|             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') | ||||
|              | ||||
|         # cargos a nivel de factura | ||||
|         for charge in self.invoice_allowance_charge: | ||||
|             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): | ||||
|   | ||||
| @@ -540,10 +540,31 @@ class DIANInvoiceXML(fe.FeXML): | ||||
|             line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code) | ||||
|             #DIAN 1.7.-2020: FBB04 | ||||
|             line.set_element('./cac:Price/cbc:BaseQuantity', | ||||
|                              invoice_line.quantity, | ||||
|                              invoice_line.price.quantity, | ||||
|                              unitCode=invoice_line.quantity.code) | ||||
|  | ||||
|             for idx, charge in enumerate(invoice_line.allowance_charge): | ||||
|                 next_append_charge = idx > 0 | ||||
|                 fexml.append_allowance_charge(line, index + 1, charge, append=next_append_charge) | ||||
|                  | ||||
|     def set_allowance_charge(fexml, invoice): | ||||
|         for idx, charge in enumerate(invoice.invoice_allowance_charge): | ||||
|             next_append = idx > 0 | ||||
|             fexml.append_allowance_charge(fexml, idx + 1, charge, append=next_append) | ||||
|  | ||||
|     def append_allowance_charge(fexml, parent, idx, charge, append=False): | ||||
|             line = parent.fragment('./cac:AllowanceCharge', append=append) | ||||
|             #DIAN 1.7.-2020: FAQ02 | ||||
|             line.set_element('./cbc:ID', idx) | ||||
|             #DIAN 1.7.-2020: FAQ03 | ||||
|             line.set_element('./cbc:ChargeIndicator', str(charge.charge_indicator).lower()) | ||||
|             if charge.reason: | ||||
|                 line.set_element('./cbc:AllowanceChargeReasonCode', charge.reason.code) | ||||
|                 line.set_element('./cbc:allowanceChargeReason', charge.reason.reason) | ||||
|             line.set_element('./cbc:MultiplierFactorNumeric', str(round(charge.multiplier_factor_numeric, 2))) | ||||
|             fexml.set_element_amount_for(line, './cbc:Amount', charge.amount) | ||||
|             fexml.set_element_amount_for(line, './cbc:BaseAmount', charge.base_amount) | ||||
|              | ||||
|     def attach_invoice(fexml, invoice): | ||||
|         """adiciona etiquetas a FEXML y retorna FEXML | ||||
|         en caso de fallar validacion retorna None""" | ||||
| @@ -579,6 +600,7 @@ class DIANInvoiceXML(fe.FeXML): | ||||
|         fexml.set_invoice_totals(invoice) | ||||
|         fexml.set_invoice_lines(invoice) | ||||
|         fexml.set_payment_mean(invoice) | ||||
|         fexml.set_allowance_charge(invoice) | ||||
|         fexml.set_billing_reference(invoice) | ||||
|  | ||||
|         return fexml | ||||
|   | ||||
| @@ -20,3 +20,34 @@ def test_amount_equals(): | ||||
|     assert price1 == price2 | ||||
|     assert price1 == form.Amount(100) + form.Amount(10) | ||||
|     assert price1 == form.Amount(10) * form.Amount(10) + form.Amount(10) | ||||
|     assert form.Amount(110) == (form.Amount(1.10) * form.Amount(100)) | ||||
|  | ||||
| def test_round_half_even(): | ||||
|     # https://www.w3.org/TR/xpath-functions-31/#func-round-half-to-even | ||||
|     assert form.Amount(0.5).round(0).float() == 0.0 | ||||
|     assert form.Amount(1.5).round(0).float() == 2.0 | ||||
|     assert form.Amount(2.5).round(0).float() == 2.0 | ||||
|     assert form.Amount(3.567812e+3).round(2).float() == 3567.81e0 | ||||
|     assert form.Amount(4.7564e-3).round(2).float() == 0.0e0 | ||||
|  | ||||
| def test_round(): | ||||
|     # Entre 0 y 5 Mantener el dígito menos significativo | ||||
|     assert form.Amount(1.133).round(2) == form.Amount(1.13) | ||||
|     # Entre 6 y 9 Incrementar el dígito menos significativo | ||||
|     assert form.Amount(1.166).round(2) == form.Amount(1.17) | ||||
|     # 5, y el segundo dígito siguiente al dígito menos significativo es cero o par Mantener el dígito menos significativo | ||||
|     assert str(form.Amount(1.1542).round(2)) == str(form.Amount(1.15)) | ||||
|     # 5, y el segundo dígito siguiente al dígito menos significativo es impar Incrementar el dígito menos significativo | ||||
|     assert str(form.Amount(1.1563).round(2)) == str(form.Amount(1.16)) | ||||
|  | ||||
| def test_amount_truncate(): | ||||
|     assert form.Amount(1.1569).truncate_as_string(2) == '1.15' | ||||
|     assert form.Amount(587.0700).truncate_as_string(2) == '587.07' | ||||
|     assert form.Amount(14705.8800).truncate_as_string(2) == '14705.88' | ||||
|     assert form.Amount(9423.7000).truncate_as_string(2) == '9423.70' | ||||
|     assert form.Amount(10084.03).truncate_as_string(2) == '10084.03' | ||||
|     assert form.Amount(10000.02245).truncate_as_string(2) == '10000.02' | ||||
|     assert form.Amount(10000.02357).truncate_as_string(2) == '10000.02' | ||||
|  | ||||
| def test_amount_format(): | ||||
|     assert str(round(form.Amount(1.1569),2)) == '1.16' | ||||
|   | ||||
| @@ -110,3 +110,19 @@ def test_xml_sign_dian(monkeypatch): | ||||
|         helpers.mock_urlopen(m) | ||||
|         xmlsigned = signer.sign_xml_string(xmlstring) | ||||
|     assert "Signature" in xmlsigned | ||||
|  | ||||
| def test_xml_sign_dian_using_bytes(monkeypatch): | ||||
|     xml = fe.FeXML('Invoice', | ||||
|                 'http://www.dian.gov.co/contratos/facturaelectronica/v1') | ||||
|     xml.find_or_create_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent') | ||||
|     ublextension = xml.fragment('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension', append=True) | ||||
|     extcontent = ublextension.find_or_create_element('/ext:UBLExtension/ext:ExtensionContent') | ||||
|  | ||||
|     xmlstring = xml.tostring() | ||||
|     p12_data = open('./tests/example.p12', 'rb').read() | ||||
|     signer = fe.DianXMLExtensionSigner.from_bytes(p12_data) | ||||
|  | ||||
|     with monkeypatch.context() as m: | ||||
|         helpers.mock_urlopen(m) | ||||
|         xmlsigned = signer.sign_xml_string(xmlstring) | ||||
|     assert "Signature" in xmlsigned | ||||
|   | ||||
| @@ -55,6 +55,13 @@ def test_invoicesimple_zip(simple_invoice): | ||||
|     with fe.DianZIP(zipdata) as dianzip: | ||||
|         name_invoice = dianzip.add_invoice_xml(simple_invoice.invoice_ident, str(xml_invoice)) | ||||
|  | ||||
|     # el zip ademas de archivar debe comprimir los archivos | ||||
|     # de lo contrario la DIAN lo rechaza | ||||
|     with zipfile.ZipFile(zipdata) as dianzip: | ||||
|         dianzip.testzip() | ||||
|         for zipinfo in dianzip.infolist(): | ||||
|             assert zipinfo.compress_type == zipfile.ZIP_DEFLATED, "se espera el zip comprimido" | ||||
|  | ||||
|     with zipfile.ZipFile(zipdata) as dianzip: | ||||
|         xml_data = dianzip.open(name_invoice).read().decode('utf-8') | ||||
|         assert xml_data == str(xml_invoice) | ||||
| @@ -112,7 +119,7 @@ def test_invoice_cufe(simple_invoice_without_lines): | ||||
|     simple_invoice.invoice_supplier.ident = form.PartyIdentification('700085371', '5', '31') | ||||
|     simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31') | ||||
|     simple_invoice.add_invoice_line(form.InvoiceLine( | ||||
|         quantity = form.Quantity(1, '94'), | ||||
|         quantity = form.Quantity(1.00, '94'), | ||||
|         description = 'producto', | ||||
|         item = form.StandardItem(111), | ||||
|         price = form.Price(form.Amount(1_500_000), '01', ''), | ||||
| @@ -170,6 +177,7 @@ def test_invoice_cufe(simple_invoice_without_lines): | ||||
|     assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_credit_note_cude(simple_credit_note_without_lines): | ||||
|     simple_invoice = simple_credit_note_without_lines | ||||
|     simple_invoice.invoice_ident = '8110007871' | ||||
|   | ||||
| @@ -38,7 +38,10 @@ def test_invoice_legalmonetary(): | ||||
|     assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) | ||||
|     assert inv.invoice_legal_monetary_total.charge_total_amount == form.Amount(0.0) | ||||
|  | ||||
|  | ||||
| def test_allowancecharge_as_discount(): | ||||
|     discount = form.AllowanceChargeAsDiscount(amount=form.Amount(1000.0)) | ||||
|     assert discount.isDiscount() == True | ||||
|      | ||||
| def test_FAU10(): | ||||
|     inv = form.NationalSalesInvoice() | ||||
|     inv.add_invoice_line(form.InvoiceLine( | ||||
| @@ -58,7 +61,7 @@ def test_FAU10(): | ||||
|             ] | ||||
|         ) | ||||
|     )) | ||||
|     inv.add_allownace_charge(form.AllowanceCharge(amount=form.Amount(19.0))) | ||||
|     inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) | ||||
|  | ||||
|     inv.calculate() | ||||
|     assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) | ||||
| @@ -86,7 +89,7 @@ def test_FAU14(): | ||||
|             ] | ||||
|         ) | ||||
|     )) | ||||
|     inv.add_allownace_charge(form.AllowanceCharge(amount=form.Amount(19.0))) | ||||
|     inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) | ||||
|     inv.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0))) | ||||
|     inv.calculate() | ||||
|  | ||||
|   | ||||
| @@ -6,9 +6,14 @@ | ||||
| """Tests for `facho` package.""" | ||||
|  | ||||
| import pytest | ||||
| from datetime import datetime | ||||
| import copy | ||||
|  | ||||
| from facho.fe import form | ||||
| from facho.fe import form_xml | ||||
|  | ||||
| from fixtures import * | ||||
|  | ||||
| def test_import_DIANInvoiceXML(): | ||||
|     try: | ||||
|         form_xml.DIANInvoiceXML | ||||
| @@ -27,3 +32,65 @@ def test_import_DIANCreditNoteXML(): | ||||
|         form_xml.DIANCreditNoteXML | ||||
|     except AttributeError: | ||||
|         pytest.fail("unexpected not found") | ||||
|  | ||||
| def test_allowance_charge_in_invoice(simple_invoice_without_lines): | ||||
|     inv = copy.copy(simple_invoice_without_lines) | ||||
|     inv.add_invoice_line(form.InvoiceLine( | ||||
|         quantity = form.Quantity(1, '94'), | ||||
|         description = 'producto facho', | ||||
|         item = form.StandardItem(9999), | ||||
|         price = form.Price( | ||||
|             amount = form.Amount(100.0), | ||||
|             type_code = '01', | ||||
|             type = 'x' | ||||
|         ), | ||||
|         tax = form.TaxTotal( | ||||
|             subtotals = [ | ||||
|                 form.TaxSubTotal( | ||||
|                     percent = 19.0, | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     )) | ||||
|     inv.add_allowance_charge(form.AllowanceCharge(amount=form.Amount(19.0))) | ||||
|     inv.calculate() | ||||
|      | ||||
|     xml = form_xml.DIANInvoiceXML(inv) | ||||
|     assert xml.get_element_text('./cac:AllowanceCharge/cbc:ID') == '1' | ||||
|     assert xml.get_element_text('./cac:AllowanceCharge/cbc:ChargeIndicator') == 'true' | ||||
|     assert xml.get_element_text('./cac:AllowanceCharge/cbc:Amount') == '19.0' | ||||
|     assert xml.get_element_text('./cac:AllowanceCharge/cbc:BaseAmount') == '100.0' | ||||
|  | ||||
| def test_allowance_charge_in_invoice_line(simple_invoice_without_lines): | ||||
|     inv = copy.copy(simple_invoice_without_lines) | ||||
|     inv.add_invoice_line(form.InvoiceLine( | ||||
|         quantity = form.Quantity(1, '94'), | ||||
|         description = 'producto facho', | ||||
|         item = form.StandardItem(9999), | ||||
|         price = form.Price( | ||||
|             amount = form.Amount(100.0), | ||||
|             type_code = '01', | ||||
|             type = 'x' | ||||
|         ), | ||||
|         tax = form.TaxTotal( | ||||
|             subtotals = [ | ||||
|                 form.TaxSubTotal( | ||||
|                     percent = 19.0, | ||||
|                 ) | ||||
|             ] | ||||
|         ), | ||||
|         allowance_charge = [ | ||||
|             form.AllowanceChargeAsDiscount(amount=form.Amount(10.0)) | ||||
|         ] | ||||
|     )) | ||||
|     inv.calculate() | ||||
|  | ||||
|     # se aplico descuento | ||||
|     assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(90.0) | ||||
|      | ||||
|     xml = form_xml.DIANInvoiceXML(inv) | ||||
|  | ||||
|     with pytest.raises(AttributeError): | ||||
|         assert xml.get_element_text('/fe:Invoice/cac:AllowanceCharge/cbc:ID') == '1' | ||||
|     xml.get_element_text('/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:ID') == '1' | ||||
|     xml.get_element_text('/fe:Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:BaseAmount') == '100.0' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user