Compare commits
	
		
			52 Commits
		
	
	
		
			quantity-f
			...
			model
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2e130d39e6 | ||
|  | 6716efd121 | ||
|  | 302812328e | ||
|  | 3a89c6d3e5 | ||
|  | 3a349a746e | ||
|  | efe93ecc3c | ||
|  | 64b312a432 | ||
|  | 088fa9e6e0 | ||
|  | ddee0e45c1 | ||
|  | 69a74c0714 | ||
|  | a1a9746353 | ||
|  | b3e4a088b7 | ||
|  | 507ddbe558 | ||
|  | 2e8aa35b29 | ||
|  | ba908b938c | ||
|  | 4f15926656 | ||
|  | 1d6d1e2601 | ||
|  | 47a0dd33e2 | ||
|  | 53b5207e35 | ||
|  | bd25bef21f | ||
|  | 5f5a6182c9 | ||
|  | ab462a6ca5 | ||
|  | c694603505 | ||
|  | b6219bd171 | ||
|  | a9dde83e81 | ||
|  | 3eacb29afa | ||
|  | f630a544c2 | ||
|  | ba4e3d546f | ||
|  | 92bae58e51 | ||
|  | 58e7387292 | ||
|  | a015a9361b | ||
|  | 0216d0141a | ||
|  | 6cc4610b45 | ||
|  | 49feee8809 | ||
|  | d78a429711 | ||
|  | 84996066fa | ||
|  | 7d060e1786 | ||
|  | 4e68025e48 | ||
|  | ead21bd4f2 | ||
|  | 5e79850686 | ||
|  | 988d01daf7 | ||
|  | 28a963b76a | ||
|  | af99a7593a | ||
|  | d02c0b13b8 | ||
|  | 79209964e0 | ||
|  | 38f4c5ae45 | ||
|  | 1143b26988 | ||
|  | e571009945 | ||
|  | 48619106c5 | ||
|  | bcf5120d82 | ||
|  | f648188834 | ||
|  | 67156ec9a6 | 
| @@ -3,4 +3,4 @@ History | |||||||
| ======= | ======= | ||||||
|  |  | ||||||
|  |  | ||||||
| * First release on PyPI. | * 0.2.1 version usada en produccion. | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.rst
									
									
									
									
									
								
							| @@ -2,8 +2,6 @@ | |||||||
| facho | facho | ||||||
| ===== | ===== | ||||||
|  |  | ||||||
| !!INESTABLE NO RECOMENDAMOS USO PARA PRODUCION!! |  | ||||||
|  |  | ||||||
| Libreria para facturacion electronica colombia. | Libreria para facturacion electronica colombia. | ||||||
|  |  | ||||||
| - facho/facho.py: abstracion para manipulacion del XML | - facho/facho.py: abstracion para manipulacion del XML | ||||||
| @@ -24,7 +22,7 @@ usando pip:: | |||||||
| CLI | 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 |   facho --help | ||||||
|  |  | ||||||
| CONTRIBUIR | CONTRIBUIR | ||||||
| @@ -32,17 +30,13 @@ CONTRIBUIR | |||||||
|  |  | ||||||
| ver **CONTRIBUTING.rst** | ver **CONTRIBUTING.rst** | ||||||
|  |  | ||||||
|  | USO | ||||||
|  | === | ||||||
|  |  | ||||||
|  | ver **USAGE.rst** | ||||||
|  |  | ||||||
|  |  | ||||||
| DIAN HABILITACION | 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 | 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.xpath_for = {} | ||||||
|         self.extensions = [] |         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): |     def append_element(self, elem, new_elem): | ||||||
|         #elem = self.find_or_create_element(xpath, append=append) |         #elem = self.find_or_create_element(xpath, append=append) | ||||||
|         #self.builder.append(elem, new_elem) |         #self.builder.append(elem, new_elem) | ||||||
| @@ -252,6 +257,9 @@ class FachoXML: | |||||||
|     def get_element_text(self, xpath, format_=str): |     def get_element_text(self, xpath, format_=str): | ||||||
|         xpath = self.fragment_prefix + self._path_xpath_for(xpath) |         xpath = self.fragment_prefix + self._path_xpath_for(xpath) | ||||||
|         elem = self.builder.xpath(self.root, xpath) |         elem = self.builder.xpath(self.root, xpath) | ||||||
|  |         if elem is None: | ||||||
|  |             raise AttributeError('xpath %s invalid' % (xpath)) | ||||||
|  |  | ||||||
|         text = self.builder.get_text(elem) |         text = self.builder.get_text(elem) | ||||||
|         return format_(text) |         return format_(text) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -138,10 +138,16 @@ class GetStatusResponse: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def fromdict(cls, data): |     def fromdict(cls, data): | ||||||
|  |         if data['ErrorMessage']: | ||||||
|  |             error_message = data['ErrorMessage']['string'] | ||||||
|  |         else: | ||||||
|  |             error_message = None | ||||||
|  |              | ||||||
|         return cls(data['IsValid'], |         return cls(data['IsValid'], | ||||||
|                    data['StatusDescription'], |                    data['StatusDescription'], | ||||||
|                    data['StatusCode'], |                    data['StatusCode'], | ||||||
|                    data['ErrorMessage']['string']) |                    error_message) | ||||||
|  |                     | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class GetStatus(SOAPService): | class GetStatus(SOAPService): | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ from contextlib import contextmanager | |||||||
| from .data.dian import codelist | from .data.dian import codelist | ||||||
| from . import form | from . import form | ||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
| AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] | AMBIENTE_PRUEBAS = codelist.TipoAmbiente.by_name('Pruebas')['code'] | ||||||
| AMBIENTE_PRODUCCION = codelist.TipoAmbiente.by_name('Producción')['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 | # 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.' | 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: | def fe_from_string(document: str) -> FachoXML: | ||||||
|     xml = LXMLBuilder.from_string(document) |     return FeXML.from_string(document) | ||||||
|     return FachoXML(xml, nsmap=NAMESPACES) |  | ||||||
|  |  | ||||||
| from contextlib import contextmanager | from contextlib import contextmanager | ||||||
| @contextmanager | @contextmanager | ||||||
| @@ -69,6 +70,7 @@ def mock_xades_policy(): | |||||||
|         mock.return_value = UrllibPolicyMock() |         mock.return_value = UrllibPolicyMock() | ||||||
|         yield |         yield | ||||||
|  |  | ||||||
|  |          | ||||||
| class FeXML(FachoXML): | class FeXML(FachoXML): | ||||||
|  |  | ||||||
|     def __init__(self, root, namespace): |     def __init__(self, root, namespace): | ||||||
| @@ -79,6 +81,10 @@ class FeXML(FachoXML): | |||||||
|         self._cn = root.rstrip('/') |         self._cn = root.rstrip('/') | ||||||
|         #self.find_or_create_element(self._cn) |         #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): |     def tostring(self, **kw): | ||||||
|         return super().tostring(**kw)\ |         return super().tostring(**kw)\ | ||||||
|             .replace("fe:", "")\ |             .replace("fe:", "")\ | ||||||
| @@ -185,14 +191,14 @@ class DianXMLExtensionCUFE(DianXMLExtensionCUDFE): | |||||||
|             '%s' % build_vars['NumFac'], |             '%s' % build_vars['NumFac'], | ||||||
|             '%s' % build_vars['FecFac'], |             '%s' % build_vars['FecFac'], | ||||||
|             '%s' % build_vars['HoraFac'], |             '%s' % build_vars['HoraFac'], | ||||||
|             form.Amount(build_vars['ValorBruto']).format('%.02f'), |             form.Amount(build_vars['ValorBruto']).truncate_as_string(2), | ||||||
|             CodImpuesto1, |             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, |             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, |             CodImpuesto3, | ||||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'), |             build_vars['ValorImpuestoPara'].get(CodImpuesto3, form.Amount(0.0)).truncate_as_string(2), | ||||||
|             form.Amount(build_vars['ValorTotalPagar']).format('%.02f'), |             build_vars['ValorTotalPagar'].truncate_as_string(2), | ||||||
|             '%s' % build_vars['NitOFE'], |             '%s' % build_vars['NitOFE'], | ||||||
|             '%s' % build_vars['NumAdq'], |             '%s' % build_vars['NumAdq'], | ||||||
|             '%s' % build_vars['ClTec'], |             '%s' % build_vars['ClTec'], | ||||||
| @@ -222,14 +228,14 @@ class DianXMLExtensionCUDE(DianXMLExtensionCUDFE): | |||||||
|             '%s' % build_vars['NumFac'], |             '%s' % build_vars['NumFac'], | ||||||
|             '%s' % build_vars['FecFac'], |             '%s' % build_vars['FecFac'], | ||||||
|             '%s' % build_vars['HoraFac'], |             '%s' % build_vars['HoraFac'], | ||||||
|             form.Amount(build_vars['ValorBruto']).format('%.02f'), |             form.Amount(build_vars['ValorBruto']).truncate_as_string(2), | ||||||
|             CodImpuesto1, |             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, |             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, |             CodImpuesto3, | ||||||
|             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).format('%.02f'), |             form.Amount(build_vars['ValorImpuestoPara'].get(CodImpuesto3, 0.0)).truncate_as_string(2), | ||||||
|             form.Amount(build_vars['ValorTotalPagar']).format('%.02f'), |             form.Amount(build_vars['ValorTotalPagar']).truncate_as_string(2), | ||||||
|             '%s' % build_vars['NitOFE'], |             '%s' % build_vars['NitOFE'], | ||||||
|             '%s' % build_vars['NumAdq'], |             '%s' % build_vars['NumAdq'], | ||||||
|             '%s' % build_vars['Software-PIN'], |             '%s' % build_vars['Software-PIN'], | ||||||
| @@ -277,15 +283,23 @@ class DianXMLExtensionSoftwareSecurityCode(FachoXMLExtension): | |||||||
| class DianXMLExtensionSigner: | class DianXMLExtensionSigner: | ||||||
|  |  | ||||||
|     def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False): |     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._passphrase = None | ||||||
|         self._mockpolicy = mockpolicy |         self._mockpolicy = mockpolicy | ||||||
|         if passphrase: |         if passphrase: | ||||||
|             self._passphrase = passphrase.encode('utf-8') |             self._passphrase = passphrase.encode('utf-8') | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def from_pkcs12(self, filepath, password=None): |     def from_bytes(cls, data, passphrase=None, mockpolicy=False): | ||||||
|         p12 = OpenSSL.crypto.load_pkcs12(open(filepath, 'rb').read(), password) |         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): |     def sign_xml_string(self, document): | ||||||
|         xml = LXMLBuilder.from_string(document) |         xml = LXMLBuilder.from_string(document) | ||||||
| @@ -341,7 +355,7 @@ class DianXMLExtensionSigner: | |||||||
|             POLICY_NAME, |             POLICY_NAME, | ||||||
|             xmlsig.constants.TransformSha256) |             xmlsig.constants.TransformSha256) | ||||||
|         ctx = xades.XAdESContext(policy) |         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)) |                                                    self._passphrase)) | ||||||
|  |  | ||||||
|         if self._mockpolicy: |         if self._mockpolicy: | ||||||
| @@ -360,6 +374,7 @@ class DianXMLExtensionSigner: | |||||||
|         extcontent = fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') |         extcontent = fachoxml.builder.xpath(fachoxml.root, './ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent') | ||||||
|         fachoxml.append_element(extcontent, signature) |         fachoxml.append_element(extcontent, signature) | ||||||
|  |  | ||||||
|  |          | ||||||
| class DianXMLExtensionAuthorizationProvider(FachoXMLExtension): | class DianXMLExtensionAuthorizationProvider(FachoXMLExtension): | ||||||
|     # RESOLUCION 0004: pagina 176 |     # RESOLUCION 0004: pagina 176 | ||||||
|  |  | ||||||
| @@ -429,7 +444,7 @@ class DianZIP: | |||||||
|     MAX_FILES = 50 |     MAX_FILES = 50 | ||||||
|  |  | ||||||
|     def __init__(self, file_like): |     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 |         self.num_files = 0 | ||||||
|  |  | ||||||
|     def add_invoice_xml(self, name, xml_data): |     def add_invoice_xml(self, name, xml_data): | ||||||
| @@ -452,8 +467,8 @@ class DianZIP: | |||||||
|  |  | ||||||
| class DianXMLExtensionSignerVerifier: | class DianXMLExtensionSignerVerifier: | ||||||
|  |  | ||||||
|     def __init__(self, pkcs12_path, passphrase=None, mockpolicy=False): |     def __init__(self, pkcs12_path_or_bytes, passphrase=None, mockpolicy=False): | ||||||
|         self._pkcs12_path = pkcs12_path |         self._pkcs12_path_or_bytes = pkcs12_path_or_bytes | ||||||
|         self._passphrase = None |         self._passphrase = None | ||||||
|         self._mockpolicy = mockpolicy |         self._mockpolicy = mockpolicy | ||||||
|         if passphrase: |         if passphrase: | ||||||
| @@ -470,7 +485,12 @@ class DianXMLExtensionSignerVerifier: | |||||||
|         fachoxml.root.append(signature) |         fachoxml.root.append(signature) | ||||||
|  |  | ||||||
|         ctx = xades.XAdESContext() |         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)) |                                                    self._passphrase)) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| import hashlib | import hashlib | ||||||
| from functools import reduce | from functools import reduce | ||||||
| import copy | import copy | ||||||
|  | import dataclasses | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from datetime import datetime, date | from datetime import datetime, date | ||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
| @@ -53,8 +54,8 @@ class AmountCollection(Collection): | |||||||
|         return total |         return total | ||||||
|  |  | ||||||
| class Amount: | 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'), precision = DECIMAL_PRECISION): | ||||||
|  |         self.precision = precision | ||||||
|         #DIAN 1.7.-2020: 1.2.3.1 |         #DIAN 1.7.-2020: 1.2.3.1 | ||||||
|         if isinstance(amount, Amount): |         if isinstance(amount, Amount): | ||||||
|             if amount < Amount(0.0): |             if amount < Amount(0.0): | ||||||
| @@ -63,42 +64,60 @@ class Amount: | |||||||
|             self.amount = amount.amount |             self.amount = amount.amount | ||||||
|             self.currency = amount.currency |             self.currency = amount.currency | ||||||
|         else: |         else: | ||||||
|             if amount < 0: |             if float(amount) < 0: | ||||||
|                 raise ValueError('amount must be positive >= 0') |                 raise ValueError('amount must be positive >= 0') | ||||||
|  |  | ||||||
|             self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, |             self.amount = Decimal(amount, decimal.Context(prec=self.precision, | ||||||
|                                                           #DIAN 1.7.-2020: 1.2.1.1 |                                                           #DIAN 1.7.-2020: 1.2.1.1 | ||||||
|                                                           rounding=decimal.ROUND_HALF_EVEN )) |                                                           rounding=decimal.ROUND_HALF_EVEN )) | ||||||
|             self.currency = currency |             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): |     def __round__(self, prec): | ||||||
|         return round(self.amount, prec) |         return round(self.amount, prec) | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return '%.06f' % self.amount |         return str(self.float()) | ||||||
|  |  | ||||||
|     def __lt__(self, other): |     def __lt__(self, other): | ||||||
|         if not self.is_same_currency(other): |         if not self.is_same_currency(other): | ||||||
|             raise AmountCurrencyError() |             raise AmountCurrencyError() | ||||||
|         return round(self.amount, DECIMAL_PRECISION) < round(other, 2) |         return round(self.amount, self.precision) < round(other, 2) | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|         if not self.is_same_currency(other): |         if not self.is_same_currency(other): | ||||||
|             raise AmountCurrencyError() |             raise AmountCurrencyError() | ||||||
|         return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) |         return round(self.amount, self.precision) == round(other.amount, self.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 | ||||||
|  |         if isinstance(val, Decimal): | ||||||
|  |             return self.fromNumber(float(val)) | ||||||
|  |  | ||||||
|  |         raise TypeError("cant cast %s to amount" % (type(val))) | ||||||
|  |      | ||||||
|  |     def __add__(self, rother): | ||||||
|  |         other = self._cast(rother) | ||||||
|         if not self.is_same_currency(other): |         if not self.is_same_currency(other): | ||||||
|             raise AmountCurrencyError() |             raise AmountCurrencyError() | ||||||
|         return Amount(self.amount + other.amount, self.currency) |         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): |         if not self.is_same_currency(other): | ||||||
|             raise AmountCurrencyError() |             raise AmountCurrencyError() | ||||||
|         return Amount(self.amount - other.amount, self.currency) |         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): |         if not self.is_same_currency(other): | ||||||
|             raise AmountCurrencyError() |             raise AmountCurrencyError() | ||||||
|         return Amount(self.amount * other.amount, self.currency) |         return Amount(self.amount * other.amount, self.currency) | ||||||
| @@ -106,8 +125,9 @@ class Amount: | |||||||
|     def is_same_currency(self, other): |     def is_same_currency(self, other): | ||||||
|         return self.currency == other.currency |         return self.currency == other.currency | ||||||
|  |  | ||||||
|     def format(self, formatter): |     def truncate_as_string(self, prec): | ||||||
|         return formatter % self.float() |         parts = str(self.float()).split('.', 1) | ||||||
|  |         return '%s.%s' % (parts[0], parts[1][0:prec].ljust(prec,'0')) | ||||||
|  |  | ||||||
|     def float(self): |     def float(self): | ||||||
|         return float(round(self.amount, DECIMAL_PRECISION)) |         return float(round(self.amount, DECIMAL_PRECISION)) | ||||||
| @@ -116,27 +136,25 @@ class Amount: | |||||||
| class Quantity: | class Quantity: | ||||||
|      |      | ||||||
|     def __init__(self, val, code): |     def __init__(self, val, code): | ||||||
|         if not isinstance(val, int): |         if type(val) not in [float, int]: | ||||||
|             raise ValueError('val expected int') |             raise ValueError('val expected int or float') | ||||||
|         if code not in codelist.UnidadesMedida: |         if code not in codelist.UnidadesMedida: | ||||||
|             raise ValueError("code [%s] not found" % (code)) |             raise ValueError("code [%s] not found" % (code)) | ||||||
|  |  | ||||||
|         self.value = val |         self.value = Amount(val) | ||||||
|         self.code = code |         self.code = code | ||||||
|  |  | ||||||
|     def __mul__(self, other): |     def __mul__(self, other): | ||||||
|         if isinstance(other, Amount): |  | ||||||
|             return Amount(self.value) * other |  | ||||||
|         return self.value * other |         return self.value * other | ||||||
|  |  | ||||||
|     def __lt__(self, other): |     def __lt__(self, other): | ||||||
|         if isinstance(other, Amount): |  | ||||||
|             return Amount(self.value) < other |  | ||||||
|         return self.value < other |         return self.value < other | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return str(self.value) |         return str(self.value) | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         return str(self) | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| class Item: | class Item: | ||||||
| @@ -323,11 +341,15 @@ class Price: | |||||||
|     amount: Amount |     amount: Amount | ||||||
|     type_code: str |     type_code: str | ||||||
|     type: str |     type: str | ||||||
|  |     quantity: int = 1 | ||||||
|  |  | ||||||
|     def __post_init__(self): |     def __post_init__(self): | ||||||
|         if self.type_code not in codelist.CodigoPrecioReferencia: |         if self.type_code not in codelist.CodigoPrecioReferencia: | ||||||
|             raise ValueError("type_code [%s] not found" % (self.type_code)) |             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 | @dataclass | ||||||
| class PaymentMean: | class PaymentMean: | ||||||
| @@ -378,6 +400,52 @@ class InvoiceDocumentReference(BillingReference): | |||||||
|     date: fecha de emision de la nota credito relacionada |     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 | @dataclass | ||||||
| class InvoiceLine: | class InvoiceLine: | ||||||
|     # RESOLUCION 0004: pagina 155 |     # RESOLUCION 0004: pagina 155 | ||||||
| @@ -391,9 +459,31 @@ class InvoiceLine: | |||||||
|     # de subtotal |     # de subtotal | ||||||
|     tax: typing.Optional[TaxTotal] |     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 |     @property | ||||||
|     def total_amount(self): |     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 |     @property | ||||||
|     def total_tax_inclusive_amount(self): |     def total_tax_inclusive_amount(self): | ||||||
| @@ -441,37 +531,6 @@ class LegalMonetaryTotal: | |||||||
|             - self.prepaid_amount |             - 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): | class NationalSalesInvoiceDocumentType(str): | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
| @@ -570,7 +629,7 @@ class Invoice: | |||||||
|  |  | ||||||
|         self.invoice_operation_type = operation |         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) |         self.invoice_allowance_charge.append(charge) | ||||||
|  |  | ||||||
|     def add_invoice_line(self, line: InvoiceLine): |     def add_invoice_line(self, line: InvoiceLine): | ||||||
| @@ -618,11 +677,22 @@ class Invoice: | |||||||
|         #DIAN 1.7.-2020: FAU14 |         #DIAN 1.7.-2020: FAU14 | ||||||
|         self.invoice_legal_monetary_total.calculate() |         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): |     def calculate(self): | ||||||
|         for invline in self.invoice_lines: |         for invline in self.invoice_lines: | ||||||
|             invline.calculate() |             invline.calculate() | ||||||
|         self._calculate_legal_monetary_total() |         self._calculate_legal_monetary_total() | ||||||
|  |         self._refresh_charges_base_amount() | ||||||
|  |  | ||||||
| class NationalSalesInvoice(Invoice): | class NationalSalesInvoice(Invoice): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|   | |||||||
| @@ -540,9 +540,30 @@ class DIANInvoiceXML(fe.FeXML): | |||||||
|             line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code) |             line.set_element('./cac:Price/cbc:PriceAmount', invoice_line.price.amount, currencyID=invoice_line.price.amount.currency.code) | ||||||
|             #DIAN 1.7.-2020: FBB04 |             #DIAN 1.7.-2020: FBB04 | ||||||
|             line.set_element('./cac:Price/cbc:BaseQuantity', |             line.set_element('./cac:Price/cbc:BaseQuantity', | ||||||
|                              invoice_line.quantity, |                              invoice_line.price.quantity, | ||||||
|                              unitCode=invoice_line.quantity.code) |                              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): |     def attach_invoice(fexml, invoice): | ||||||
|         """adiciona etiquetas a FEXML y retorna FEXML |         """adiciona etiquetas a FEXML y retorna FEXML | ||||||
| @@ -579,6 +600,7 @@ class DIANInvoiceXML(fe.FeXML): | |||||||
|         fexml.set_invoice_totals(invoice) |         fexml.set_invoice_totals(invoice) | ||||||
|         fexml.set_invoice_lines(invoice) |         fexml.set_invoice_lines(invoice) | ||||||
|         fexml.set_payment_mean(invoice) |         fexml.set_payment_mean(invoice) | ||||||
|  |         fexml.set_allowance_charge(invoice) | ||||||
|         fexml.set_billing_reference(invoice) |         fexml.set_billing_reference(invoice) | ||||||
|  |  | ||||||
|         return fexml |         return fexml | ||||||
|   | |||||||
							
								
								
									
										367
									
								
								facho/fe/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								facho/fe/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,367 @@ | |||||||
|  | import facho.model as model | ||||||
|  | import facho.model.fields as fields | ||||||
|  | import facho.fe.form as form | ||||||
|  | from facho import fe | ||||||
|  | from .common import * | ||||||
|  | from . import dian | ||||||
|  |  | ||||||
|  | from datetime import date, datetime | ||||||
|  | from collections import defaultdict | ||||||
|  | from copy import copy | ||||||
|  | import hashlib | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PhysicalLocation(model.Model): | ||||||
|  |     __name__ = 'PhysicalLocation' | ||||||
|  |  | ||||||
|  |     address = fields.Many2One(Address, namespace='cac') | ||||||
|  |      | ||||||
|  | class PartyTaxScheme(model.Model): | ||||||
|  |     __name__ = 'PartyTaxScheme' | ||||||
|  |  | ||||||
|  |     registration_name = fields.Many2One(Name, name='RegistrationName', namespace='cbc') | ||||||
|  |     company_id = fields.Many2One(ID, name='CompanyID', namespace='cbc') | ||||||
|  |     tax_level_code = fields.Many2One(ID, name='TaxLevelCode', namespace='cbc', default='ZZ') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Party(model.Model): | ||||||
|  |     __name__ = 'Party' | ||||||
|  |  | ||||||
|  |     id = fields.Virtual(setter='_on_set_id') | ||||||
|  |     name = fields.Many2One(PartyName, namespace='cac') | ||||||
|  |  | ||||||
|  |     tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac') | ||||||
|  |     location = fields.Many2One(PhysicalLocation, namespace='cac') | ||||||
|  |     contact = fields.Many2One(Contact, namespace='cac') | ||||||
|  |      | ||||||
|  |     def _on_set_id(self, name, value): | ||||||
|  |         self.tax_scheme.company_id = value | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  | class AccountingCustomerParty(model.Model): | ||||||
|  |     __name__ = 'AccountingCustomerParty' | ||||||
|  |  | ||||||
|  |     party = fields.Many2One(Party, namespace='cac') | ||||||
|  |  | ||||||
|  | class AccountingSupplierParty(model.Model): | ||||||
|  |     __name__ = 'AccountingSupplierParty' | ||||||
|  |  | ||||||
|  |     party = fields.Many2One(Party, namespace='cac') | ||||||
|  |  | ||||||
|  | class Quantity(model.Model): | ||||||
|  |     __name__  = 'Quantity' | ||||||
|  |  | ||||||
|  |     code = fields.Attribute('unitCode', default='NAR') | ||||||
|  |  | ||||||
|  |     def __setup__(self): | ||||||
|  |         self.value = 0 | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         self.value = value | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self.value | ||||||
|  |  | ||||||
|  | class Amount(model.Model): | ||||||
|  |     __name__ = 'Amount' | ||||||
|  |  | ||||||
|  |     currency = fields.Attribute('currencyID', default='COP') | ||||||
|  |     value = fields.Amount(name='amount', default=0.00, precision=2) | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         self.value = value | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self.value | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self.value) | ||||||
|  |  | ||||||
|  | class Price(model.Model): | ||||||
|  |     __name__ = 'Price' | ||||||
|  |  | ||||||
|  |     amount = fields.Many2One(Amount, name='PriceAmount', namespace='cbc') | ||||||
|  |      | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         self.amount = value | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self.amount | ||||||
|  |  | ||||||
|  | class Percent(model.Model): | ||||||
|  |     __name__ = 'Percent' | ||||||
|  |  | ||||||
|  | class TaxScheme(model.Model): | ||||||
|  |     __name__ = 'TaxScheme' | ||||||
|  |  | ||||||
|  |     id = fields.Many2One(ID, namespace='cbc') | ||||||
|  |     name= fields.Many2One(Name, namespace='cbc') | ||||||
|  |  | ||||||
|  | class TaxCategory(model.Model): | ||||||
|  |     __name__ = 'TaxCategory' | ||||||
|  |  | ||||||
|  |     percent = fields.Many2One(Percent, namespace='cbc') | ||||||
|  |     tax_scheme = fields.Many2One(TaxScheme, namespace='cac') | ||||||
|  |      | ||||||
|  | class TaxSubTotal(model.Model): | ||||||
|  |     __name__ = 'TaxSubTotal' | ||||||
|  |  | ||||||
|  |     taxable_amount = fields.Many2One(Amount, name='TaxableAmount', namespace='cbc', default=0.00) | ||||||
|  |     tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00) | ||||||
|  |     tax_percent = fields.Many2One(Percent, namespace='cbc') | ||||||
|  |     tax_category = fields.Many2One(TaxCategory, namespace='cac') | ||||||
|  |  | ||||||
|  |     percent = fields.Virtual(setter='set_category', getter='get_category') | ||||||
|  |     scheme = fields.Virtual(setter='set_category', getter='get_category') | ||||||
|  |  | ||||||
|  |     def set_category(self, name, value): | ||||||
|  |         if name == 'percent': | ||||||
|  |             self.tax_category.percent = value | ||||||
|  |             # TODO(bit4bit) debe variar en conjunto? | ||||||
|  |             self.tax_percent = value | ||||||
|  |         elif name == 'scheme': | ||||||
|  |             self.tax_category.tax_scheme.id = value | ||||||
|  |  | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def get_category(self, name, value): | ||||||
|  |         if name == 'percent': | ||||||
|  |             return value | ||||||
|  |         elif name == 'scheme': | ||||||
|  |             return self.tax_category.tax_scheme | ||||||
|  |  | ||||||
|  | class TaxTotal(model.Model): | ||||||
|  |     __name__ = 'TaxTotal' | ||||||
|  |  | ||||||
|  |     tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00) | ||||||
|  |     subtotals = fields.One2Many(TaxSubTotal, namespace='cac') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AllowanceCharge(model.Model): | ||||||
|  |     __name__ = 'AllowanceCharge' | ||||||
|  |  | ||||||
|  |     amount = fields.Many2One(Amount, namespace='cbc') | ||||||
|  |     is_discount = fields.Virtual(default=False) | ||||||
|  |      | ||||||
|  |     def isCharge(self): | ||||||
|  |         return self.is_discount == False | ||||||
|  |  | ||||||
|  |     def isDiscount(self): | ||||||
|  |         return self.is_discount == True | ||||||
|  |  | ||||||
|  | class Taxes: | ||||||
|  |     class Scheme: | ||||||
|  |         def __init__(self, scheme): | ||||||
|  |             self.scheme = scheme | ||||||
|  |  | ||||||
|  |     class Iva(Scheme): | ||||||
|  |         def __init__(self, percent): | ||||||
|  |             super().__init__('01') | ||||||
|  |             self.percent = percent | ||||||
|  |  | ||||||
|  |         def calculate(self, amount): | ||||||
|  |             return form.Amount(amount) * form.Amount(self.percent / 100) | ||||||
|  |      | ||||||
|  | class InvoiceLine(model.Model): | ||||||
|  |     __name__ = 'InvoiceLine' | ||||||
|  |  | ||||||
|  |     id = fields.Many2One(ID, namespace='cbc') | ||||||
|  |     quantity = fields.Many2One(Quantity, name='InvoicedQuantity', namespace='cbc') | ||||||
|  |     taxtotal = fields.Many2One(TaxTotal, namespace='cac') | ||||||
|  |     price = fields.Many2One(Price, namespace='cac') | ||||||
|  |     amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc') | ||||||
|  |     allowance_charge = fields.One2Many(AllowanceCharge, 'cac') | ||||||
|  |     tax_amount = fields.Virtual(getter='get_tax_amount') | ||||||
|  |      | ||||||
|  |     def __setup__(self): | ||||||
|  |         self._taxs = defaultdict(list) | ||||||
|  |         self._subtotals = {} | ||||||
|  |  | ||||||
|  |     def add_tax(self, tax): | ||||||
|  |         if not isinstance(tax, Taxes.Scheme): | ||||||
|  |             raise ValueError('tax expected TaxIva') | ||||||
|  |  | ||||||
|  |         # inicialiamos subtotal para impuesto | ||||||
|  |         if not tax.scheme in self._subtotals: | ||||||
|  |             subtotal = self.taxtotal.subtotals.create() | ||||||
|  |             subtotal.scheme = tax.scheme | ||||||
|  |              | ||||||
|  |             self._subtotals[tax.scheme] = subtotal | ||||||
|  |          | ||||||
|  |         self._taxs[tax.scheme].append(tax) | ||||||
|  |  | ||||||
|  |     def get_tax_amount(self, name, value): | ||||||
|  |         total = form.Amount(0) | ||||||
|  |         for (scheme, subtotal) in self._subtotals.items(): | ||||||
|  |             total += subtotal.tax_amount | ||||||
|  |  | ||||||
|  |         return total | ||||||
|  |  | ||||||
|  |     @fields.on_change(['price', 'quantity']) | ||||||
|  |     def update_amount(self, name, value): | ||||||
|  |         charge = form.AmountCollection(self.allowance_charge)\ | ||||||
|  |             .filter(lambda charge: charge.isCharge())\ | ||||||
|  |             .map(lambda charge: charge.amount)\ | ||||||
|  |             .sum() | ||||||
|  |  | ||||||
|  |         discount = form.AmountCollection(self.allowance_charge)\ | ||||||
|  |             .filter(lambda charge: charge.isDiscount())\ | ||||||
|  |             .map(lambda charge: charge.amount)\ | ||||||
|  |             .sum() | ||||||
|  |  | ||||||
|  |         total = form.Amount(self.quantity)  * form.Amount(self.price) | ||||||
|  |         self.amount = total + charge - discount | ||||||
|  |          | ||||||
|  |         for (scheme, subtotal) in self._subtotals.items(): | ||||||
|  |             subtotal.tax_amount = 0 | ||||||
|  |  | ||||||
|  |         for (scheme, taxes) in self._taxs.items(): | ||||||
|  |             for tax in taxes: | ||||||
|  |                 self._subtotals[scheme].tax_amount += tax.calculate(self.amount) | ||||||
|  |  | ||||||
|  | class LegalMonetaryTotal(model.Model): | ||||||
|  |     __name__ = 'LegalMonetaryTotal' | ||||||
|  |  | ||||||
|  |     line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc', default=0) | ||||||
|  |  | ||||||
|  |     tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', namespace='cbc', default=form.Amount(0)) | ||||||
|  |     tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', namespace='cbc', default=form.Amount(0)) | ||||||
|  |     charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', namespace='cbc', default=form.Amount(0)) | ||||||
|  |     payable_amount = fields.Many2One(Amount, name='PayableAmount', namespace='cbc', default=form.Amount(0)) | ||||||
|  |  | ||||||
|  |     @fields.on_change(['tax_inclusive_amount', 'charge_total']) | ||||||
|  |     def update_payable_amount(self, name, value): | ||||||
|  |         self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DIANExtensionContent(model.Model): | ||||||
|  |     __name__ = 'ExtensionContent' | ||||||
|  |      | ||||||
|  |     dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') | ||||||
|  |  | ||||||
|  | class DIANExtension(model.Model): | ||||||
|  |     __name__ = 'UBLExtension' | ||||||
|  |  | ||||||
|  |     content = fields.Many2One(DIANExtensionContent, namespace='ext') | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self.content.dian | ||||||
|  |  | ||||||
|  | class UBLExtension(model.Model): | ||||||
|  |     __name__ = 'UBLExtension' | ||||||
|  |  | ||||||
|  |     content = fields.Many2One(Element, name='ExtensionContent', namespace='ext', default='') | ||||||
|  |      | ||||||
|  | class UBLExtensions(model.Model): | ||||||
|  |     __name__ = 'UBLExtensions' | ||||||
|  |  | ||||||
|  |     dian = fields.Many2One(DIANExtension, namespace='ext', create=True) | ||||||
|  |     extension = fields.Many2One(UBLExtension, namespace='ext', create=True) | ||||||
|  |  | ||||||
|  | class Invoice(model.Model): | ||||||
|  |     __name__ = 'Invoice' | ||||||
|  |     __namespace__ = fe.NAMESPACES | ||||||
|  |  | ||||||
|  |     _ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext') | ||||||
|  |     # nos interesa el acceso solo los atributos de la DIAN | ||||||
|  |     dian = fields.Virtual(getter='get_dian_extension') | ||||||
|  |      | ||||||
|  |     profile_id = fields.Many2One(Element, name='ProfileID', namespace='cbc', default='DIAN 2.1') | ||||||
|  |     profile_execute_id = fields.Many2One(Element, name='ProfileExecuteID', namespace='cbc', default='2') | ||||||
|  |      | ||||||
|  |     id = fields.Many2One(ID, namespace='cbc') | ||||||
|  |     issue = fields.Virtual(setter='set_issue') | ||||||
|  |     issue_date = fields.Many2One(Date, name='IssueDate', namespace='cbc') | ||||||
|  |     issue_time = fields.Many2One(Time, name='IssueTime', namespace='cbc') | ||||||
|  |      | ||||||
|  |     period = fields.Many2One(Period, name='InvoicePeriod', namespace='cac') | ||||||
|  |  | ||||||
|  |     supplier = fields.Many2One(AccountingSupplierParty, namespace='cac') | ||||||
|  |     customer = fields.Many2One(AccountingCustomerParty, namespace='cac') | ||||||
|  |     legal_monetary_total = fields.Many2One(LegalMonetaryTotal, namespace='cac') | ||||||
|  |     lines = fields.One2Many(InvoiceLine, namespace='cac') | ||||||
|  |      | ||||||
|  |     taxtotal_01 = fields.Many2One(TaxTotal) | ||||||
|  |     taxtotal_04 = fields.Many2One(TaxTotal) | ||||||
|  |     taxtotal_03 = fields.Many2One(TaxTotal) | ||||||
|  |  | ||||||
|  |     def __setup__(self): | ||||||
|  |         self._namespace_prefix = 'fe' | ||||||
|  |         # Se requieren minimo estos impuestos para | ||||||
|  |         # validar el cufe | ||||||
|  |         self._subtotal_01 = self.taxtotal_01.subtotals.create() | ||||||
|  |         self._subtotal_01.scheme = '01' | ||||||
|  |         self._subtotal_01.percent = 19.0 | ||||||
|  |  | ||||||
|  |         self._subtotal_04 = self.taxtotal_04.subtotals.create() | ||||||
|  |         self._subtotal_04.scheme = '04' | ||||||
|  |          | ||||||
|  |         self._subtotal_03 = self.taxtotal_03.subtotals.create() | ||||||
|  |         self._subtotal_03.scheme = '03' | ||||||
|  |  | ||||||
|  |     def cufe(self, token, environment): | ||||||
|  |  | ||||||
|  |         valor_bruto = self.legal_monetary_total.line_extension_amount | ||||||
|  |         valor_total_pagar = self.legal_monetary_total.payable_amount | ||||||
|  |  | ||||||
|  |         valor_impuesto_01 = form.Amount(0.0) | ||||||
|  |         valor_impuesto_04 = form.Amount(0.0) | ||||||
|  |         valor_impuesto_03 = form.Amount(0.0) | ||||||
|  |  | ||||||
|  |         for line in self.lines: | ||||||
|  |             for subtotal in line.taxtotal.subtotals: | ||||||
|  |                 if subtotal.scheme.id == '01': | ||||||
|  |                     valor_impuesto_01 += subtotal.tax_amount | ||||||
|  |                 elif subtotal.scheme.id == '04': | ||||||
|  |                     valor_impuesto_04 += subtotal.tax_amount | ||||||
|  |                 elif subtotal.scheme.id == '03': | ||||||
|  |                     valor_impuesto_03 += subtotal.tax_amount | ||||||
|  |  | ||||||
|  |         pattern = [ | ||||||
|  |             '%s' % str(self.id), | ||||||
|  |             '%s' % str(self.issue_date), | ||||||
|  |             '%s' % str(self.issue_time), | ||||||
|  |             valor_bruto.truncate_as_string(2), | ||||||
|  |             '01', valor_impuesto_01.truncate_as_string(2), | ||||||
|  |             '04', valor_impuesto_04.truncate_as_string(2), | ||||||
|  |             '03', valor_impuesto_03.truncate_as_string(2), | ||||||
|  |             valor_total_pagar.truncate_as_string(2), | ||||||
|  |             str(self.supplier.party.id), | ||||||
|  |             str(self.customer.party.id), | ||||||
|  |             str(token), | ||||||
|  |             str(environment) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         cufe = "".join(pattern) | ||||||
|  |         h = hashlib.sha384() | ||||||
|  |         h.update(cufe.encode('utf-8')) | ||||||
|  |         return h.hexdigest() | ||||||
|  |  | ||||||
|  |     @fields.on_change(['lines']) | ||||||
|  |     def update_legal_monetary_total(self, name, value): | ||||||
|  |         self.legal_monetary_total.line_extension_amount = 0 | ||||||
|  |         self.legal_monetary_total.tax_inclusive_amount = 0 | ||||||
|  |  | ||||||
|  |         for line in self.lines: | ||||||
|  |             self.legal_monetary_total.line_extension_amount += line.amount | ||||||
|  |             self.legal_monetary_total.tax_inclusive_amount += line.amount + line.tax_amount | ||||||
|  |  | ||||||
|  |     def set_issue(self, name, value): | ||||||
|  |         if not isinstance(value, datetime): | ||||||
|  |             raise ValueError('expected type datetime') | ||||||
|  |         self.issue_date = value.date() | ||||||
|  |         self.issue_time = value | ||||||
|  |  | ||||||
|  |     def get_dian_extension(self, name, _value): | ||||||
|  |         return self._ubl_extensions.dian | ||||||
|  |  | ||||||
|  |     def to_xml(self, **kw): | ||||||
|  |         # al generar documento el namespace | ||||||
|  |         # se hace respecto a la raiz | ||||||
|  |         return super().to_xml(**kw)\ | ||||||
|  |             .replace("fe:", "")\ | ||||||
|  |             .replace("xmlns:fe", "xmlns") | ||||||
							
								
								
									
										90
									
								
								facho/fe/model/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								facho/fe/model/common.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import facho.model as model | ||||||
|  | import facho.model.fields as fields | ||||||
|  |  | ||||||
|  | from datetime import date, datetime | ||||||
|  |  | ||||||
|  | __all__ = ['Element', 'PartyName', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country', 'Contact'] | ||||||
|  |  | ||||||
|  | class Element(model.Model): | ||||||
|  |     """ | ||||||
|  |     Lo usuamos para elementos que solo manejan contenido | ||||||
|  |     """ | ||||||
|  |     __name__ = 'Element' | ||||||
|  |  | ||||||
|  | class Name(model.Model): | ||||||
|  |     __name__ = 'Name' | ||||||
|  |  | ||||||
|  | class Date(model.Model): | ||||||
|  |     __name__ = 'Date' | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         if isinstance(value, str): | ||||||
|  |             return value | ||||||
|  |         if isinstance(value, date): | ||||||
|  |             return value.isoformat() | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self._value) | ||||||
|  |  | ||||||
|  | class Time(model.Model): | ||||||
|  |     __name__ = 'Time' | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         if isinstance(value, str): | ||||||
|  |             return value | ||||||
|  |         if isinstance(value, date): | ||||||
|  |             return value.strftime('%H:%M:%S-05:00') | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self._value) | ||||||
|  |  | ||||||
|  | class Period(model.Model): | ||||||
|  |     __name__ = 'Period' | ||||||
|  |  | ||||||
|  |     start_date = fields.Many2One(Date, name='StartDate', namespace='cbc') | ||||||
|  |  | ||||||
|  |     end_date = fields.Many2One(Date, name='EndDate', namespace='cbc') | ||||||
|  |  | ||||||
|  | class ID(model.Model): | ||||||
|  |     __name__ = 'ID' | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self._value | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self._value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Country(model.Model): | ||||||
|  |     __name__ = 'Country' | ||||||
|  |  | ||||||
|  |     name = fields.Many2One(Element, name='Name', namespace='cbc') | ||||||
|  |  | ||||||
|  | class Address(model.Model): | ||||||
|  |     __name__ = 'Address' | ||||||
|  |  | ||||||
|  |     #DIAN 1.7.-2020: FAJ08 | ||||||
|  |     #DIAN 1.7.-2020: CAJ09 | ||||||
|  |     id = fields.Many2One(Element, name='ID', namespace='cbc') | ||||||
|  |  | ||||||
|  |     #DIAN 1.7.-2020: FAJ09 | ||||||
|  |     #DIAN 1.7.-2020: CAJ10 | ||||||
|  |     city = fields.Many2One(Element, name='CityName', namespace='cbc') | ||||||
|  |      | ||||||
|  |  | ||||||
|  | class PartyName(model.Model): | ||||||
|  |     __name__ = 'PartyName' | ||||||
|  |      | ||||||
|  |     name = fields.Many2One(Name, namespace='cbc') | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         self.name = value | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         return self.name | ||||||
|  |  | ||||||
|  | class Contact(model.Model): | ||||||
|  |     __name__ = 'Contact' | ||||||
|  |  | ||||||
|  |     email = fields.Many2One(Name, name='ElectronicEmail', namespace='cbc') | ||||||
							
								
								
									
										58
									
								
								facho/fe/model/dian.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								facho/fe/model/dian.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | import facho.model as model | ||||||
|  | import facho.model.fields as fields | ||||||
|  | from .common import * | ||||||
|  |  | ||||||
|  | class DIANElement(Element): | ||||||
|  |     """ | ||||||
|  |     Elemento que contiene atributos por defecto. | ||||||
|  |      | ||||||
|  |     Puede extender esta clase y modificar los atributos nuevamente | ||||||
|  |     """ | ||||||
|  |     __name__ = 'DIANElement' | ||||||
|  |      | ||||||
|  |     scheme_id = fields.Attribute('schemeID', default='4') | ||||||
|  |     scheme_name = fields.Attribute('schemeName', default='31') | ||||||
|  |     scheme_agency_name = fields.Attribute('schemeAgencyName', default='CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)') | ||||||
|  |     scheme_agency_id = fields.Attribute('schemeAgencyID', default='195') | ||||||
|  |      | ||||||
|  | class SoftwareProvider(model.Model): | ||||||
|  |     __name__ = 'SoftwareProvider' | ||||||
|  |  | ||||||
|  |     provider_id = fields.Many2One(Element, name='ProviderID', namespace='sts') | ||||||
|  |     software_id = fields.Many2One(Element, name='SoftwareID', namespace='sts') | ||||||
|  |  | ||||||
|  | class InvoiceSource(model.Model): | ||||||
|  |     __name__ = 'InvoiceSource' | ||||||
|  |  | ||||||
|  |     identification_code = fields.Many2One(Element, name='IdentificationCode', namespace='sts', default='CO') | ||||||
|  |      | ||||||
|  | class AuthorizedInvoices(model.Model): | ||||||
|  |     __name__ = 'AuthorizedInvoices' | ||||||
|  |  | ||||||
|  |     prefix = fields.Many2One(Element, name='Prefix', namespace='sts') | ||||||
|  |     from_range = fields.Many2One(Element, name='From', namespace='sts') | ||||||
|  |     to_range = fields.Many2One(Element, name='To', namespace='sts') | ||||||
|  |      | ||||||
|  | class InvoiceControl(model.Model): | ||||||
|  |     __name__ = 'InvoiceControl' | ||||||
|  |  | ||||||
|  |     authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts') | ||||||
|  |     period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts') | ||||||
|  |     invoices = fields.Many2One(AuthorizedInvoices, namespace='sts') | ||||||
|  |  | ||||||
|  | class AuthorizationProvider(model.Model): | ||||||
|  |     __name__ = 'AuthorizationProvider' | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     id = fields.Many2One(DIANElement, name='AuthorizationProviderID', namespace='sts', default='800197268') | ||||||
|  |      | ||||||
|  | class DianExtensions(model.Model): | ||||||
|  |     __name__ = 'DianExtensions' | ||||||
|  |  | ||||||
|  |     authorization_provider = fields.Many2One(AuthorizationProvider, namespace='sts', create=True) | ||||||
|  |  | ||||||
|  |     software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts') | ||||||
|  |     software_provider = fields.Many2One(SoftwareProvider, namespace='sts') | ||||||
|  |     source = fields.Many2One(InvoiceSource, namespace='sts') | ||||||
|  |     control = fields.Many2One(InvoiceControl, namespace='sts') | ||||||
|  |  | ||||||
							
								
								
									
										175
									
								
								facho/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								facho/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | |||||||
|  | from .fields import Field | ||||||
|  | from collections import defaultdict | ||||||
|  |  | ||||||
|  | class ModelMeta(type): | ||||||
|  |     def __new__(cls, name, bases, ns): | ||||||
|  |         new = type.__new__(cls, name, bases, ns) | ||||||
|  |  | ||||||
|  |         # mapeamos asignacion en declaracion de clase | ||||||
|  |         # a attributo de objeto | ||||||
|  |         if '__name__' in ns: | ||||||
|  |             new.__name__ = ns['__name__'] | ||||||
|  |         if '__namespace__' in ns: | ||||||
|  |             new.__namespace__ = ns['__namespace__'] | ||||||
|  |         else: | ||||||
|  |             new.__namespace__ = {} | ||||||
|  |              | ||||||
|  |         return new | ||||||
|  |  | ||||||
|  | class ModelBase(object, metaclass=ModelMeta): | ||||||
|  |  | ||||||
|  |     def __new__(cls, *args, **kwargs): | ||||||
|  |         obj = super().__new__(cls, *args, **kwargs) | ||||||
|  |         obj._xml_attributes = {} | ||||||
|  |         obj._fields = {} | ||||||
|  |         obj._value = None | ||||||
|  |         obj._namespace_prefix = None | ||||||
|  |         obj._on_change_fields = defaultdict(list) | ||||||
|  |         obj._order_fields = [] | ||||||
|  |          | ||||||
|  |         def on_change_fields_for_function(): | ||||||
|  |             # se recorre arbol de herencia buscando attributo on_changes | ||||||
|  |             for parent_cls in type(obj).__mro__: | ||||||
|  |                 for parent_attr in dir(parent_cls): | ||||||
|  |                     parent_meth = getattr(parent_cls, parent_attr, None) | ||||||
|  |                     if not callable(parent_meth): | ||||||
|  |                         continue | ||||||
|  |                     on_changes = getattr(parent_meth, 'on_changes', None) | ||||||
|  |                     if on_changes: | ||||||
|  |                         return (parent_meth, on_changes) | ||||||
|  |             return (None, []) | ||||||
|  |  | ||||||
|  |         # forzamos registros de campos al modelo | ||||||
|  |         # al instanciar | ||||||
|  |         for (key, v) in type(obj).__dict__.items(): | ||||||
|  |             if isinstance(v, fields.Field): | ||||||
|  |                 obj._order_fields.append(key) | ||||||
|  |  | ||||||
|  |             if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function) or isinstance(v, fields.Amount): | ||||||
|  |                 if hasattr(v, 'default') and v.default is not None: | ||||||
|  |                     setattr(obj, key, v.default) | ||||||
|  |                 if hasattr(v, 'create') and v.create == True: | ||||||
|  |                     setattr(obj, key, '') | ||||||
|  |  | ||||||
|  |                 # register callbacks for changes | ||||||
|  |                 (fun, on_change_fields) = on_change_fields_for_function() | ||||||
|  |                 for field in on_change_fields: | ||||||
|  |                     obj._on_change_fields[field].append(fun) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         # post inicializacion del objeto | ||||||
|  |         obj.__setup__() | ||||||
|  |         return obj | ||||||
|  |  | ||||||
|  |     def _set_attribute(self, field, name, value): | ||||||
|  |         self._xml_attributes[field] = (name, value) | ||||||
|  |  | ||||||
|  |     def __setitem__(self, key, val): | ||||||
|  |         self._xml_attributes[key] = val | ||||||
|  |  | ||||||
|  |     def __getitem__(self, key): | ||||||
|  |         return self._xml_attributes[key] | ||||||
|  |  | ||||||
|  |     def _get_field(self, name): | ||||||
|  |         return self._fields[name] | ||||||
|  |  | ||||||
|  |     def _set_field(self, name, field): | ||||||
|  |         field.name = name | ||||||
|  |         self._fields[name] = field | ||||||
|  |  | ||||||
|  |     def _set_content(self, value): | ||||||
|  |         default = self.__default_set__(value) | ||||||
|  |         if default is not None: | ||||||
|  |             self._value = default | ||||||
|  |  | ||||||
|  |     def to_xml(self): | ||||||
|  |         """ | ||||||
|  |         Genera xml del modelo y sus relaciones | ||||||
|  |         """ | ||||||
|  |         def _hook_before_xml(): | ||||||
|  |             self.__before_xml__() | ||||||
|  |             for field in self._fields.values(): | ||||||
|  |                 if hasattr(field, '__before_xml__'): | ||||||
|  |                     field.__before_xml__() | ||||||
|  |                      | ||||||
|  |         _hook_before_xml() | ||||||
|  |  | ||||||
|  |         tag = self.__name__ | ||||||
|  |         ns = '' | ||||||
|  |         if self._namespace_prefix is not None: | ||||||
|  |             ns = "%s:" % (self._namespace_prefix) | ||||||
|  |  | ||||||
|  |         pair_attributes = ["%s=\"%s\"" % (k, v) for (k, v) in self._xml_attributes.values()] | ||||||
|  |  | ||||||
|  |         for (prefix, url) in self.__namespace__.items(): | ||||||
|  |             pair_attributes.append("xmlns:%s=\"%s\"" % (prefix, url)) | ||||||
|  |         attributes = "" | ||||||
|  |         if pair_attributes: | ||||||
|  |             attributes = " " + " ".join(pair_attributes) | ||||||
|  |  | ||||||
|  |         content = "" | ||||||
|  |  | ||||||
|  |         ordered_fields = {} | ||||||
|  |         for name in self._order_fields: | ||||||
|  |             if name in self._fields: | ||||||
|  |                 ordered_fields[name] = True | ||||||
|  |             else: | ||||||
|  |                 for key in self._fields.keys(): | ||||||
|  |                     if key.startswith(name): | ||||||
|  |                         ordered_fields[key] = True | ||||||
|  |  | ||||||
|  |         for name in ordered_fields.keys(): | ||||||
|  |             value = self._fields[name] | ||||||
|  |             # al ser virtual no adicinamos al arbol xml | ||||||
|  |             if hasattr(value, 'virtual') and value.virtual: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             if hasattr(value, 'to_xml'): | ||||||
|  |                 content += value.to_xml() | ||||||
|  |             elif isinstance(value, str): | ||||||
|  |                 content += value | ||||||
|  |  | ||||||
|  |         if self._value is not None: | ||||||
|  |             content += str(self._value) | ||||||
|  |  | ||||||
|  |         if content == "": | ||||||
|  |             return "<%s%s%s/>" % (ns, tag, attributes) | ||||||
|  |         else: | ||||||
|  |             return "<%s%s%s>%s</%s%s>" % (ns, tag, attributes, content, ns, tag) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Model(ModelBase): | ||||||
|  |     """ | ||||||
|  |     Model clase que representa el modelo | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __before_xml__(self): | ||||||
|  |         """ | ||||||
|  |         Ejecuta antes de generar el xml, este | ||||||
|  |         metodo sirve para realizar actualizaciones | ||||||
|  |         en los campos en el ultimo momento | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def __default_set__(self, value): | ||||||
|  |         """ | ||||||
|  |         Al asignar un valor al modelo atraves de una relacion (person.relation = '33') | ||||||
|  |         se puede personalizar como hacer esta asignacion. | ||||||
|  |         """ | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __default_get__(self, name, value): | ||||||
|  |         """ | ||||||
|  |         Al obtener el valor atraves de una relacion (age = person.age) | ||||||
|  |         Retorno de valor por defecto | ||||||
|  |         """ | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     def __setup__(self): | ||||||
|  |         """ | ||||||
|  |         Inicializar modelo | ||||||
|  |         """ | ||||||
|  |          | ||||||
							
								
								
									
										21
									
								
								facho/model/fields/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								facho/model/fields/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | from .attribute import Attribute | ||||||
|  | from .many2one import Many2One | ||||||
|  | from .one2many import One2Many | ||||||
|  | from .function import Function | ||||||
|  | from .virtual import Virtual | ||||||
|  | from .field import Field | ||||||
|  | from .amount import Amount | ||||||
|  |  | ||||||
|  | __all__ = [Attribute, One2Many, Many2One, Virtual, Field, Amount] | ||||||
|  |  | ||||||
|  | def on_change(fields): | ||||||
|  |     from functools import wraps | ||||||
|  |      | ||||||
|  |     def decorator(func): | ||||||
|  |         setattr(func, 'on_changes', fields) | ||||||
|  |  | ||||||
|  |         @wraps(func) | ||||||
|  |         def wrapper(self, *arg, **kwargs): | ||||||
|  |             return func(self, *arg, **kwargs) | ||||||
|  |         return wrapper | ||||||
|  |     return decorator | ||||||
							
								
								
									
										35
									
								
								facho/model/fields/amount.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								facho/model/fields/amount.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | from .field import Field | ||||||
|  | from collections import defaultdict | ||||||
|  | import facho.fe.form as form | ||||||
|  |  | ||||||
|  | class Amount(Field): | ||||||
|  |     """ | ||||||
|  |     Amount representa un campo moneda usando form.Amount | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, name=None, default=None, precision=6): | ||||||
|  |         self.field_name = name | ||||||
|  |         self.values = {} | ||||||
|  |         self.default = default | ||||||
|  |         self.precision = precision | ||||||
|  |          | ||||||
|  |     def __get__(self, model, cls): | ||||||
|  |         if model is None: | ||||||
|  |             return self | ||||||
|  |         assert self.name is not None | ||||||
|  |   | ||||||
|  |         self.__init_value(model) | ||||||
|  |         model._set_field(self.name, self) | ||||||
|  |         return self.values[model] | ||||||
|  |  | ||||||
|  |     def __set__(self, model, value): | ||||||
|  |         assert self.name is not None | ||||||
|  |         self.__init_value(model) | ||||||
|  |         model._set_field(self.name, self) | ||||||
|  |         self.values[model] = form.Amount(value, precision=self.precision) | ||||||
|  |  | ||||||
|  |         self._changed_field(model, self.name, value) | ||||||
|  |  | ||||||
|  |     def __init_value(self, model): | ||||||
|  |         if model not in self.values: | ||||||
|  |             self.values[model] = form.Amount(self.default or 0) | ||||||
							
								
								
									
										29
									
								
								facho/model/fields/attribute.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								facho/model/fields/attribute.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | from .field import Field | ||||||
|  |  | ||||||
|  | class Attribute(Field): | ||||||
|  |     """ | ||||||
|  |     Attribute es un atributo del elemento actual. | ||||||
|  |     """ | ||||||
|  |      | ||||||
|  |     def __init__(self, name, default=None): | ||||||
|  |         """ | ||||||
|  |         :param name: nombre del atribute | ||||||
|  |         :param default: valor por defecto del attributo | ||||||
|  |         """ | ||||||
|  |         self.attribute = name | ||||||
|  |         self.value = default | ||||||
|  |         self.default = default | ||||||
|  |  | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         if inst is None: | ||||||
|  |             return self | ||||||
|  |  | ||||||
|  |         assert self.name is not None | ||||||
|  |         return self.value | ||||||
|  |  | ||||||
|  |     def __set__(self, inst, value): | ||||||
|  |         assert self.name is not None | ||||||
|  |         self.value = value | ||||||
|  |          | ||||||
|  |         self._changed_field(inst, self.name, value) | ||||||
|  |         inst._set_attribute(self.name, self.attribute, value) | ||||||
							
								
								
									
										60
									
								
								facho/model/fields/field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								facho/model/fields/field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | import warnings | ||||||
|  |  | ||||||
|  | class Field: | ||||||
|  |     def __set_name__(self, owner, name, virtual=False): | ||||||
|  |         self.name = name | ||||||
|  |         self.virtual = virtual | ||||||
|  |  | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         if inst is None: | ||||||
|  |             return self | ||||||
|  |         assert self.name is not None | ||||||
|  |         return inst._fields[self.name] | ||||||
|  |  | ||||||
|  |     def __set__(self, inst, value): | ||||||
|  |         assert self.name is not None | ||||||
|  |         inst._fields[self.name] = value | ||||||
|  |  | ||||||
|  |     def _set_namespace(self, inst, name, namespaces): | ||||||
|  |         if name is None: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         #TODO(bit4bit) aunque las pruebas confirmar | ||||||
|  |         #que si se escribe el namespace que es | ||||||
|  |         #no ahi confirmacion de declaracion previa del namespace | ||||||
|  |  | ||||||
|  |         inst._namespace_prefix = name | ||||||
|  |  | ||||||
|  |     def _call(self, inst, method, *args): | ||||||
|  |         call = getattr(inst, method or '', None) | ||||||
|  |  | ||||||
|  |         if callable(call): | ||||||
|  |             return call(*args) | ||||||
|  |  | ||||||
|  |     def _create_model(self, inst, name=None, model=None, attribute=None, namespace=None): | ||||||
|  |         try: | ||||||
|  |             return inst._fields[self.name] | ||||||
|  |         except KeyError: | ||||||
|  |             if model is not None: | ||||||
|  |                 obj = model() | ||||||
|  |             else: | ||||||
|  |                 obj = self.model() | ||||||
|  |             if name is not None: | ||||||
|  |                 obj.__name__ = name | ||||||
|  |  | ||||||
|  |             if namespace: | ||||||
|  |                 self._set_namespace(obj, namespace, inst.__namespace__) | ||||||
|  |             else: | ||||||
|  |                 self._set_namespace(obj, self.namespace, inst.__namespace__) | ||||||
|  |  | ||||||
|  |             if attribute: | ||||||
|  |                 inst._fields[attribute] = obj | ||||||
|  |             else: | ||||||
|  |                 inst._fields[self.name] = obj | ||||||
|  |  | ||||||
|  |             return obj | ||||||
|  |  | ||||||
|  |     def _changed_field(self, inst, name, value): | ||||||
|  |         for fun in inst._on_change_fields[name]: | ||||||
|  |             fun(inst, name, value) | ||||||
|  |              | ||||||
							
								
								
									
										36
									
								
								facho/model/fields/function.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								facho/model/fields/function.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | from .field import Field | ||||||
|  |  | ||||||
|  | class Function(Field): | ||||||
|  |     """ | ||||||
|  |     Permite modificar el modelo cuando se intenta, | ||||||
|  |     obtener el valor de este campo. | ||||||
|  |  | ||||||
|  |     DEPRECATED usar Virtual | ||||||
|  |     """ | ||||||
|  |     def __init__(self, field, getter=None, default=None): | ||||||
|  |         self.field = field | ||||||
|  |         self.getter = getter | ||||||
|  |         self.default = default | ||||||
|  |  | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         if inst is None: | ||||||
|  |             return self | ||||||
|  |         assert self.name is not None | ||||||
|  |  | ||||||
|  |         # si se indica `field` se adiciona | ||||||
|  |         # como campo del modelo, esto es | ||||||
|  |         # que se serializa a xml | ||||||
|  |         inst._set_field(self.name, self.field) | ||||||
|  |  | ||||||
|  |         if self.getter is not None: | ||||||
|  |             value = self._call(inst, self.getter, self.name, self.field) | ||||||
|  |  | ||||||
|  |             if value is not None: | ||||||
|  |                 self.field.__set__(inst, value) | ||||||
|  |  | ||||||
|  |         return self.field | ||||||
|  |  | ||||||
|  |     def __set__(self, inst, value): | ||||||
|  |         inst._set_field(self.name, self.field) | ||||||
|  |         self._changed_field(inst, self.name, value) | ||||||
|  |         self.field.__set__(inst, value) | ||||||
							
								
								
									
										62
									
								
								facho/model/fields/many2one.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								facho/model/fields/many2one.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | from .field import Field | ||||||
|  | from collections import defaultdict | ||||||
|  |  | ||||||
|  | class Many2One(Field): | ||||||
|  |     """ | ||||||
|  |     Many2One describe una relacion pertenece a. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False, create=False): | ||||||
|  |         """ | ||||||
|  |         :param model: nombre del modelo destino | ||||||
|  |         :param name: nombre del elemento xml | ||||||
|  |         :param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3 | ||||||
|  |         :param namespace: sufijo del namespace al que pertenece el elemento | ||||||
|  |         :param default: el valor o contenido por defecto | ||||||
|  |         :param virtual: se crea la relacion por no se ve reflejada en el xml final | ||||||
|  |         :param create: fuerza la creacion del elemento en el xml, ya que los elementos no son creados sino tienen contenido | ||||||
|  |         """ | ||||||
|  |         self.model = model | ||||||
|  |         self.setter = setter | ||||||
|  |         self.namespace = namespace | ||||||
|  |         self.field_name = name | ||||||
|  |         self.default = default | ||||||
|  |         self.virtual = virtual | ||||||
|  |         self.relations = defaultdict(dict) | ||||||
|  |         self.create = create | ||||||
|  |  | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         if inst is None: | ||||||
|  |             return self | ||||||
|  |         assert self.name is not None | ||||||
|  |  | ||||||
|  |         if self.name in self.relations: | ||||||
|  |             value = self.relations[inst][self.name] | ||||||
|  |         else: | ||||||
|  |             value = self._create_model(inst, name=self.field_name) | ||||||
|  |         self.relations[inst][self.name] = value | ||||||
|  |  | ||||||
|  |         # se puede obtener directamente un valor indicado por el modelo | ||||||
|  |         if hasattr(value, '__default_get__'): | ||||||
|  |             return value.__default_get__(self.name, value) | ||||||
|  |         elif hasattr(inst, '__default_get__'): | ||||||
|  |             return inst.__default_get__(self.name, value) | ||||||
|  |         else: | ||||||
|  |             return value | ||||||
|  |          | ||||||
|  |     def __set__(self, inst, value): | ||||||
|  |         assert self.name is not None | ||||||
|  |         inst_model = self._create_model(inst, name=self.field_name, model=self.model) | ||||||
|  |         self.relations[inst][self.name] = inst_model | ||||||
|  |  | ||||||
|  |         # si hay setter manual se ejecuta | ||||||
|  |         # de lo contrario se asigna como texto del elemento | ||||||
|  |         setter = getattr(inst, self.setter or '', None) | ||||||
|  |         if callable(setter): | ||||||
|  |             setter(inst_model, value) | ||||||
|  |         else: | ||||||
|  |             inst_model._set_content(value) | ||||||
|  |              | ||||||
|  |         self._changed_field(inst, self.name, value) | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										86
									
								
								facho/model/fields/one2many.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								facho/model/fields/one2many.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | from .field import Field | ||||||
|  | from collections import defaultdict | ||||||
|  |  | ||||||
|  | # TODO(bit4bit) lograr que isinstance se aplique | ||||||
|  | # al objeto envuelto | ||||||
|  | class _RelationProxy(): | ||||||
|  |     def __init__(self, obj, inst, attribute): | ||||||
|  |         self.__dict__['_obj'] = obj | ||||||
|  |         self.__dict__['_inst'] = inst | ||||||
|  |         self.__dict__['_attribute'] = attribute | ||||||
|  |  | ||||||
|  |     def __getattr__(self, name): | ||||||
|  |         if (name in self.__dict__): | ||||||
|  |             return self.__dict__[name] | ||||||
|  |  | ||||||
|  |         rel = getattr(self.__dict__['_obj'], name) | ||||||
|  |         if hasattr(rel, '__default_get__'): | ||||||
|  |             return rel.__default_get__(name, rel) | ||||||
|  |  | ||||||
|  |         return rel | ||||||
|  |  | ||||||
|  |     def __setattr__(self, attr, value): | ||||||
|  |         # TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios | ||||||
|  |         # algo burdo, se usa __dict__ para saltarnos el __getattr__ y evitar un fallo por recursion | ||||||
|  |         rel = getattr(self.__dict__['_obj'], attr) | ||||||
|  |         if hasattr(rel, '__default_set__'): | ||||||
|  |             response = setattr(self._obj, attr, rel.__default_set__(value)) | ||||||
|  |         else: | ||||||
|  |             response = setattr(self._obj, attr, value) | ||||||
|  |  | ||||||
|  |         for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]: | ||||||
|  |             fun(self.__dict__['_inst'], self.__dict__['_attribute'], value) | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  | class _Relation(): | ||||||
|  |     def __init__(self, creator, inst, attribute): | ||||||
|  |         self.creator = creator | ||||||
|  |         self.inst = inst | ||||||
|  |         self.attribute = attribute | ||||||
|  |         self.relations = [] | ||||||
|  |  | ||||||
|  |     def create(self): | ||||||
|  |         n_relations = len(self.relations) | ||||||
|  |         attribute = '%s_%d' % (self.attribute, n_relations) | ||||||
|  |         relation = self.creator(attribute) | ||||||
|  |         proxy = _RelationProxy(relation, self.inst, self.attribute) | ||||||
|  |  | ||||||
|  |         self.relations.append(relation) | ||||||
|  |         return proxy | ||||||
|  |  | ||||||
|  |     def __len__(self): | ||||||
|  |         return len(self.relations) | ||||||
|  |  | ||||||
|  |     def __iter__(self): | ||||||
|  |         for relation in self.relations: | ||||||
|  |             yield relation | ||||||
|  |  | ||||||
|  | class One2Many(Field): | ||||||
|  |     """ | ||||||
|  |     One2Many describe una relacion tiene muchos. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, model, name=None, namespace=None, default=None): | ||||||
|  |         """ | ||||||
|  |         :param model: nombre del modelo destino | ||||||
|  |         :param name: nombre del elemento xml cuando se crea hijo | ||||||
|  |         :param namespace: sufijo del namespace al que pertenece el elemento | ||||||
|  |         :param default: el valor o contenido por defecto | ||||||
|  |         """ | ||||||
|  |         self.model = model | ||||||
|  |         self.field_name = name | ||||||
|  |         self.namespace = namespace | ||||||
|  |         self.default = default | ||||||
|  |         self.relation = {} | ||||||
|  |          | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         assert self.name is not None | ||||||
|  |  | ||||||
|  |         def creator(attribute): | ||||||
|  |             return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute, namespace=self.namespace) | ||||||
|  |          | ||||||
|  |         if inst in self.relation: | ||||||
|  |             return self.relation[inst] | ||||||
|  |         else: | ||||||
|  |             self.relation[inst] = _Relation(creator, inst, self.name) | ||||||
|  |             return self.relation[inst] | ||||||
							
								
								
									
										54
									
								
								facho/model/fields/virtual.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								facho/model/fields/virtual.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | from .field import Field | ||||||
|  |  | ||||||
|  | # Un campo virtual | ||||||
|  | # no participa del renderizado | ||||||
|  | # pero puede interactura con este | ||||||
|  | class Virtual(Field): | ||||||
|  |     """ | ||||||
|  |     Virtual es un campo que no es renderizado en el xml final | ||||||
|  |     """ | ||||||
|  |     def __init__(self, | ||||||
|  |                  setter=None, | ||||||
|  |                  getter='', | ||||||
|  |                  default=None, | ||||||
|  |                  update_internal=False): | ||||||
|  |         """ | ||||||
|  |         :param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3 | ||||||
|  |         :param getter: nombre del metodo usando cuando se obtiene, ejemplo: valor = mode.relation | ||||||
|  |         :param default: valor por defecto | ||||||
|  |         :param update_internal: indica que cuando se asigne algun valor este se almacena localmente | ||||||
|  |         """ | ||||||
|  |         self.default = default | ||||||
|  |         self.setter = setter | ||||||
|  |         self.getter = getter | ||||||
|  |         self.values = {} | ||||||
|  |         self.update_internal = update_internal | ||||||
|  |         self.virtual = True | ||||||
|  |  | ||||||
|  |     def __get__(self, inst, cls): | ||||||
|  |         if inst is None: | ||||||
|  |             return self | ||||||
|  |         assert self.name is not None | ||||||
|  |  | ||||||
|  |         value = self.default | ||||||
|  |         try: | ||||||
|  |             value = self.values[inst] | ||||||
|  |         except KeyError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             self.values[inst] = getattr(inst, self.getter)(self.name, value) | ||||||
|  |         except AttributeError: | ||||||
|  |             self.values[inst] = value | ||||||
|  |  | ||||||
|  |         return self.values[inst] | ||||||
|  |      | ||||||
|  |     def __set__(self, inst, value): | ||||||
|  |         if self.update_internal: | ||||||
|  |             inst._value = value | ||||||
|  |  | ||||||
|  |         if self.setter is None: | ||||||
|  |             self.values[inst] = value | ||||||
|  |         else: | ||||||
|  |             self.values[inst] = self._call(inst, self.setter, self.name, value) | ||||||
|  |         self._changed_field(inst, self.name, value)         | ||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -61,6 +61,6 @@ setup( | |||||||
|     test_suite='tests', |     test_suite='tests', | ||||||
|     tests_require=test_requirements, |     tests_require=test_requirements, | ||||||
|     url='https://github.com/bit4bit/facho', |     url='https://github.com/bit4bit/facho', | ||||||
|     version='0.1.2', |     version='0.2.1', | ||||||
|     zip_safe=False, |     zip_safe=False, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -20,3 +20,34 @@ def test_amount_equals(): | |||||||
|     assert price1 == price2 |     assert price1 == price2 | ||||||
|     assert price1 == form.Amount(100) + form.Amount(10) |     assert price1 == form.Amount(100) + form.Amount(10) | ||||||
|     assert price1 == form.Amount(10) * form.Amount(10) + 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) |         helpers.mock_urlopen(m) | ||||||
|         xmlsigned = signer.sign_xml_string(xmlstring) |         xmlsigned = signer.sign_xml_string(xmlstring) | ||||||
|     assert "Signature" in xmlsigned |     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: |     with fe.DianZIP(zipdata) as dianzip: | ||||||
|         name_invoice = dianzip.add_invoice_xml(simple_invoice.invoice_ident, str(xml_invoice)) |         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: |     with zipfile.ZipFile(zipdata) as dianzip: | ||||||
|         xml_data = dianzip.open(name_invoice).read().decode('utf-8') |         xml_data = dianzip.open(name_invoice).read().decode('utf-8') | ||||||
|         assert xml_data == str(xml_invoice) |         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_supplier.ident = form.PartyIdentification('700085371', '5', '31') | ||||||
|     simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31') |     simple_invoice.invoice_customer.ident = form.PartyIdentification('800199436', '5', '31') | ||||||
|     simple_invoice.add_invoice_line(form.InvoiceLine( |     simple_invoice.add_invoice_line(form.InvoiceLine( | ||||||
|         quantity = form.Quantity(1, '94'), |         quantity = form.Quantity(1.00, '94'), | ||||||
|         description = 'producto', |         description = 'producto', | ||||||
|         item = form.StandardItem(111), |         item = form.StandardItem(111), | ||||||
|         price = form.Price(form.Amount(1_500_000), '01', ''), |         price = form.Price(form.Amount(1_500_000), '01', ''), | ||||||
| @@ -170,6 +177,7 @@ def test_invoice_cufe(simple_invoice_without_lines): | |||||||
|     assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' |     assert cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_credit_note_cude(simple_credit_note_without_lines): | def test_credit_note_cude(simple_credit_note_without_lines): | ||||||
|     simple_invoice = simple_credit_note_without_lines |     simple_invoice = simple_credit_note_without_lines | ||||||
|     simple_invoice.invoice_ident = '8110007871' |     simple_invoice.invoice_ident = '8110007871' | ||||||
|   | |||||||
| @@ -38,6 +38,9 @@ def test_invoice_legalmonetary(): | |||||||
|     assert inv.invoice_legal_monetary_total.tax_inclusive_amount == form.Amount(119.0) |     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) |     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(): | def test_FAU10(): | ||||||
|     inv = form.NationalSalesInvoice() |     inv = form.NationalSalesInvoice() | ||||||
| @@ -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() |     inv.calculate() | ||||||
|     assert inv.invoice_legal_monetary_total.line_extension_amount == form.Amount(100.0) |     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.add_prepaid_payment(form.PrePaidPayment(paid_amount = form.Amount(50.0))) | ||||||
|     inv.calculate() |     inv.calculate() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,9 +6,14 @@ | |||||||
| """Tests for `facho` package.""" | """Tests for `facho` package.""" | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  | from datetime import datetime | ||||||
|  | import copy | ||||||
|  |  | ||||||
|  | from facho.fe import form | ||||||
| from facho.fe import form_xml | from facho.fe import form_xml | ||||||
|  |  | ||||||
|  | from fixtures import * | ||||||
|  |  | ||||||
| def test_import_DIANInvoiceXML(): | def test_import_DIANInvoiceXML(): | ||||||
|     try: |     try: | ||||||
|         form_xml.DIANInvoiceXML |         form_xml.DIANInvoiceXML | ||||||
| @@ -27,3 +32,65 @@ def test_import_DIANCreditNoteXML(): | |||||||
|         form_xml.DIANCreditNoteXML |         form_xml.DIANCreditNoteXML | ||||||
|     except AttributeError: |     except AttributeError: | ||||||
|         pytest.fail("unexpected not found") |         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' | ||||||
|   | |||||||
							
								
								
									
										601
									
								
								tests/test_model.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										601
									
								
								tests/test_model.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,601 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # This file is part of facho.  The COPYRIGHT file at the top level of | ||||||
|  | # this repository contains the full copyright notices and license terms. | ||||||
|  |  | ||||||
|  | """Tests for `facho` package.""" | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | import facho.model | ||||||
|  | import facho.model.fields as fields | ||||||
|  |  | ||||||
|  | def test_model_to_element(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |  | ||||||
|  |     assert "<Person/>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_to_element_with_attribute(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         id = fields.Attribute('id') | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     person.id =  33 | ||||||
|  |  | ||||||
|  |     personb = Person() | ||||||
|  |     personb.id = 44 | ||||||
|  |  | ||||||
|  |     assert "<Person id=\"33\"/>" == person.to_xml() | ||||||
|  |     assert "<Person id=\"44\"/>" == personb.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_to_element_with_attribute_as_element(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |          | ||||||
|  |         id = fields.Many2One(ID) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert "<Person><ID>33</ID></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_many2one_with_custom_attributes(): | ||||||
|  |     class TaxAmount(facho.model.Model): | ||||||
|  |         __name__ = 'TaxAmount' | ||||||
|  |  | ||||||
|  |         currencyID = fields.Attribute('currencyID') | ||||||
|  |          | ||||||
|  |     class TaxTotal(facho.model.Model): | ||||||
|  |         __name__ = 'TaxTotal' | ||||||
|  |  | ||||||
|  |         amount = fields.Many2One(TaxAmount) | ||||||
|  |  | ||||||
|  |     tax_total = TaxTotal() | ||||||
|  |     tax_total.amount = 3333 | ||||||
|  |     tax_total.amount.currencyID = 'COP' | ||||||
|  |     assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml() | ||||||
|  |  | ||||||
|  | def test_many2one_with_custom_setter(): | ||||||
|  |  | ||||||
|  |     class PhysicalLocation(facho.model.Model): | ||||||
|  |         __name__ = 'PhysicalLocation' | ||||||
|  |  | ||||||
|  |         id = fields.Attribute('ID') | ||||||
|  |          | ||||||
|  |     class Party(facho.model.Model): | ||||||
|  |         __name__ = 'Party' | ||||||
|  |  | ||||||
|  |         location = fields.Many2One(PhysicalLocation, setter='location_setter') | ||||||
|  |  | ||||||
|  |         def location_setter(self, field, value): | ||||||
|  |             field.id = value | ||||||
|  |              | ||||||
|  |     party = Party() | ||||||
|  |     party.location = 99 | ||||||
|  |     assert '<Party><PhysicalLocation ID="99"/></Party>' == party.to_xml() | ||||||
|  |  | ||||||
|  | def test_many2one_always_create(): | ||||||
|  |     class Name(facho.model.Model): | ||||||
|  |         __name__ = 'Name' | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         name = fields.Many2One(Name, default='facho') | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person><Name>facho</Name></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_many2one_nested_always_create(): | ||||||
|  |     class Name(facho.model.Model): | ||||||
|  |         __name__ = 'Name' | ||||||
|  |  | ||||||
|  |     class Contact(facho.model.Model): | ||||||
|  |         __name__ = 'Contact' | ||||||
|  |  | ||||||
|  |         name = fields.Many2One(Name, default='facho') | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         contact = fields.Many2One(Contact, create=True) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person><Contact><Name>facho</Name></Contact></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_many2one_auto_create(): | ||||||
|  |     class TaxAmount(facho.model.Model): | ||||||
|  |         __name__ = 'TaxAmount' | ||||||
|  |  | ||||||
|  |         currencyID = fields.Attribute('currencyID') | ||||||
|  |          | ||||||
|  |     class TaxTotal(facho.model.Model): | ||||||
|  |         __name__ = 'TaxTotal' | ||||||
|  |  | ||||||
|  |         amount = fields.Many2One(TaxAmount) | ||||||
|  |  | ||||||
|  |     tax_total = TaxTotal() | ||||||
|  |     tax_total.amount.currencyID = 'COP' | ||||||
|  |     tax_total.amount = 3333 | ||||||
|  |     assert '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_model(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = ID() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert "<Person><ID>33</ID></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_multiple_model(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID) | ||||||
|  |         id2 = fields.Many2One(ID) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     person.id2 = 44 | ||||||
|  |     assert "<Person><ID>33</ID><ID>44</ID></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_model_failed_initialization(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert "<Person><ID>33</ID></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_model_with_custom_name(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID, name='DID') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert "<Person><DID>33</DID></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_model_default_initialization_with_attributes(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |  | ||||||
|  |         reference = fields.Attribute('REFERENCE') | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     person.id.reference = 'haber' | ||||||
|  |     assert '<Person><ID REFERENCE="haber">33</ID></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_with_xml_namespace(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         __namespace__ = { | ||||||
|  |             'facho': 'http://lib.facho.cyou' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person xmlns:facho="http://lib.facho.cyou"/>' | ||||||
|  |  | ||||||
|  | def test_model_with_xml_namespace_nested(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         __namespace__ = { | ||||||
|  |             'facho': 'http://lib.facho.cyou' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID, namespace='facho') | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_with_xml_namespace_nested_nested(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Party(facho.model.Model): | ||||||
|  |         __name__ = 'Party' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID, namespace='party') | ||||||
|  |  | ||||||
|  |         def __default_set__(self, value): | ||||||
|  |             self.id = value | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         __namespace__ = { | ||||||
|  |             'person': 'http://lib.facho.cyou', | ||||||
|  |             'party': 'http://lib.facho.cyou' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(Party, namespace='person') | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert '<Person xmlns:person="http://lib.facho.cyou" xmlns:party="http://lib.facho.cyou"><person:Party><party:ID>33</party:ID></person:Party></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_with_xml_namespace_nested_one_many(): | ||||||
|  |     class Name(facho.model.Model): | ||||||
|  |         __name__ = 'Name' | ||||||
|  |  | ||||||
|  |     class Contact(facho.model.Model): | ||||||
|  |         __name__ = 'Contact' | ||||||
|  |  | ||||||
|  |         name = fields.Many2One(Name, namespace='contact') | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         __namespace__ = { | ||||||
|  |             'facho': 'http://lib.facho.cyou', | ||||||
|  |             'contact': 'http://lib.facho.cyou' | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         contacts = fields.One2Many(Contact, namespace='facho') | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     contact = person.contacts.create() | ||||||
|  |     contact.name = 'contact1' | ||||||
|  |  | ||||||
|  |     contact = person.contacts.create() | ||||||
|  |     contact.name = 'contact2' | ||||||
|  |  | ||||||
|  |     assert '<Person xmlns:facho="http://lib.facho.cyou" xmlns:contact="http://lib.facho.cyou"><facho:Contact><contact:Name>contact1</contact:Name></facho:Contact><facho:Contact><contact:Name>contact2</contact:Name></facho:Contact></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_model_with_namespace(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |         __namespace__ = { | ||||||
|  |             "facho": "http://lib.facho.cyou"  | ||||||
|  |         } | ||||||
|  |         id = fields.Many2One(ID, namespace="facho") | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.id = 33 | ||||||
|  |     assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_hook_before_xml(): | ||||||
|  |     class Hash(facho.model.Model): | ||||||
|  |         __name__ = 'Hash' | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Many2One(Hash) | ||||||
|  |  | ||||||
|  |         def __before_xml__(self): | ||||||
|  |             self.hash = "calculate" | ||||||
|  |              | ||||||
|  |     person = Person() | ||||||
|  |     assert "<Person><Hash>calculate</Hash></Person>" == person.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_field_function_with_attribute(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Function(fields.Attribute('hash'), getter='get_hash') | ||||||
|  |  | ||||||
|  |         def get_hash(self, name, field): | ||||||
|  |             return 'calculate' | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person hash="calculate"/>' | ||||||
|  |  | ||||||
|  | def test_field_function_with_model(): | ||||||
|  |     class Hash(facho.model.Model): | ||||||
|  |         __name__ = 'Hash' | ||||||
|  |  | ||||||
|  |         id = fields.Attribute('id') | ||||||
|  |          | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Function(fields.Many2One(Hash), getter='get_hash') | ||||||
|  |  | ||||||
|  |         def get_hash(self, name, field): | ||||||
|  |             field.id = 'calculate' | ||||||
|  |  | ||||||
|  |          | ||||||
|  |     person = Person() | ||||||
|  |     assert person.hash.id == 'calculate' | ||||||
|  |     assert '<Person/>' | ||||||
|  |     | ||||||
|  |  | ||||||
|  | def test_field_function_setter(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Attribute('hash') | ||||||
|  |         password = fields.Virtual(setter='set_hash') | ||||||
|  |  | ||||||
|  |         def set_hash(self, name, value): | ||||||
|  |             self.hash = "%s+2" % (value) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.password = 'calculate' | ||||||
|  |     assert '<Person hash="calculate+2"/>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_function_only_setter(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Attribute('hash') | ||||||
|  |         password = fields.Virtual(setter='set_hash') | ||||||
|  |  | ||||||
|  |         def set_hash(self, name, value): | ||||||
|  |             self.hash = "%s+2" % (value) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.password = 'calculate' | ||||||
|  |     assert '<Person hash="calculate+2"/>' == person.to_xml() | ||||||
|  |      | ||||||
|  | def test_model_set_default_setter(): | ||||||
|  |     class Hash(facho.model.Model): | ||||||
|  |         __name__ = 'Hash' | ||||||
|  |  | ||||||
|  |         def __default_set__(self, value): | ||||||
|  |             return "%s+3" % (value) | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Many2One(Hash) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.hash = 'hola' | ||||||
|  |     assert '<Person><Hash>hola+3</Hash></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_field_virtual(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         age = fields.Virtual() | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.age = 55 | ||||||
|  |     assert person.age == 55 | ||||||
|  |     assert "<Person/>" == person.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_field_inserted_default_attribute(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Attribute('hash', default='calculate') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person hash="calculate"/>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_function_inserted_default_attribute(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         hash = fields.Function(fields.Attribute('hash'), default='calculate') | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person hash="calculate"/>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_inserted_default_many2one(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |  | ||||||
|  |         key = fields.Attribute('key') | ||||||
|  |          | ||||||
|  |         def __default_set__(self, value): | ||||||
|  |             self.key = value | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID, default="oe") | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person><ID key="oe"/></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_field_inserted_default_nested_many2one(): | ||||||
|  |     class ID(facho.model.Model): | ||||||
|  |         __name__ = 'ID' | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         id = fields.Many2One(ID, default="ole") | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     assert '<Person><ID>ole</ID></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_on_change_field(): | ||||||
|  |     class Hash(facho.model.Model): | ||||||
|  |         __name__ = 'Hash' | ||||||
|  |  | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         react = fields.Attribute('react') | ||||||
|  |         hash = fields.Many2One(Hash) | ||||||
|  |  | ||||||
|  |         @fields.on_change(['hash']) | ||||||
|  |         def on_change_react(self, name, value): | ||||||
|  |             assert name == 'hash' | ||||||
|  |             self.react = "%s+4" % (value) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.hash = 'hola' | ||||||
|  |     assert '<Person react="hola+4"><Hash>hola</Hash></Person>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_on_change_field_attribute(): | ||||||
|  |     class Person(facho.model.Model): | ||||||
|  |         __name__ = 'Person' | ||||||
|  |  | ||||||
|  |         react = fields.Attribute('react') | ||||||
|  |         hash = fields.Attribute('Hash') | ||||||
|  |  | ||||||
|  |         @fields.on_change(['hash']) | ||||||
|  |         def on_react(self, name, value): | ||||||
|  |             assert name == 'hash' | ||||||
|  |             self.react = "%s+4" % (value) | ||||||
|  |  | ||||||
|  |     person = Person() | ||||||
|  |     person.hash = 'hola' | ||||||
|  |     assert '<Person react="hola+4" Hash="hola"/>' == person.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_one2many(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         quantity = fields.Attribute('quantity') | ||||||
|  |          | ||||||
|  |     class Invoice(facho.model.Model): | ||||||
|  |         __name__ = 'Invoice' | ||||||
|  |  | ||||||
|  |         lines = fields.One2Many(Line) | ||||||
|  |  | ||||||
|  |     invoice = Invoice() | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 3 | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 5 | ||||||
|  |     assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_model_one2many_with_on_changes(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         quantity = fields.Attribute('quantity') | ||||||
|  |          | ||||||
|  |     class Invoice(facho.model.Model): | ||||||
|  |         __name__ = 'Invoice' | ||||||
|  |  | ||||||
|  |         lines = fields.One2Many(Line) | ||||||
|  |         count = fields.Attribute('count', default=0) | ||||||
|  |          | ||||||
|  |         @fields.on_change(['lines']) | ||||||
|  |         def refresh_count(self, name, value): | ||||||
|  |             self.count = len(self.lines) | ||||||
|  |  | ||||||
|  |     invoice = Invoice() | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 3 | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 5 | ||||||
|  |  | ||||||
|  |     assert len(invoice.lines) == 2 | ||||||
|  |     assert '<Invoice count="2"><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml() | ||||||
|  |  | ||||||
|  | def test_model_one2many_as_list(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         quantity = fields.Attribute('quantity') | ||||||
|  |          | ||||||
|  |     class Invoice(facho.model.Model): | ||||||
|  |         __name__ = 'Invoice' | ||||||
|  |  | ||||||
|  |         lines = fields.One2Many(Line) | ||||||
|  |  | ||||||
|  |     invoice = Invoice() | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 3 | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.quantity = 5 | ||||||
|  |  | ||||||
|  |     lines = list(invoice.lines) | ||||||
|  |     assert len(list(invoice.lines)) == 2 | ||||||
|  |  | ||||||
|  |     for line in lines: | ||||||
|  |         assert isinstance(line, Line) | ||||||
|  |     assert '<Invoice><Line quantity="3"/><Line quantity="5"/></Invoice>' == invoice.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_model_attributes_order(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         quantity = fields.Attribute('quantity') | ||||||
|  |          | ||||||
|  |     class Invoice(facho.model.Model): | ||||||
|  |         __name__ = 'Invoice' | ||||||
|  |  | ||||||
|  |         line1 = fields.Many2One(Line, name='Line1') | ||||||
|  |         line2 = fields.Many2One(Line, name='Line2') | ||||||
|  |         line3 = fields.Many2One(Line, name='Line3') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     invoice = Invoice() | ||||||
|  |     invoice.line2.quantity = 2 | ||||||
|  |     invoice.line3.quantity = 3 | ||||||
|  |     invoice.line1.quantity = 1 | ||||||
|  |  | ||||||
|  |     assert '<Invoice><Line1 quantity="1"/><Line2 quantity="2"/><Line3 quantity="3"/></Invoice>' == invoice.to_xml() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_field_amount(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         amount = fields.Amount(name='Amount', precision=1) | ||||||
|  |         amount_as_attribute = fields.Attribute('amount') | ||||||
|  |  | ||||||
|  |         @fields.on_change(['amount']) | ||||||
|  |         def on_amount(self, name, value): | ||||||
|  |             self.amount_as_attribute = self.amount | ||||||
|  |  | ||||||
|  |     line = Line() | ||||||
|  |     line.amount = 33 | ||||||
|  |  | ||||||
|  |     assert '<Line amount="33.0"/>' == line.to_xml() | ||||||
|  |          | ||||||
|  |  | ||||||
|  | def test_model_setup(): | ||||||
|  |     class Line(facho.model.Model): | ||||||
|  |         __name__ = 'Line' | ||||||
|  |  | ||||||
|  |         amount = fields.Attribute(name='amount') | ||||||
|  |  | ||||||
|  |         def __setup__(self): | ||||||
|  |             self.amount = 23 | ||||||
|  |  | ||||||
|  |     line = Line() | ||||||
|  |     assert '<Line amount="23"/>' == line.to_xml() | ||||||
							
								
								
									
										119
									
								
								tests/test_model_invoice.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								tests/test_model_invoice.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # This file is part of facho.  The COPYRIGHT file at the top level of | ||||||
|  | # this repository contains the full copyright notices and license terms. | ||||||
|  |  | ||||||
|  | """Nuevo esquema para modelar segun decreto""" | ||||||
|  |  | ||||||
|  | from datetime import datetime | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  | from lxml import etree | ||||||
|  | import facho.fe.model as model | ||||||
|  | import facho.fe.form as form | ||||||
|  | from facho import fe | ||||||
|  | import helpers | ||||||
|  |  | ||||||
|  | def simple_invoice(): | ||||||
|  |     invoice = model.Invoice() | ||||||
|  |     invoice.dian.software_security_code = '12345' | ||||||
|  |     invoice.dian.software_provider.provider_id = 'provider-id' | ||||||
|  |     invoice.dian.software_provider.software_id = 'facho' | ||||||
|  |     invoice.dian.control.prefix = 'SETP' | ||||||
|  |     invoice.dian.control.from_range =  '1000' | ||||||
|  |     invoice.dian.control.to_range = '1000' | ||||||
|  |     invoice.id = '323200000129' | ||||||
|  |     invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') | ||||||
|  |     invoice.supplier.party.id = '700085371' | ||||||
|  |     invoice.customer.party.id = '800199436' | ||||||
|  |  | ||||||
|  |     line = invoice.lines.create() | ||||||
|  |     line.add_tax(model.Taxes.Iva(19.0)) | ||||||
|  |  | ||||||
|  |     # TODO(bit4bit) acoplamiento temporal | ||||||
|  |     # se debe crear primero el subotatl | ||||||
|  |     # para poder calcularse al cambiar el precio | ||||||
|  |     line.quantity = 1 | ||||||
|  |     line.price = 1_500_000 | ||||||
|  |  | ||||||
|  |     return invoice | ||||||
|  |  | ||||||
|  | def test_simple_invoice_cufe(): | ||||||
|  |     token = '693ff6f2a553c3646a063436fd4dd9ded0311471' | ||||||
|  |     environment = fe.AMBIENTE_PRODUCCION | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |     assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' | ||||||
|  |  | ||||||
|  | def test_simple_invoice_sign_dian(monkeypatch): | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |  | ||||||
|  |     xmlstring = invoice.to_xml() | ||||||
|  |     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 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dian_extension_authorization_provider(): | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |     xml = fe.FeXML.from_string(invoice.to_xml()) | ||||||
|  |     provider_id = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') | ||||||
|  |  | ||||||
|  |     assert provider_id.attrib['schemeID'] == '4' | ||||||
|  |     assert provider_id.attrib['schemeName'] == '31' | ||||||
|  |     assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)' | ||||||
|  |     assert provider_id.attrib['schemeAgencyID'] == '195' | ||||||
|  |     assert provider_id.text == '800197268' | ||||||
|  |  | ||||||
|  | def test_invoicesimple_xml_signed_using_fexml(monkeypatch): | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |  | ||||||
|  |     xml = fe.FeXML.from_string(invoice.to_xml()) | ||||||
|  |  | ||||||
|  |     signer = fe.DianXMLExtensionSigner('./tests/example.p12') | ||||||
|  |  | ||||||
|  |     print(xml.tostring()) | ||||||
|  |     with monkeypatch.context() as m: | ||||||
|  |         import helpers | ||||||
|  |         helpers.mock_urlopen(m) | ||||||
|  |         xml.add_extension(signer) | ||||||
|  |  | ||||||
|  |     elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') | ||||||
|  |     assert elem.text is not None | ||||||
|  |  | ||||||
|  | def test_invoice_supplier_party(): | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |     invoice.supplier.party.name = 'superfacho' | ||||||
|  |     invoice.supplier.party.tax_scheme.registration_name = 'legal-superfacho' | ||||||
|  |     invoice.supplier.party.contact.email = 'superfacho@etrivial.net' | ||||||
|  |      | ||||||
|  |     xml = fe.FeXML.from_string(invoice.to_xml()) | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') | ||||||
|  |     assert name.text == 'superfacho' | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName') | ||||||
|  |     assert name.text == 'legal-superfacho' | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicEmail') | ||||||
|  |     assert name.text == 'superfacho@etrivial.net' | ||||||
|  |  | ||||||
|  | def test_invoice_customer_party(): | ||||||
|  |     invoice = simple_invoice() | ||||||
|  |     invoice.customer.party.name = 'superfacho-customer' | ||||||
|  |     invoice.customer.party.tax_scheme.registration_name = 'legal-superfacho-customer' | ||||||
|  |     invoice.customer.party.contact.email = 'superfacho@etrivial.net' | ||||||
|  |  | ||||||
|  |     xml = fe.FeXML.from_string(invoice.to_xml()) | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') | ||||||
|  |     assert name.text == 'superfacho-customer' | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName') | ||||||
|  |     assert name.text == 'legal-superfacho-customer' | ||||||
|  |  | ||||||
|  |     name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicEmail') | ||||||
|  |     assert name.text == 'superfacho@etrivial.net' | ||||||
		Reference in New Issue
	
	Block a user