diff --git a/facho/cli.py b/facho/cli.py index 9a41b40..6644ef6 100644 --- a/facho/cli.py +++ b/facho/cli.py @@ -44,6 +44,48 @@ def consultaResolucionesFacturacion(nit, nit_proveedor, id_software, username, p print(str(resp)) +@click.command() +@click.option('--private-key', required=True) +@click.option('--public-key', required=True) +@click.option('--habilitacion/--produccion', default=False) +@click.option('--password') +@click.option('--test-setid', required=True) +@click.argument('filename', required=True) +@click.argument('zipfile', type=click.Path(exists=True)) +def soap_send_test_set_async(private_key, public_key, habilitacion, password, test_setid, filename, zipfile): + from facho.fe.client import dian + + client = dian.DianSignatureClient(private_key, public_key, password=password) + req = dian.SendTestSetAsync + if habilitacion: + req = dian.Habilitacion.SendTestSetAsync + resp = client.request(req( + filename, + open(zipfile, 'rb').read(), + test_setid, + )) + print(resp) + +@click.command() +@click.option('--private-key', required=True) +@click.option('--public-key', required=True) +@click.option('--habilitacion/--produccion', default=False) +@click.option('--password') +@click.argument('filename', required=True) +@click.argument('zipfile', type=click.Path(exists=True)) +def soap_send_bill_async(private_key, public_key, habilitacion, password, filename, zipfile): + from facho.fe.client import dian + + client = dian.DianSignatureClient(private_key, public_key, password=password) + req = dian.SendBillAsync + if habilitacion: + req = dian.Habilitacion.SendBillAsync + resp = client.request(req( + filename, + open(zipfile, 'rb').read() + )) + print(resp) + @click.command() @click.option('--private-key', required=True) @click.option('--public-key', required=True) @@ -64,6 +106,24 @@ def soap_send_bill_sync(private_key, public_key, habilitacion, password, filenam )) print(resp) +@click.command() +@click.option('--private-key', required=True) +@click.option('--public-key', required=True) +@click.option('--habilitacion/--produccion', default=False) +@click.option('--password') +@click.option('--track-id', required=True) +def soap_get_status_zip(private_key, public_key, habilitacion, password, track_id): + from facho.fe.client import dian + + client = dian.DianSignatureClient(private_key, public_key, password=password) + req = dian.GetStatusZip + if habilitacion: + req = dian.Habilitacion.GetStatusZip + resp = client.request(req( + trackId = track_id + )) + print(resp) + @click.command() @click.option('--private-key', required=True) @click.option('--public-key', required=True) @@ -113,7 +173,7 @@ def generate_invoice(private_key, passphrase, scriptname): if private_key: signer = fe.DianXMLExtensionSigner(private_key, passphrase=passphrase) xml.add_extension(signer) - print(str(xml)) + print(xml.tostring(xml_declaration=True)) @click.group() @@ -121,6 +181,9 @@ def main(): pass main.add_command(consultaResolucionesFacturacion) +main.add_command(soap_send_test_set_async) +main.add_command(soap_send_bill_async) main.add_command(soap_send_bill_sync) main.add_command(soap_get_status) +main.add_command(soap_get_status_zip) main.add_command(generate_invoice) diff --git a/facho/facho.py b/facho/facho.py index 3300049..750ffc6 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -98,8 +98,10 @@ class LXMLBuilder: def set_attribute(self, elem, key, value): elem.attrib[key] = value - def tostring(self, elem): - return tostring(elem).decode('utf-8') + def tostring(self, elem, **attrs): + attrs['pretty_print'] = attrs.pop('pretty_print', True) + attrs['encoding'] = attrs.pop('encoding', 'UTF-8') + return tostring(elem, **attrs).decode('utf-8') class FachoXML: @@ -207,8 +209,8 @@ class FachoXML: text = self.builder.get_text(elem) return format_(text) - def tostring(self): - return self.builder.tostring(self.root) + def tostring(self, **kw): + return self.builder.tostring(self.root, **kw) def __str__(self): return self.tostring() diff --git a/facho/fe/client/dian.py b/facho/fe/client/dian.py index 448a1d4..04cd6bb 100644 --- a/facho/fe/client/dian.py +++ b/facho/fe/client/dian.py @@ -80,7 +80,7 @@ class ConsultaResolucionesFacturacionPeticion(SOAPService): return ConsultaResolucionesFacturacionRespuesta.fromdict(as_dict) @dataclass -class SendBillAsync: +class SendBillAsync(SOAPService): fileName: str contentFile: str @@ -91,7 +91,7 @@ class SendBillAsync: return 'SendBillAsync' def build_response(self, as_dict): - return {} + return as_dict @@ -108,7 +108,7 @@ class SendTestSetAsync(SOAPService): return 'SendTestSetAsync' def build_response(self, as_dict): - return {} + return as_dict @dataclass class SendBillSync(SOAPService): @@ -122,7 +122,7 @@ class SendBillSync(SOAPService): return 'SendBillSync' def build_response(self, as_dict): - return {} + return as_dict @dataclass @@ -136,12 +136,31 @@ class GetStatus(SOAPService): return 'GetStatus' def build_response(self, as_dict): - return {} + return as_dict + +@dataclass +class GetStatusZip(SOAPService): + trackId: bytes + + def get_wsdl(self): + return 'https://colombia-dian-webservices-input-sbx.azurewebsites.net/WcfDianCustomerServices.svc?wsdl' + + def get_service(self): + return 'GetStatusZip' + + def build_response(self, as_dict): + return as_dict class Habilitacion: WSDL = 'https://vpfe-hab.dian.gov.co/WcfDianCustomerServices.svc?wsdl' + + class SendBillAsync(SendBillAsync): + def get_wsdl(self): + return Habilitacion.WSDL + + class SendBillSync(SendBillSync): def get_wsdl(self): return Habilitacion.WSDL @@ -153,6 +172,10 @@ class Habilitacion: class GetStatus(GetStatus): def get_wsdl(self): return Habilitacion.WSDL + + class GetStatusZip(GetStatusZip): + def get_wsdl(self): + return Habilitacion.WSDL class DianGateway: @@ -200,12 +223,11 @@ class DianSignatureClient(DianGateway): from zeep.wsse import utils client = zeep.Client(service.get_wsdl(), wsse= - [ - BinarySignature( - self.private_key_path, self.public_key_path, self.password, - signature_method=xmlsec.Transform.RSA_SHA256, - digest_method=xmlsec.Transform.SHA256) - ], + BinarySignature( + self.private_key_path, self.public_key_path, self.password, + signature_method=xmlsec.Transform.RSA_SHA256, + digest_method=xmlsec.Transform.SHA256) + , ) return client diff --git a/facho/fe/client/wsse/signature.py b/facho/fe/client/wsse/signature.py index be90c2f..3db013a 100644 --- a/facho/fe/client/wsse/signature.py +++ b/facho/fe/client/wsse/signature.py @@ -346,6 +346,9 @@ def _verify_envelope_with_key(envelope, key): security = header.find(QName(ns.WSSE, "Security")) signature = security.find(QName(ns.DS, "Signature")) + # la DIAN no cumple a cabalidad token-profile 1.0 + if signature is None: + return SignatureVerificationFailed() ctx = xmlsec.SignatureContext() diff --git a/facho/fe/fe.py b/facho/fe/fe.py index 70163e2..28a7ac2 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -55,6 +55,7 @@ class DianXMLExtensionCUFE(FachoXMLExtension): def build(self, fachoxml): cufe = self._generate_cufe(self.invoice, fachoxml) fachoxml.set_element('/fe:Invoice/cbc:UUID[schemaName="CUFE-SHA384"]', cufe) + fachoxml.set_element('/fe:Invoice/cbc:ProfileID', 'DIAN 2.1') fachoxml.set_element('/fe:Invoice/cbc:ProfileExecutionID', self._tipo_ambiente()) return '', [] diff --git a/facho/fe/form.py b/facho/fe/form.py index ef6c67c..7f9f993 100644 --- a/facho/fe/form.py +++ b/facho/fe/form.py @@ -181,6 +181,7 @@ class DIANInvoiceXML(fe.FeXML): invoice.calculate() + fexml.set_element('/fe:Invoice/cbc:UBLVersionID', 'UBL 2.1') fexml.set_element('/fe:Invoice/cbc:ID', invoice.invoice_ident) fexml.set_element('/fe:Invoice/cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) fexml.set_element('/fe:Invoice/cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S%z'))