From 8f327f7abca2055745d3c7e11562a857751256a1 Mon Sep 17 00:00:00 2001 From: sinergia Date: Tue, 12 Mar 2024 17:05:55 -0500 Subject: [PATCH] feat: UPDATE Habilitacion RUSTIK --- facho/facho.py | 88 ++++++++++++++++++++++-------------- facho/fe/fe.py | 52 ++++++++++++++------- facho/fe/form_xml/invoice.py | 13 ++---- 3 files changed, 93 insertions(+), 60 deletions(-) diff --git a/facho/facho.py b/facho/facho.py index c88c83e..ef39bff 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -1,12 +1,11 @@ # This file is part of facho. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. - from lxml import etree -from lxml.etree import Element, SubElement, tostring +from lxml.etree import Element, tostring import re from collections import defaultdict from copy import deepcopy -from pprint import pprint + class FachoValueInvalid(Exception): def __init__(self, xpath): @@ -32,7 +31,10 @@ class LXMLBuilder: def __init__(self, nsmap): self.nsmap = nsmap - self._re_node_expr = re.compile(r'^(?P((?P\w+):)?(?P[a-zA-Z0-9_-]+))(?P\[.+\])?') + self._re_node_expr = \ + re.compile( + r'^(?P((?P\w+):)?(?P[a-zA-Z0-9_-]+))' + r'(?P\[.+\])?') self._re_attrs = re.compile(r'(\w+)\s*=\s*\"?(\w+)\"?') def match_expression(self, node_expr): @@ -121,7 +123,7 @@ class LXMLBuilder: elem.attrib[key] = value @classmethod - def remove_attributes(cls, elem, keys, exclude = []): + def remove_attributes(cls, elem, keys, exclude=[]): for key in keys: if key in exclude: continue @@ -143,7 +145,8 @@ class LXMLBuilder: self.remove_attributes(el, keys, exclude=['facho_optional']) is_optional = el.get('facho_optional', 'False') == 'True' - if is_optional and el.getchildren() == [] and el.keys() == ['facho_optional']: + if is_optional and el.getchildren() == [] and el.keys() == [ + 'facho_optional']: el.getparent().remove(el) return tostring(elem, **attrs).decode('utf-8') @@ -153,14 +156,15 @@ class FachoXML: """ Decora XML con funciones de consulta XPATH de un solo elemento """ - def __init__(self, root, builder=None, nsmap=None, fragment_prefix='',fragment_root_element=None): + def __init__(self, root, builder=None, nsmap=None, fragment_prefix='', + fragment_root_element=None): if builder is None: self.builder = LXMLBuilder(nsmap) else: self.builder = builder self.nsmap = nsmap - + if isinstance(root, str): self.root = self.builder.build_element_from_string(root, nsmap) else: @@ -181,15 +185,15 @@ class FachoXML: return etree.QName(self.root).namespace def append_element(self, elem, new_elem): - #elem = self.find_or_create_element(xpath, append=append) - #self.builder.append(elem, new_elem) + # elem = self.find_or_create_element(xpath, append=append) + # self.builder.append(elem, new_elem) self.builder.append(elem, new_elem) def add_extension(self, extension): extension.build(self) - - def fragment(self, xpath, append=False, append_not_exists=False): + def fragment( + self, xpath, append=False, append_not_exists=False): nodes = xpath.split('/') nodes.pop() root_prefix = '/'.join(nodes) @@ -199,7 +203,9 @@ class FachoXML: if parent is None: parent = self.find_or_create_element(xpath, append=append) - return FachoXML(parent, nsmap=self.nsmap, fragment_prefix=root_prefix, fragment_root_element=self.root) + return FachoXML( + parent, nsmap=self.nsmap, fragment_prefix=root_prefix, + fragment_root_element=self.root) def register_alias_xpath(self, alias, xpath): self.xpath_for[alias] = xpath @@ -235,7 +241,8 @@ class FachoXML: """ xpath = self._path_xpath_for(xpath) node_paths = xpath.split('/') - node_paths.pop(0) #remove empty / + # remove empty / + node_paths.pop(0) root_tag = node_paths.pop(0) root_node = self.builder.build_from_expression(root_tag) @@ -243,10 +250,10 @@ class FachoXML: # restaurar ya que no es la raiz y asignar actual como raiz node_paths.insert(0, root_tag) root_node = self.root - + if not self.builder.same_tag(root_node.tag, self.root.tag): - - raise ValueError('xpath %s must be absolute to /%s' % (xpath, self.root.tag)) + raise ValueError('xpath %s must be absolute to /%s' % ( + xpath, self.root.tag)) # crea jerarquia segun xpath indicado parent = None @@ -256,8 +263,8 @@ class FachoXML: for node_path in node_paths: node_expr = self.builder.match_expression(node_path) node = self.builder.build_from_expression(node_path) - - child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap) + child = self.builder.find_relative( + current_elem, node_expr['path'], self.nsmap) parent = current_elem if child is not None: @@ -268,11 +275,12 @@ class FachoXML: node_expr = self.builder.match_expression(node_tag) node = self.builder.build_from_expression(node_tag) - child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap) + child = self.builder.find_relative( + current_elem, node_expr['path'], self.nsmap) parent = current_elem if child is not None: current_elem = child - + if parent == current_elem: self.builder.append(parent, node) return node @@ -289,9 +297,10 @@ class FachoXML: self.builder.append(parent, node) return node - if self.builder.is_attribute(last_slibing, 'facho_placeholder', 'True'): + if self.builder.is_attribute( + last_slibing, 'facho_placeholder', 'True'): self._remove_facho_attributes(last_slibing) - return last_slibing + return last_slibing self.builder.append_next(last_slibing, node) return node @@ -302,7 +311,8 @@ class FachoXML: self._remove_facho_attributes(current_elem) return current_elem - def set_element_validator(self, xpath, validator = False): + def set_element_validator( + self, xpath, validator=False): """ validador al asignar contenido a xpath indicado @@ -315,8 +325,9 @@ class FachoXML: self._validators[key] = lambda v, attrs: True else: self._validators[key] = validator - - def set_element(self, xpath, content, **attrs): + + def set_element( + self, xpath, content, **attrs): """ asigna contenido ubicado por ruta tipo XPATH. @param xpath ruta tipo XPATH @@ -358,7 +369,8 @@ class FachoXML: self.builder.set_attribute(elem, k, str(v)) return self - def get_element_attribute(self, xpath, attribute, multiple=False): + def get_element_attribute( + self, xpath, attribute, multiple=False): elem = self.get_element(xpath, multiple=multiple) if elem is None: @@ -395,14 +407,16 @@ class FachoXML: return None return format_(text) - def get_element_text_or_attribute(self, xpath, default=None, multiple=False, raise_on_fail=False): + def get_element_text_or_attribute( + self, xpath, default=None, multiple=False, raise_on_fail=False): parts = xpath.split('/') - is_attribute = parts[-1].startswith('@') + is_attribute = parts[-1].startswith('@') if is_attribute: attribute_name = parts.pop(-1).lstrip('@') element_path = "/".join(parts) try: - val = self.get_element_attribute(element_path, attribute_name, multiple=multiple) + val = self.get_element_attribute( + element_path, attribute_name, multiple=multiple) if val is None: return default return val @@ -435,7 +449,8 @@ class FachoXML: if isinstance(xpath, tuple): val = xpath[0] else: - val = self.get_element_text_or_attribute(xpath, raise_on_fail=raise_on_fail) + val = self.get_element_text_or_attribute( + xpath, raise_on_fail=raise_on_fail) vals.append(val) return vals @@ -457,7 +472,8 @@ class FachoXML: return True def _remove_facho_attributes(self, elem): - self.builder.remove_attributes(elem, ['facho_optional', 'facho_placeholder']) + self.builder.remove_attributes( + elem, ['facho_optional', 'facho_placeholder']) def tostring(self, **kw): return self.builder.tostring(self.root, **kw) @@ -469,15 +485,17 @@ class FachoXML: root = self.root if self.fragment_root_element is not None: root = self.fragment_root_element - + if isinstance(self.nsmap, dict): nsmap = dict(map(reversed, self.nsmap.items())) ns = nsmap[etree.QName(root).namespace] + ':' if self.fragment_root_element is not None: - new_xpath = '/' + ns + etree.QName(root).localname + '/' + etree.QName(self.root).localname + '/' + xpath.lstrip('/') + new_xpath = '/' + ns + etree.QName(root).localname + '/' + \ + etree.QName(self.root).localname + '/' + xpath.lstrip('/') else: - new_xpath = '/' + ns + etree.QName(root).localname + '/' + xpath.lstrip('/') + new_xpath = '/' + ns + etree.QName(root).localname + '/' + \ + xpath.lstrip('/') return new_xpath def __str__(self): diff --git a/facho/fe/fe.py b/facho/fe/fe.py index c136fc4..f62437a 100644 --- a/facho/fe/fe.py +++ b/facho/fe/fe.py @@ -30,28 +30,45 @@ SCHEME_AGENCY_ATTRS = { POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' POLICY_NAME = u'Política de firma para facturas electrónicas de la República de Colombia.' + +# NAMESPACES = { +# 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2', +# 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual', +# 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste', +# 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', +# 'xs': 'http://www.w3.org/2001/XMLSchema-instance', +# 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', +# 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', +# 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', +# 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', +# 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003', +# 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', +# 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', +# 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', +# 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', +# 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', +# 'xades': 'http://uri.etsi.org/01903/v1.3.2#', +# 'xades141': 'http://uri.etsi.org/01903/v1.4.1#', +# 'ds': 'http://www.w3.org/2000/09/xmldsig#', +# 'sig': 'http://www.w3.org/2000/09/xmldsig#', +# } + + NAMESPACES = { - 'atd': 'urn:oasis:names:specification:ubl:schema:xsd:AttachedDocument-2', - 'nomina': 'dian:gov:co:facturaelectronica:NominaIndividual', - 'nominaajuste': 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste', 'fe': 'http://www.dian.gov.co/contratos/facturaelectronica/v1', - 'xs': 'http://www.w3.org/2001/XMLSchema-instance', 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', - 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', - 'clm54217': 'urn:un:unece:uncefact:codelist:specification:54217:2001', - 'clmIANAMIMEMediaType': 'urn:un:unece:uncefact:codelist:specification:IANAMIMEMediaType:2003', 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', 'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', - 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', + 'udt': 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2', 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xades': 'http://uri.etsi.org/01903/v1.3.2#', - 'xades141': 'http://uri.etsi.org/01903/v1.4.1#', 'ds': 'http://www.w3.org/2000/09/xmldsig#', - 'sig': 'http://www.w3.org/2000/09/xmldsig#', + 'xades': 'http://uri.etsi.org/01903/v1.3.2#', } + + def fe_from_string(document: str) -> FachoXML: return FeXML.from_string(document) @@ -77,23 +94,24 @@ def mock_xades_policy(): class FeXML(FachoXML): def __init__(self, root, namespace): - + # raise Exception(namespace) super().__init__("{%s}%s" % (namespace, root), nsmap=NAMESPACES) @classmethod def from_string(cls, document: str) -> 'FeXML': return super().from_string(document, namespaces=NAMESPACES) - + def tostring(self, **kw): # MACHETE(bit4bit) la DIAN espera que la etiqueta raiz no este en un namespace root_namespace = self.root_namespace() xmlns_name = {v: k for k, v in NAMESPACES.items()}[root_namespace] return super().tostring(**kw)\ - .replace(xmlns_name + ':', '')\ - .replace('xmlns:'+xmlns_name, 'xmlns')\ - .replace('schemaLocation', 'xsi:schemaLocation') - + .replace(xmlns_name + ':', '')\ + .replace('xmlns:'+xmlns_name, 'xmlns')\ + .replace('http://www.dian.gov.co/contratos/facturaelectronica/v1', 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2') + + class DianXMLExtensionCUDFE(FachoXMLExtension): def __init__(self, invoice, tipo_ambiente = AMBIENTE_PRUEBAS): diff --git a/facho/fe/form_xml/invoice.py b/facho/fe/form_xml/invoice.py index 0482a49..3e6444f 100644 --- a/facho/fe/form_xml/invoice.py +++ b/facho/fe/form_xml/invoice.py @@ -652,7 +652,6 @@ class DIANInvoiceXML(fe.FeXML): fexml.placeholder_for('./cbc:ProfileExecutionID') fexml.set_element('./cbc:ID', invoice.invoice_ident) fexml.placeholder_for('./cbc:UUID') - fexml.set_element('./cbc:DocumentCurrencyCode', 'COP') fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) #DIAN 1.7.-2020: FAD10 fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00')) @@ -661,24 +660,22 @@ class DIANInvoiceXML(fe.FeXML): listAgencyID='195', listAgencyName='No matching global declaration available for the validation root', listURI='http://www.dian.gov.co') + fexml.set_element('./cbc:DocumentCurrencyCode', 'COP') fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines)) fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (fexml.tag_document()), invoice.invoice_period_start.strftime('%Y-%m-%d')) fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (fexml.tag_document()), invoice.invoice_period_end.strftime('%Y-%m-%d')) - + fexml.set_billing_reference(invoice) fexml.customize(invoice) - fexml.set_supplier(invoice) fexml.set_customer(invoice) - fexml.set_legal_monetary(invoice) - fexml.set_invoice_totals(invoice) - fexml.set_invoice_lines(invoice) fexml.set_payment_mean(invoice) + fexml.set_invoice_totals(invoice) + fexml.set_legal_monetary(invoice) + fexml.set_invoice_lines(invoice) fexml.set_allowance_charge(invoice) - fexml.set_billing_reference(invoice) - return fexml def customize(fexml, invoice):