From 18103d8e800804b9825ab637e72d7797f26dd27d Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Fri, 10 Jan 2025 17:25:05 +0100 Subject: [PATCH] read invoice-line, taxes --- document.py | 162 +++++++++++++++++++++++++++++++++++++++++++++++++-- locale/de.po | 12 ++++ locale/en.po | 12 ++++ message.xml | 9 +++ 4 files changed, 189 insertions(+), 6 deletions(-) diff --git a/document.py b/document.py index 16f9d30..d0aa3e7 100644 --- a/document.py +++ b/document.py @@ -7,7 +7,7 @@ import os.path from lxml import etree -from datetime import datetime +from datetime import datetime, date from decimal import Decimal from trytond.pool import PoolMeta, Pool from trytond.transaction import Transaction @@ -110,7 +110,9 @@ class Incoming(metaclass=PoolMeta): if nodes: for node1 in nodes: result.append( - node1.text if vtype is None else vtype(node1.text)) + node1.text if vtype is None + else self._readxml_convertdate(node1.text) if vtype == date + else vtype(node1.text)) if not allow_list: break @@ -414,10 +416,118 @@ class Incoming(metaclass=PoolMeta): lines_data.append( self._readxml_invoice_line(xmltree, xpath_line_item, x)) print('\n## lines_data:', lines_data) + lines = [ + self._readxml_getinvoiceline(invoice, x) + for x in lines_data] + invoice.lines = lines + for pos in range(len(lines)): + invoice.lines[pos].on_change_account() + invoice.on_change_lines() raise ValueError('stop') return invoice + def _readxml_getinvoiceline(self, invoice, line_data): + """ create invoice line in memory + + Args: + line_data (dict): values from xml to create + invoice line + """ + pool = Pool() + Line = pool.get('account.invoice.line') + Tax = pool.get('account.tax') + Configuration = pool.get('document.incoming.configuration') + + cfg1 = Configuration.get_singleton() + print('\n## line_data-vor:', line_data) + + def get_tax_by_percent(percent): + """ search for tax by percent of supplier tax + + Args: + percent (Decimal): tax percent, 7% --> 0.07 + + Raises: + UserError: if no product category is configured or + no matching tax was found + + Returns: + record: model 'account.tax + """ + if not (cfg1 and cfg1.product_category): + raise UserError(gettext( + 'document_incoming_invoice_xml.msg_no_prodcat_configured')) + + for pcat in cfg1.product_category: + for s_tax in pcat.supplier_taxes: + # get current taxes of supplier-tax at + # invoice-date + current_taxes = Tax.compute( + [s_tax], Decimal('1'), 1.0, invoice.invoice_date) + + # deny result of multiple or none taxes + if len(current_taxes) != 1: + continue + + if (current_taxes[0]['tax'].rate == percent) and ( + current_taxes[0]['tax'].type == 'percentage'): + # found it + return s_tax + + # no product category found + raise UserError(gettext( + 'document_incoming_invoice_xml.msg_no_prodcat_found', + percent=percent * Decimal('100.0'))) + + line = Line( + invoice=invoice, + type='line', + quantity=line_data.get('quantity', {}).pop('billed', None), + unit_price=line_data.get('unit_net_price', {}).pop('amount', None)) + line_no = line_data.pop('line_no', None) + + # description + descr = [ + '[%s]' % line_no if line_no else None, + line_data.pop('name', None), + line_data.pop('description', None)] + line.description = '; '.join([x for x in descr if x]) + + # taxes + taxes = [] + line_taxes = line_data.get('taxes', []) + for x in line_taxes: + percent = x.get('percent', None) + if (x.get('type', '') == 'VAT') and (percent is not None): + percent = percent / Decimal('100') + taxes.append(get_tax_by_percent(percent)) + # remove not-found-taxes + taxes = [x for x in taxes if x] + # check result + if len(line_taxes) != len(taxes): + raise UserError(gettext( + 'document_incoming_invoice_xml.msg_numtaxes_invalid', + descr=line.description, + taxin='|'.join([ + str(x.get('percent', '-')) for x in line_taxes]), + taxout='|'.join([ + str(x.rate * Decimal('100.0')) for x in taxes]))) + line.taxes = taxes + + #line.account = + + # cleanup used values + for x in ['quantity', 'unit_net_price']: + if not line_data.get(x, {}): + del line_data[x] + + line.on_change_invoice() + + print('-- line_data-nach:', line_data, (line,)) + return line + + def _readxml_invoice_line(self, xmldata, xpth, pos): """ read invoice-line from xml @@ -529,7 +639,7 @@ class Incoming(metaclass=PoolMeta): 'ram:SpecifiedLineTradeAgreement', 'ram:NetPriceProductTradePrice'], [ ('ram:ChargeAmount', 'amount', Decimal), - ('ram:BasisQuantity', 'basequantity', Decimal)]) + ('ram:BasisQuantity', 'basequantity', Decimal)])[0] # notice ignored field if self._readxml_getvalue(xmldata, xpath_netprice + [ 'ram:AppliedTradeAllowanceCharge']): @@ -546,13 +656,14 @@ class Incoming(metaclass=PoolMeta): 'ram:SpecifiedLineTradeAgreement', 'ram:GrossPriceProductTradePrice'], [ ('ram:ChargeAmount', 'amount', Decimal), - ('ram:BasisQuantity', 'basequantity', Decimal)]) + ('ram:BasisQuantity', 'basequantity', Decimal)])[0] # notice ignored field if self._readxml_getvalue(xmldata, xpath_grossprice + [ 'ram:AppliedTradeAllowanceCharge']): result['convert_note'].append( 'skip: ' + self._readxml_xpath( - xpath_grossprice + ['ram:AppliedTradeAllowanceCharge'])) + xpath_grossprice + + ['ram:AppliedTradeAllowanceCharge'])) # quantity xpath_quantity = xpth + [pos] + ['ram:SpecifiedLineTradeDelivery'] @@ -561,7 +672,7 @@ class Incoming(metaclass=PoolMeta): ('ram:BilledQuantity', 'billed', Decimal), ('ram:ChargeFreeQuantity', 'chargefree', Decimal), ('ram:PackageQuantity', 'package', Decimal), - ]) + ])[0] # notice ignored fields for x in [ 'ShipToTradeParty', 'UltimateShipToTradeParty', @@ -575,6 +686,45 @@ class Incoming(metaclass=PoolMeta): 'skip: ' + self._readxml_xpath(xp_to_check)) # taxes + xpath_trade = xpth + [pos] + ['ram:SpecifiedLineTradeSettlement'] + result['taxes'] = read_listdata([ + 'ram:SpecifiedLineTradeSettlement', + 'ram:ApplicableTradeTax'], [ + ('ram:CalculatedAmount', 'amount', Decimal), + ('ram:TypeCode', 'type'), + ('ram:ExemptionReason', 'reason'), + ('ram:BasisAmount', 'base', Decimal), + ('ram:LineTotalBasisAmount', 'basetotal', Decimal), + ('ram:AllowanceChargeBasisAmount', 'feebase', Decimal), + ('ram:CategoryCode', 'category_code'), + ('ram:ExemptionReasonCode', 'reason_code'), + ('ram:TaxPointDate', 'taxdate', date), + ('ram:DueDateTypeCode', 'duecode'), + ('ram:RateApplicablePercent', 'percent', Decimal)]) + + # total amounts + result['total'] = read_listdata([ + 'ram:SpecifiedLineTradeSettlement', + 'ram:SpecifiedTradeSettlementLineMonetarySummation'], [ + ('ram:LineTotalAmount', 'amount', Decimal), + ('ram:ChargeTotalAmount', 'charge', Decimal), + ('ram:AllowanceTotalAmount', 'fee', Decimal), + ('ram:TaxTotalAmount', 'tax', Decimal), + ('ram:GrandTotalAmount', 'grand', Decimal), + ('ram:TotalAllowanceChargeAmount', 'feecharge', Decimal), + ]) + + # notice ignored fields + for x in [ + 'BillingSpecifiedPeriod', 'SpecifiedTradeAllowanceCharge', + 'ActualDeliverySupplyChainEvent', + 'InvoiceReferencedDocument', + 'AdditionalReferencedDocument', + 'ReceivableSpecifiedTradeAccountingAccount']: + xp_to_check = xpath_trade + ['ram:' + x] + if self._readxml_getvalue(xmldata, xp_to_check): + result['convert_note'].append( + 'skip: ' + self._readxml_xpath(xp_to_check)) # skip None values return {x: result[x] for x in result.keys() if result[x]} diff --git a/locale/de.po b/locale/de.po index 55247a1..10ce046 100644 --- a/locale/de.po +++ b/locale/de.po @@ -22,6 +22,18 @@ msgctxt "model:ir.message,text:msg_not_our_company" msgid "The buyer party '%(partytxt)s' differs from the corporate party." msgstr "Die Käuferpartei '%(partytxt)s' weicht von der Unternehmenspartei ab." +msgctxt "model:ir.message,text:msg_no_prodcat_configured" +msgid "There is no product category configured for XML import in Document-Incoming." +msgstr "Es ist in Document-Incoming keine Produktkategorie für den XML-Import kofiguriert." + +msgctxt "model:ir.message,text:msg_no_prodcat_found" +msgid "No product category with a %(percent)s %% tax found." +msgstr "Keine Produktkategorie mit einer Steuer von %(percent)s %% gefunden." + +msgctxt "model:ir.message,text:msg_numtaxes_invalid" +msgid "Invalid number of taxes in line %(descr)s: wanted='%(taxin)s', found='%(taxout)s'." +msgstr "Ungültige Anzahl Steuern in Zeile %(descr)s: gesucht='%(taxin)s', gefunden='%(taxout)s'." + ################################### # document.incoming.configuration # diff --git a/locale/en.po b/locale/en.po index c5e6a63..c24e952 100644 --- a/locale/en.po +++ b/locale/en.po @@ -18,6 +18,18 @@ msgctxt "model:ir.message,text:msg_not_our_company" msgid "The buyer party '%(partytxt)s' differs from the corporate party." msgstr "The buyer party '%(partytxt)s' differs from the corporate party." +msgctxt "model:ir.message,text:msg_no_prodcat_configured" +msgid "There is no product category configured for XML import in Document-Incoming." +msgstr "There is no product category configured for XML import in Document-Incoming." + +msgctxt "model:ir.message,text:msg_no_prodcat_found" +msgid "No product category with a %(percent)s %% tax found." +msgstr "No product category with a %(percent)s %% tax found." + +msgctxt "model:ir.message,text:msg_numtaxes_invalid" +msgid "Invalid number of taxes in line %(descr)s: wanted='%(taxin)s', found='%(taxout)s'." +msgstr "Invalid number of taxes in line %(descr)s: wanted='%(taxin)s', found='%(taxout)s'." + msgctxt "field:document.incoming.configuration,create_supplier:" msgid "Create Supplier Party" msgstr "Create Supplier Party" diff --git a/message.xml b/message.xml index dc825f7..19ebe70 100644 --- a/message.xml +++ b/message.xml @@ -17,6 +17,15 @@ The buyer party '%(partytxt)s' differs from the corporate party. + + There is no product category configured for XML import in Document-Incoming. + + + No product category with a %(percent)s %% tax found. + + + Invalid number of taxes in line %(descr)s: wanted='%(taxin)s', found='%(taxout)s'. +