import invoice-line

This commit is contained in:
Frederik Jaeckel 2025-01-09 17:01:34 +01:00
parent e8cceb75c1
commit 1b440525ac
2 changed files with 339 additions and 48 deletions

View file

@ -8,6 +8,7 @@
import os.path import os.path
from lxml import etree from lxml import etree
from datetime import datetime from datetime import datetime
from decimal import Decimal
from trytond.pool import PoolMeta, Pool from trytond.pool import PoolMeta, Pool
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.exceptions import UserError from trytond.exceptions import UserError
@ -70,48 +71,46 @@ class Incoming(metaclass=PoolMeta):
invoice = xml_read_func(invoice, xmltree) invoice = xml_read_func(invoice, xmltree)
return invoice return invoice
def _readxml_xpath(self, tags):
""" generate xpath
Args:
tags (list): list of string or integer to build path
"""
parts = []
for x in tags:
if isinstance(x, str):
parts.append(x)
elif isinstance(x, int):
if parts[-1].endswith(']'):
raise ValueError('multiple list selector')
parts[-1] += '[%d]' % x
result = '/' + '/'.join(parts)
return result
def _readxml_getvalue( def _readxml_getvalue(
self, xmltree, tags, vtype=None, allow_list=False, self, xmltree, tags, vtype=None, allow_list=False):
childs=[]):
""" read 'text'-part from xml-xpath, """ read 'text'-part from xml-xpath,
convert to 'vtype' convert to 'vtype'
Args: Args:
tags (list): list of tags to build xpath tags (list): list of tags to build xpath
vtype (type-class, optional): to convert value of text-part vtype (type-class, optional): to convert value of text-part
(if not childs). Defaults to None. Defaults to None.
allow_list (boolean, optional): get result as list of values, allow_list (boolean, optional): get result as list of values,
Defaults to False. Defaults to False.
childs (list): read child-items of selected node
[('ns', 'tag', <type>),...],
if emtpy: read text-part of selected node, Defaults to []
Returns: Returns:
various: converted value or None various: converted value or None
""" """
result = [] result = []
xpath = '/' + '/'.join(tags) xpath = self._readxml_xpath(tags)
nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap) nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap)
# dict to find children of selected node
childs_dict = {
'{%(ns)s}%(tag)s' % {'ns': xmltree.nsmap[x[0]], 'tag': x[1]}: {
'type': x[2], 'tag': x[1]}
for x in childs}
if nodes: if nodes:
for node1 in nodes: for node1 in nodes:
if not childs_dict: result.append(
result.append( node1.text if vtype is None else vtype(node1.text))
node1.text if vtype is None else vtype(node1.text))
else:
values = {}
for x in node1.getchildren():
if x.tag in childs_dict.keys():
values[childs_dict[x.tag]['tag']] = (
x.text if childs_dict[x.tag]['type'] is None
else childs_dict[x.tag]['type'](x.text))
result.append(values)
if not allow_list: if not allow_list:
break break
@ -134,7 +133,7 @@ class Incoming(metaclass=PoolMeta):
various: converted value or None various: converted value or None
""" """
result = None result = None
xpath = '/' + '/'.join(tags) xpath = self._readxml_xpath(tags)
nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap) nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap)
if nodes: if nodes:
result = nodes[0].attrib.get(attrib, None) result = nodes[0].attrib.get(attrib, None)
@ -300,28 +299,30 @@ class Incoming(metaclass=PoolMeta):
config = Configuration.get_singleton() config = Configuration.get_singleton()
# rsm:CrossIndustryInvoice
xpath_cross_ind = ['rsm:CrossIndustryInvoice']
# rsm:CrossIndustryInvoice/rsm:ExchangedDocument
xpath_exchg_doc = xpath_cross_ind + ['rsm:ExchangedDocument']
# check invoice-type: # check invoice-type:
# allowed codes 380 incoice, 381 credit note # allowed codes 380 incoice, 381 credit note
inv_code = self._readxml_getvalue(xmltree, [ inv_code = self._readxml_getvalue(
'rsm:CrossIndustryInvoice', xmltree, xpath_exchg_doc + ['ram:TypeCode'], int)
'rsm:ExchangedDocument', 'ram:TypeCode'], int)
if inv_code not in [380, 381]: if inv_code not in [380, 381]:
raise UserError(gettext( raise UserError(gettext(
'document_incoming_invoice_xml.msg_convert_error', 'document_incoming_invoice_xml.msg_convert_error',
msg='invalid type-code: %(code)s (expect: 380, 381)' % { msg='invalid type-code: %(code)s (expect: 380, 381)' % {
'code': str(inv_code)})) 'code': str(inv_code)}))
invoice_number = self._readxml_getvalue(xmltree, [ invoice_number = self._readxml_getvalue(
'rsm:CrossIndustryInvoice', xmltree, xpath_exchg_doc + ['ram:ID'])
'rsm:ExchangedDocument', 'ram:ID'])
if config and config.number_target == 'number': if config and config.number_target == 'number':
invoice.number = invoice_number invoice.number = invoice_number
else: else:
invoice.reference = invoice_number invoice.reference = invoice_number
# invoice-date # invoice-date
date_path = [ date_path = xpath_exchg_doc + [
'rsm:CrossIndustryInvoice', 'rsm:ExchangedDocument',
'ram:IssueDateTime', 'udt:DateTimeString'] 'ram:IssueDateTime', 'udt:DateTimeString']
date_format = self._readxml_getattrib(xmltree, date_path, 'format') date_format = self._readxml_getattrib(xmltree, date_path, 'format')
if date_format != '102': if date_format != '102':
@ -333,12 +334,19 @@ class Incoming(metaclass=PoolMeta):
self._readxml_getvalue(xmltree, date_path)) self._readxml_getvalue(xmltree, date_path))
# IncludedNote, 1st line --> 'description', 2nd ff. --> 'comment' # IncludedNote, 1st line --> 'description', 2nd ff. --> 'comment'
note_list = self._readxml_getvalue(xmltree, [ xpath_notes = xpath_exchg_doc + ['ram:IncludedNote']
'rsm:CrossIndustryInvoice', # count number of notes
'rsm:ExchangedDocument', 'ram:IncludedNote'], num_nodes = len(xmltree.xpath(
allow_list=True, self._readxml_xpath(xpath_notes), namespaces=xmltree.nsmap))
childs=[('ram', 'Content', str), ('ram', 'ContentCode', str),
('ram', 'SubjectCode', str)]) # read notes and their fields
note_list = []
for x in range(1, num_nodes + 1):
note_list.append({
y: self._readxml_getvalue(
xmltree, xpath_notes + [x, 'ram:%s' % y])
for y in ['Content', 'ContentCode', 'SubjectCode']
})
if note_list: if note_list:
invoice.description = note_list[0].get('Content', None) invoice.description = note_list[0].get('Content', None)
@ -351,12 +359,20 @@ class Incoming(metaclass=PoolMeta):
'msg': x.get('Content', ''), 'msg': x.get('Content', ''),
} for x in note_list[1:]]) } for x in note_list[1:]])
# rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction
xpath_suppl_chain = xpath_cross_ind + [
'rsm:SupplyChainTradeTransaction']
# rsm:CrossIndustryInvoice/
# rsm:SupplyChainTradeTransaction/
# ram:ApplicableHeaderTradeAgreement
xpath_appl_head_agree = xpath_suppl_chain + [
'ram:ApplicableHeaderTradeAgreement']
# supplier party # supplier party
add_party = config and config.create_supplier add_party = config and config.create_supplier
seller_party = self._readxml_party_data(xmltree, [ seller_party = self._readxml_party_data(
'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction', xmltree, xpath_appl_head_agree + ['ram:SellerTradeParty'],
'ram:ApplicableHeaderTradeAgreement', create_party=add_party)
'ram:SellerTradeParty'], create_party=add_party)
if seller_party: if seller_party:
if 'party' in seller_party.keys(): if 'party' in seller_party.keys():
invoice.party = seller_party['party'] invoice.party = seller_party['party']
@ -369,10 +385,9 @@ class Incoming(metaclass=PoolMeta):
for x in seller_party.keys()]))) for x in seller_party.keys()])))
# company party # company party
buyer_party = self._readxml_party_data(xmltree, [ buyer_party = self._readxml_party_data(
'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction', xmltree, xpath_appl_head_agree + ['ram:BuyerTradeParty'],
'ram:ApplicableHeaderTradeAgreement', create_party=False)
'ram:BuyerTradeParty'], create_party=False)
# check if we found our company # check if we found our company
if config and not config.accept_other_company: if config and not config.accept_other_company:
company_party_id = self._readxml_find_party(buyer_party) company_party_id = self._readxml_find_party(buyer_party)
@ -384,10 +399,186 @@ class Incoming(metaclass=PoolMeta):
buyer_party[x].replace('\n', '; ') buyer_party[x].replace('\n', '; ')
for x in buyer_party.keys()]))) for x in buyer_party.keys()])))
# lines of invoice # invoice - lines
# rsm:CrossIndustryInvoice/
# rsm:SupplyChainTradeTransaction/
# ram:IncludedSupplyChainTradeLineItem
xpath_line_item = xpath_suppl_chain + [
'ram:IncludedSupplyChainTradeLineItem']
# get number of invoice-lines
num_lines = len(xmltree.xpath(
self._readxml_xpath(xpath_line_item), namespaces=xmltree.nsmap))
print('\n## num_lines:', num_lines)
lines_data = []
for x in range(1, num_lines + 1):
lines_data.append(
self._readxml_invoice_line(xmltree, xpath_line_item, x))
print('\n## lines_data:', lines_data)
raise ValueError('stop')
return invoice return invoice
def _readxml_invoice_line(self, xmldata, xpth, pos):
""" read invoice-line from xml
Args:
xmldata (): xml-tree object
xpth (list): xpath to invoice-line
pos (int): number of line to read
Returns:
dict: invoice line data
"""
def read_listdata(xtag, key_list):
""" read list of values from xml
Args:
xtag (str): xml-tag to read as list
key_list (list): [('<xtag>', '<key>')]
Returns:
list: list of dict
"""
if isinstance(xtag, str):
xtag = [xtag]
xpath_list = xpth + [pos] + xtag
num_listitem = len(xmldata.xpath(
self._readxml_xpath(xpath_list), namespaces=xmldata.nsmap))
listdata = []
for x in range(1, num_listitem + 1):
values = {}
for list_item in key_list:
(xkey, key, xtype) = (
list_item if len(list_item) == 3
else tuple(list(list_item) + [None]))
value = self._readxml_getvalue(
xmldata, xpath_list + [x, xkey], vtype=xtype)
if value is not None:
values[key] = value
if values:
listdata.append(values)
return listdata
result = {'convert_note': []}
result['line_no'] = self._readxml_getvalue(xmldata, xpth + [pos] + [
'ram:AssociatedDocumentLineDocument', 'ram:LineID'])
result['line_note'] = '\n'.join(self._readxml_getvalue(
xmldata, xpth + [pos] + [
'ram:AssociatedDocumentLineDocument', 'ram:IncludedNote',
'ram:Content'], allow_list=True))
for xkey, key in [
('ram:ID', 'prod_id'),
('ram:GlobalID', 'glob_id'),
('ram:SellerAssignedID', 'seller_id'),
('ram:BuyerAssignedID', 'buyer_id'),
('ram:IndustryAssignedID', 'industy_id'),
('ram:ModelID', 'model_id'),
('ram:Name', 'name'),
('ram:Description', 'description'),
('ram:BatchID', 'lot'),
('ram:BrandName', 'brand_name'),
('ram:ModelName', 'model_name'),
('ram:OriginTradeCountry', 'trade_country')
]:
result[key] = self._readxml_getvalue(xmldata, xpth + [pos] + [
'ram:SpecifiedTradeProduct', xkey])
# attributes of product
result['attributes'] = read_listdata([
'ram:SpecifiedTradeProduct',
'ram:ApplicableProductCharacteristic'], [
('ram:TypeCode', 'code'),
('ram:Description', 'description'),
('ram:ValueMeasure', 'uom'),
('ram:ValueY', 'value')])
# classification of product
result['classification'] = read_listdata([
'ram:SpecifiedTradeProduct',
'ram:DesignatedProductClassification'], [
('ram:ClassCode', 'code'),
('ram:ClassName', 'name')])
# serial-numbers of product
result['serialno'] = read_listdata([
'ram:SpecifiedTradeProduct',
'ram:IndividualTradeProductInstance'], [
('ram:BatchID', 'lot'),
('ram:SupplierAssignedSerialID', 'serial')])
# referenced product
result['refprod'] = read_listdata(
['ram:SpecifiedTradeProduct', 'ram:IncludedReferencedProduct'], [
('ram:ID', 'id'),
('ram:GlobalID', 'global_id'),
('ram:SellerAssignedID', 'seller_id'),
('ram:BuyerAssignedID', 'buyer_id'),
('ram:Name', 'name'),
('ram:Description', 'description'),
('ram:UnitQuantity', 'quantity', Decimal),
])
# net price
xpath_netprice = xpth + [pos] + [
'ram:SpecifiedLineTradeAgreement', 'ram:NetPriceProductTradePrice']
if self._readxml_getvalue(xmldata, xpath_netprice):
result['unit_net_price'] = read_listdata([
'ram:SpecifiedLineTradeAgreement',
'ram:NetPriceProductTradePrice'], [
('ram:ChargeAmount', 'amount', Decimal),
('ram:BasisQuantity', 'basequantity', Decimal)])
# notice ignored field
if self._readxml_getvalue(xmldata, xpath_netprice + [
'ram:AppliedTradeAllowanceCharge']):
result['convert_note'].append(
'skip: ' + self._readxml_xpath(
xpath_netprice + ['ram:AppliedTradeAllowanceCharge']))
# gross price
xpath_grossprice = xpth + [pos] + [
'ram:SpecifiedLineTradeAgreement',
'ram:GrossPriceProductTradePrice']
if self._readxml_getvalue(xmldata, xpath_grossprice):
result['unit_gross_price'] = read_listdata([
'ram:SpecifiedLineTradeAgreement',
'ram:GrossPriceProductTradePrice'], [
('ram:ChargeAmount', 'amount', Decimal),
('ram:BasisQuantity', 'basequantity', Decimal)])
# notice ignored field
if self._readxml_getvalue(xmldata, xpath_grossprice + [
'ram:AppliedTradeAllowanceCharge']):
result['convert_note'].append(
'skip: ' + self._readxml_xpath(
xpath_grossprice + ['ram:AppliedTradeAllowanceCharge']))
# quantity
xpath_quantity = xpth + [pos] + ['ram:SpecifiedLineTradeDelivery']
result['quantity'] = read_listdata(
['ram:SpecifiedLineTradeDelivery'], [
('ram:BilledQuantity', 'billed', Decimal),
('ram:ChargeFreeQuantity', 'chargefree', Decimal),
('ram:PackageQuantity', 'package', Decimal),
])
# notice ignored fields
for x in [
'ShipToTradeParty', 'UltimateShipToTradeParty',
'ActualDeliverySupplyChainEvent',
'DespatchAdviceReferencedDocument',
'ReceivingAdviceReferencedDocument',
'DeliveryNoteReferencedDocument']:
xp_to_check = xpath_quantity + ['ram:' + x]
if self._readxml_getvalue(xmldata, xp_to_check):
result['convert_note'].append(
'skip: ' + self._readxml_xpath(xp_to_check))
# taxes
# skip None values
return {x: result[x] for x in result.keys() if result[x]}
def _readxml_convertdate(self, date_string): def _readxml_convertdate(self, date_string):
""" convert date-string to python-date """ convert date-string to python-date

View file

@ -52,6 +52,106 @@
</ram:SpecifiedTradeSettlementLineMonetarySummation> </ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement> </ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem> </ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
<ram:IncludedNote>
<ram:Content>Description of Line 2</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Description of Line 2, line 2</ram:Content>
</ram:IncludedNote>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:ID>2</ram:ID>
<ram:GlobalID>3</ram:GlobalID>
<ram:SellerAssignedID>4</ram:SellerAssignedID>
<ram:BuyerAssignedID>5</ram:BuyerAssignedID>
<ram:IndustryAssignedID>6</ram:IndustryAssignedID>
<ram:ModelID>7</ram:ModelID>
<ram:Name>Name of Product 2</ram:Name>
<ram:Description>Description of Product 2</ram:Description>
<ram:BatchID>batch23</ram:BatchID>
<ram:BrandName>Brand-Name</ram:BrandName>
<ram:ModelName>Model-Name</ram:ModelName>
<ram:ApplicableProductCharacteristic>
<ram:TypeCode>123</ram:TypeCode>
<ram:Description>Kilogram</ram:Description>
<ram:ValueMeasure>kg</ram:ValueMeasure>
<ram:ValueY>23</ram:ValueY>
</ram:ApplicableProductCharacteristic>
<ram:DesignatedProductClassification>
<ram:ClassCode>3c</ram:ClassCode>
<ram:ClassName>product-class 1</ram:ClassName>
</ram:DesignatedProductClassification>
<ram:IndividualTradeProductInstance>
<ram:BatchID>22</ram:BatchID>
<ram:SupplierAssignedSerialID>1234</ram:SupplierAssignedSerialID>
</ram:IndividualTradeProductInstance>
<ram:OriginTradeCountry>DE</ram:OriginTradeCountry>
<ram:IncludedReferencedProduct>
<ram:ID>1</ram:ID>
<ram:GlobalID>2</ram:GlobalID>
<ram:SellerAssignedID>3</ram:SellerAssignedID>
<ram:BuyerAssignedID>4</ram:BuyerAssignedID>
<ram:Name>ref-prod-1</ram:Name>
<ram:Description>description of ref-prod-1</ram:Description>
<ram:UnitQuantity>1.0</ram:UnitQuantity>
</ram:IncludedReferencedProduct>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount currencyID="EUR">800.00</ram:ChargeAmount>
<ram:BasisQuantity unitCode="C62">1.0</ram:BasisQuantity>
</ram:NetPriceProductTradePrice>
<ram:GrossPriceProductTradePrice>
<ram:ChargeAmount currencyID="EUR">950.00</ram:ChargeAmount>
<ram:BasisQuantity unitCode="C62">1.0</ram:BasisQuantity>
</ram:GrossPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">1.5</ram:BilledQuantity>
<ram:PackageQuantity unitCode="C62">1.5</ram:PackageQuantity>
<ram:ActualDeliverySupplyChainEvent format="102">20240101</ram:ActualDeliverySupplyChainEvent>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount currencyID="EUR">1200.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>3</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Name of Product 3</ram:Name>
<ram:Description>Description of Product 3</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount currencyID="EUR">150.00</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">2.0</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>7.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount currencyID="EUR">300.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement> <ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty> <ram:SellerTradeParty>
<ram:Name>Name of the Supplier</ram:Name> <ram:Name>Name of the Supplier</ram:Name>