feat: UPDATE Habilitacion RUSTIK

This commit is contained in:
sinergia 2024-03-12 17:05:55 -05:00
parent b7c9f2b201
commit 8f327f7abc
3 changed files with 93 additions and 60 deletions

View File

@ -1,12 +1,11 @@
# This file is part of facho. The COPYRIGHT file at the top level of # This file is part of facho. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from lxml import etree from lxml import etree
from lxml.etree import Element, SubElement, tostring from lxml.etree import Element, tostring
import re import re
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from pprint import pprint
class FachoValueInvalid(Exception): class FachoValueInvalid(Exception):
def __init__(self, xpath): def __init__(self, xpath):
@ -32,7 +31,10 @@ class LXMLBuilder:
def __init__(self, nsmap): def __init__(self, nsmap):
self.nsmap = nsmap self.nsmap = nsmap
self._re_node_expr = re.compile(r'^(?P<path>((?P<ns>\w+):)?(?P<tag>[a-zA-Z0-9_-]+))(?P<attrs>\[.+\])?') self._re_node_expr = \
re.compile(
r'^(?P<path>((?P<ns>\w+):)?(?P<tag>[a-zA-Z0-9_-]+))'
r'(?P<attrs>\[.+\])?')
self._re_attrs = re.compile(r'(\w+)\s*=\s*\"?(\w+)\"?') self._re_attrs = re.compile(r'(\w+)\s*=\s*\"?(\w+)\"?')
def match_expression(self, node_expr): def match_expression(self, node_expr):
@ -143,7 +145,8 @@ class LXMLBuilder:
self.remove_attributes(el, keys, exclude=['facho_optional']) self.remove_attributes(el, keys, exclude=['facho_optional'])
is_optional = el.get('facho_optional', 'False') == 'True' 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) el.getparent().remove(el)
return tostring(elem, **attrs).decode('utf-8') return tostring(elem, **attrs).decode('utf-8')
@ -153,7 +156,8 @@ class FachoXML:
""" """
Decora XML con funciones de consulta XPATH de un solo elemento 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: if builder is None:
self.builder = LXMLBuilder(nsmap) self.builder = LXMLBuilder(nsmap)
else: else:
@ -188,8 +192,8 @@ class FachoXML:
def add_extension(self, extension): def add_extension(self, extension):
extension.build(self) extension.build(self)
def fragment(
def fragment(self, xpath, append=False, append_not_exists=False): self, xpath, append=False, append_not_exists=False):
nodes = xpath.split('/') nodes = xpath.split('/')
nodes.pop() nodes.pop()
root_prefix = '/'.join(nodes) root_prefix = '/'.join(nodes)
@ -199,7 +203,9 @@ class FachoXML:
if parent is None: if parent is None:
parent = self.find_or_create_element(xpath, append=append) 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): def register_alias_xpath(self, alias, xpath):
self.xpath_for[alias] = xpath self.xpath_for[alias] = xpath
@ -235,7 +241,8 @@ class FachoXML:
""" """
xpath = self._path_xpath_for(xpath) xpath = self._path_xpath_for(xpath)
node_paths = xpath.split('/') node_paths = xpath.split('/')
node_paths.pop(0) #remove empty / # remove empty /
node_paths.pop(0)
root_tag = node_paths.pop(0) root_tag = node_paths.pop(0)
root_node = self.builder.build_from_expression(root_tag) root_node = self.builder.build_from_expression(root_tag)
@ -245,8 +252,8 @@ class FachoXML:
root_node = self.root root_node = self.root
if not self.builder.same_tag(root_node.tag, self.root.tag): if not self.builder.same_tag(root_node.tag, self.root.tag):
raise ValueError('xpath %s must be absolute to /%s' % (
raise ValueError('xpath %s must be absolute to /%s' % (xpath, self.root.tag)) xpath, self.root.tag))
# crea jerarquia segun xpath indicado # crea jerarquia segun xpath indicado
parent = None parent = None
@ -256,8 +263,8 @@ class FachoXML:
for node_path in node_paths: for node_path in node_paths:
node_expr = self.builder.match_expression(node_path) node_expr = self.builder.match_expression(node_path)
node = self.builder.build_from_expression(node_path) node = self.builder.build_from_expression(node_path)
child = self.builder.find_relative(
child = self.builder.find_relative(current_elem, node_expr['path'], self.nsmap) current_elem, node_expr['path'], self.nsmap)
parent = current_elem parent = current_elem
if child is not None: if child is not None:
@ -268,7 +275,8 @@ class FachoXML:
node_expr = self.builder.match_expression(node_tag) node_expr = self.builder.match_expression(node_tag)
node = self.builder.build_from_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 parent = current_elem
if child is not None: if child is not None:
current_elem = child current_elem = child
@ -289,7 +297,8 @@ class FachoXML:
self.builder.append(parent, node) self.builder.append(parent, node)
return 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) self._remove_facho_attributes(last_slibing)
return last_slibing return last_slibing
self.builder.append_next(last_slibing, node) self.builder.append_next(last_slibing, node)
@ -302,7 +311,8 @@ class FachoXML:
self._remove_facho_attributes(current_elem) self._remove_facho_attributes(current_elem)
return 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 validador al asignar contenido a xpath indicado
@ -316,7 +326,8 @@ class FachoXML:
else: else:
self._validators[key] = validator 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. asigna contenido ubicado por ruta tipo XPATH.
@param xpath ruta tipo XPATH @param xpath ruta tipo XPATH
@ -358,7 +369,8 @@ class FachoXML:
self.builder.set_attribute(elem, k, str(v)) self.builder.set_attribute(elem, k, str(v))
return self 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) elem = self.get_element(xpath, multiple=multiple)
if elem is None: if elem is None:
@ -395,14 +407,16 @@ class FachoXML:
return None return None
return format_(text) 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('/') parts = xpath.split('/')
is_attribute = parts[-1].startswith('@') is_attribute = parts[-1].startswith('@')
if is_attribute: if is_attribute:
attribute_name = parts.pop(-1).lstrip('@') attribute_name = parts.pop(-1).lstrip('@')
element_path = "/".join(parts) element_path = "/".join(parts)
try: 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: if val is None:
return default return default
return val return val
@ -435,7 +449,8 @@ class FachoXML:
if isinstance(xpath, tuple): if isinstance(xpath, tuple):
val = xpath[0] val = xpath[0]
else: 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) vals.append(val)
return vals return vals
@ -457,7 +472,8 @@ class FachoXML:
return True return True
def _remove_facho_attributes(self, elem): 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): def tostring(self, **kw):
return self.builder.tostring(self.root, **kw) return self.builder.tostring(self.root, **kw)
@ -475,9 +491,11 @@ class FachoXML:
ns = nsmap[etree.QName(root).namespace] + ':' ns = nsmap[etree.QName(root).namespace] + ':'
if self.fragment_root_element is not None: 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: else:
new_xpath = '/' + ns + etree.QName(root).localname + '/' + xpath.lstrip('/') new_xpath = '/' + ns + etree.QName(root).localname + '/' + \
xpath.lstrip('/')
return new_xpath return new_xpath
def __str__(self): def __str__(self):

View File

@ -30,28 +30,45 @@ SCHEME_AGENCY_ATTRS = {
POLICY_ID = 'https://facturaelectronica.dian.gov.co/politicadefirma/v2/politicadefirmav2.pdf' 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.' 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 = { 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', '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', 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-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', 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2',
'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2', 'qdt': 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2',
'sts': 'dian:gov:co:facturaelectronica:Structures-2-1', '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', '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#', '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: def fe_from_string(document: str) -> FachoXML:
return FeXML.from_string(document) return FeXML.from_string(document)
@ -77,7 +94,7 @@ def mock_xades_policy():
class FeXML(FachoXML): class FeXML(FachoXML):
def __init__(self, root, namespace): def __init__(self, root, namespace):
# raise Exception(namespace)
super().__init__("{%s}%s" % (namespace, root), super().__init__("{%s}%s" % (namespace, root),
nsmap=NAMESPACES) nsmap=NAMESPACES)
@ -92,7 +109,8 @@ class FeXML(FachoXML):
return super().tostring(**kw)\ return super().tostring(**kw)\
.replace(xmlns_name + ':', '')\ .replace(xmlns_name + ':', '')\
.replace('xmlns:'+xmlns_name, 'xmlns')\ .replace('xmlns:'+xmlns_name, 'xmlns')\
.replace('schemaLocation', 'xsi:schemaLocation') .replace('http://www.dian.gov.co/contratos/facturaelectronica/v1', 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2')
class DianXMLExtensionCUDFE(FachoXMLExtension): class DianXMLExtensionCUDFE(FachoXMLExtension):

View File

@ -652,7 +652,6 @@ class DIANInvoiceXML(fe.FeXML):
fexml.placeholder_for('./cbc:ProfileExecutionID') fexml.placeholder_for('./cbc:ProfileExecutionID')
fexml.set_element('./cbc:ID', invoice.invoice_ident) fexml.set_element('./cbc:ID', invoice.invoice_ident)
fexml.placeholder_for('./cbc:UUID') fexml.placeholder_for('./cbc:UUID')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d')) fexml.set_element('./cbc:IssueDate', invoice.invoice_issue.strftime('%Y-%m-%d'))
#DIAN 1.7.-2020: FAD10 #DIAN 1.7.-2020: FAD10
fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00')) fexml.set_element('./cbc:IssueTime', invoice.invoice_issue.strftime('%H:%M:%S-05:00'))
@ -661,24 +660,22 @@ class DIANInvoiceXML(fe.FeXML):
listAgencyID='195', listAgencyID='195',
listAgencyName='No matching global declaration available for the validation root', listAgencyName='No matching global declaration available for the validation root',
listURI='http://www.dian.gov.co') listURI='http://www.dian.gov.co')
fexml.set_element('./cbc:DocumentCurrencyCode', 'COP')
fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines)) fexml.set_element('./cbc:LineCountNumeric', len(invoice.invoice_lines))
fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (fexml.tag_document()), fexml.set_element('./cac:%sPeriod/cbc:StartDate' % (fexml.tag_document()),
invoice.invoice_period_start.strftime('%Y-%m-%d')) invoice.invoice_period_start.strftime('%Y-%m-%d'))
fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (fexml.tag_document()), fexml.set_element('./cac:%sPeriod/cbc:EndDate' % (fexml.tag_document()),
invoice.invoice_period_end.strftime('%Y-%m-%d')) invoice.invoice_period_end.strftime('%Y-%m-%d'))
fexml.set_billing_reference(invoice)
fexml.customize(invoice) fexml.customize(invoice)
fexml.set_supplier(invoice) fexml.set_supplier(invoice)
fexml.set_customer(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_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_allowance_charge(invoice)
fexml.set_billing_reference(invoice)
return fexml return fexml
def customize(fexml, invoice): def customize(fexml, invoice):