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 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<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+)\"?')
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):

View File

@ -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):

View File

@ -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):