feat: UPDATE Habilitacion RUSTIK
This commit is contained in:
parent
b7c9f2b201
commit
8f327f7abc
@ -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):
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user