read some fields

This commit is contained in:
Frederik Jaeckel 2025-01-06 17:52:48 +01:00
parent a5cbb23cdc
commit 0024f76192
14 changed files with 520 additions and 1 deletions

View file

@ -11,6 +11,7 @@ Requires
======== ========
- Tryton 7.0 - Tryton 7.0
Changes Changes
======= =======

View file

@ -4,8 +4,10 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from trytond.pool import Pool from trytond.pool import Pool
from .document import Incoming
def register(): def register():
Pool.register( Pool.register(
Incoming,
module='document_incoming_invoice_xml', type_='model') module='document_incoming_invoice_xml', type_='model')

227
document.py Normal file
View file

@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
# This file is part of the document-incoming-invoice-xml-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
# https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/xrechnung%20%28german%20cius%29/xrechnung_crossindustryinvoice%3B%202017-10-18.scm/html/021.htm?https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/xrechnung%20%28german%20cius%29/xrechnung_crossindustryinvoice%3B%202017-10-18.scm/html/025.htm
import os.path
from lxml import etree
from datetime import datetime
from trytond.pool import PoolMeta
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.model import fields
from trytond.pyson import Eval
xml_types = [
(['xsd', 'Factur-X_1.07.2_EXTENDED', 'Factur-X_1.07.2_EXTENDED.xsd'],
'Factur-X extended', 'facturx_extended'),
(['xsd', 'Factur-X_1.07.2_EN16931', 'Factur-X_1.07.2_EN16931.xsd'],
'Factur-X EN16931', ''),
(['xsd', 'Factur-X_1.07.2_BASIC', 'Factur-X_1.07.2_BASIC.xsd'],
'Factur-X basic', ''),
(['xsd', 'Factur-X_1.07.2_BASICWL', 'Factur-X_1.07.2_BASICWL.xsd'],
'Factur-X basic-wl', ''),
(['xsd', '/Factur-X_1.07.2_MINIMUM', 'Factur-X_1.07.2_MINIMUM.xsd'],
'Factur-X minimum', ''),
(['xsd', 'CII D22B XSD', 'CrossIndustryInvoice_100pD22B.xsd'],
'CrossIndustryInvoice D22', ''),
(['xsd', 'os-UBL-2.1', 'xsd/maindoc', 'UBL-Invoice-2.1.xsd'],
'XRechnung - Invoice', ''),
(['xsd', 'os-UBL-2.1', 'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd'],
'XRechnung - Credit Note', ''),
(['xsd', 'os-UBL-2.1', 'xsd', 'maindoc', 'UBL-DebitNote-2.1.xsd'],
'XRechnung - Debit Note', '')]
class Incoming(metaclass=PoolMeta):
__name__ = 'document.incoming'
xsd_type = fields.Char(
string='XML Content', readonly=True,
states={'invisible': Eval('mime_type', '') != 'application/xml'})
@classmethod
def default_company(cls):
return Transaction().context.get('company')
def _process_supplier_invoice(self):
""" try to detect content of 'data', read values
"""
invoice = super()._process_supplier_invoice()
self.xsd_type = None
if self.mime_type == 'application/xml':
# detect xml-content
xml_info = self._facturx_detect_content()
if xml_info:
(xsd_type, funcname, xmltree) = xml_info
self.xsd_type = xsd_type
xml_read_func = getattr(self, '_readxml_%s' % funcname, None)
if not xml_read_func:
raise UserError(gettext(
'document_incoming_invoice_xml.msg_not_implemented',
xmltype=xsd_type))
# read xml data
invoice = xml_read_func(invoice, xmltree)
return invoice
def _readxml_getvalue(
self, xmltree, tags, vtype=None, allow_list=False,
text_field=True):
""" read 'text'-part from xml-xpath,
convert to 'vtype'
Args:
tags (list): list of tags to build xpath
vtype (type-class, optional): to convert value. Defaults to None.
allow_list (boolean): get result as list of values
text_field (boolean): return content of 'text'-part of node
Returns:
various: converted value or None
"""
result = []
xpath = '/' + '/'.join(tags)
nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap)
if nodes:
if not text_field:
result.extend(nodes)
else:
for node1 in nodes:
if node1.text:
result.append(
node1.text if vtype is None
else vtype(node1.text))
if not allow_list:
return result[0]
return result
def _readxml_getattrib(self, xmltree, tags, attrib, vtype=None):
""" read attribute from xml-xpath,
convert to 'vtype'
Args:
tags (list): list fo tags to build xpath
attrib (str): name of attribute to read
vtype (type-class, optional): to convert value. Defaults to None.
Returns:
various: converted value or None
"""
result = None
xpath = '/' + '/'.join(tags)
nodes = xmltree.xpath(xpath, namespaces=xmltree.nsmap)
if nodes:
result = nodes[0].attrib.get(attrib, None)
if result and vtype:
result = vtype(result)
return result
def _readxml_facturx_extended(self, invoice, xmltree):
""" read factur-x extended
Args:
invoice (record): model account.invoice
Returns:
record: model account.invoice
"""
# check invoice-type:
# allowed codes 380 incoice, 381 credit note
inv_code = self._readxml_getvalue(xmltree, [
'rsm:CrossIndustryInvoice',
'rsm:ExchangedDocument', 'ram:TypeCode'], int)
if inv_code not in [380, 381]:
raise UserError(gettext(
'document_incoming_invoice_xml.msg_convert_error',
msg='invalid type-code: %(code)s (expect: 380, 381)' % {
'code': str(inv_code)}))
invoice.number = self._readxml_getvalue(xmltree, [
'rsm:CrossIndustryInvoice',
'rsm:ExchangedDocument', 'ram:ID'])
# invoice-date
date_path = [
'rsm:CrossIndustryInvoice', 'rsm:ExchangedDocument',
'ram:IssueDateTime', 'udt:DateTimeString']
date_format = self._readxml_getattrib(xmltree, date_path, 'format')
if date_format != '102':
raise UserError(gettext(
'document_incoming_invoice_xml.msg_convert_error',
msg='invalid date-format: %(code)s (expect: 102)' % {
'code': str(date_format)}))
invoice.invoice_date = self._readxml_convertdate(
self._readxml_getvalue(xmltree, date_path))
# IncludedNote, 1st line --> 'description', 2nd ff. --> 'comment'
note_list = self._readxml_getvalue(xmltree, [
'rsm:CrossIndustryInvoice',
'rsm:ExchangedDocument', 'ram:IncludedNote'],
allow_list=True, text_field=False)
print('\n## note_list:', (note_list,))
if note_list:
invoice.description = self._readxml_getvalue(
note_list[0], ['ram:Content'], allow_list=False)
if len(note_list) > 1:
for x in note_list[1:]:
print('- x:', x)
cont_code = self._readxml_getvalue(
x, ['ram:ContentCode'], allow_list=False)
print('- cont_code:', cont_code)
cont_str = self._readxml_getvalue(
x, ['ram:Content'], allow_list=False)
# descr_list = self._readxml_getvalue(xmltree, [
# 'rsm:CrossIndustryInvoice',
# 'rsm:ExchangedDocument', 'ram:IncludedNote',
# 'ram:Content'], allow_list=True)
# if descr_list:
# invoice.description = descr_list[0]
# if len(descr_list) > 1:
# invoice.comment = '\n'.join(descr_list[1:])
return invoice
def _readxml_convertdate(self, date_string):
""" convert date-string to python-date
Args:
date_string (_type_): _description_
"""
if not date_string:
return None
try:
return datetime.strptime(date_string, '%Y%m%d').date()
except ValueError:
raise UserError(gettext(
'document_incoming_invoice_xml.msg_convert_error',
msg='cannot convert %(val)s' % {
'val': date_string}))
def _facturx_detect_content(self):
""" check xml-data against xsd of XRechnung and FacturX,
begin with extended, goto minimal, then xrechnung
Returns:
tuple: ('xsd tyle', '<function to read xml>', <xml etree>)
defaults to None if not detected
"""
xml_data = etree.fromstring(self.data)
for xsdpath, xsdtype, funcname in xml_types:
fname = os.path.join(*[os.path.split(__file__)[0]] + xsdpath)
schema = etree.XMLSchema(etree.parse(fname))
try:
schema.assertValid(xml_data)
except etree.DocumentInvalid:
pass
return (xsdtype, funcname, xml_data)
return None
# end Incoming

13
document.xml Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!-- This file is part of the document-incoming-invoice-xml-module
from m-ds for Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="document_incoming_view_form">
<field name="model">document.incoming</field>
<field name="inherit" ref="document_incoming.document_incoming_view_form"/>
<field name="name">document_incoming_form</field>
</record>
</data>
</tryton>

15
locale/de.po Normal file
View file

@ -0,0 +1,15 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
##############
# ir.message #
##############
msgctxt "model:ir.message,text:msg_type_short_unique"
msgid "XML import for %(xmltype)s not implemented."
msgstr "XML import für %(xmltype)s nicht implementiert."
msgctxt "model:ir.message,text:msg_convert_error"
msgid "Conversion error: %(msg)s."
msgstr "Konvertierfehler: %(msg)s."

12
locale/en.po Normal file
View file

@ -0,0 +1,12 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "model:ir.message,text:msg_type_short_unique"
msgid "XML import for %(xmltype)s not implemented."
msgstr "XML import for %(xmltype)s not implemented."
msgctxt "model:ir.message,text:msg_convert_error"
msgid "Conversion error: %(msg)s."
msgstr "Conversion error: %(msg)s."

17
message.xml Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0"?>
<!-- This file is part of the document-incoming-invoice-xml-module
from m-ds for Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_not_implemented">
<field name="text">XML import for %(xmltype)s not implemented.</field>
</record>
<record model="ir.message" id="msg_convert_error">
<field name="text">Conversion error: %(msg)s.</field>
</record>
</data>
</tryton>

View file

@ -37,7 +37,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
major_version = 7 major_version = 7
minor_version = 0 minor_version = 0
requires = ['pypdf', 'factur-x'] requires = ['pypdf', 'factur-x', 'lxml']
for dep in info.get('depends', []): for dep in info.get('depends', []):
if not re.match(r'(ir|res|webdav)(\W|$)', dep): if not re.match(r'(ir|res|webdav)(\W|$)', dep):
if dep in modversion.keys(): if dep in modversion.keys():
@ -96,6 +96,7 @@ setup(
'trytond.modules.%s' % MODULE: ( 'trytond.modules.%s' % MODULE: (
info.get('xml', []) info.get('xml', [])
+ ['tryton.cfg', 'locale/*.po', 'tests/*.py', + ['tryton.cfg', 'locale/*.po', 'tests/*.py',
'view/*.xml',
'versiondep.txt', 'README.rst']), 'versiondep.txt', 'README.rst']),
}, },

4
tests/__init__.py Normal file
View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# This file is part of the document-incoming-invoice-xml-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

71
tests/document.py Normal file
View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# This file is part of the document-incoming-invoice-xml-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import os.path
from datetime import date
from trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.modules.company.tests import create_company, set_company
class DocumentTestCase(object):
""" check import of xml + pdf files
"""
def prep_incomingdoc_run_worker(self):
""" run tasks from queue
"""
Queue = Pool().get('ir.queue')
while True:
tasks = Queue.search([])
if not tasks:
break
for task in tasks:
task.run()
Queue.delete(tasks)
@with_transaction()
def test_xmldoc_import_facturx(self):
""" create incoming-document, load xml, detect type
"""
pool = Pool()
IncDocument = pool.get('document.incoming')
company = create_company('m-ds')
with set_company(company):
to_create = []
with open(os.path.join(
os.path.split(__file__)[0],
'facturx-extended.xml'), 'rb') as fhdl:
to_create.append({
'data': fhdl.read(),
'name': 'facturx-extended.xml',
'type': 'supplier_invoice'})
document, = IncDocument.create(to_create)
self.assertEqual(document.mime_type, 'application/xml')
self.assertEqual(document.company.id, company.id)
self.assertTrue(document.data.startswith(
b'<?xml version="1.0" encoding="UTF-8"?>\n' +
b'<rsm:CrossIndustryInvoice xmlns'))
invoice = document._process_supplier_invoice()
print('\n## invoice:', (invoice,))
self.assertEqual(invoice.type, 'in')
self.assertEqual(invoice.number, 'RE2024.01234')
self.assertEqual(invoice.invoice_date, date(2024, 6, 17))
self.assertEqual(invoice.currency.name, 'usd')
self.assertEqual(invoice.company.rec_name, 'm-ds')
invoice.save()
print('\n## invoice:', invoice)
# 'process' will queue the job to workers
IncDocument.process([document])
# run the usual call: process workers
self.prep_incomingdoc_run_worker()
# end DocumentTestCase

122
tests/facturx-extended.xml Normal file
View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>RE2024.01234</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240617</udt:DateTimeString>
</ram:IssueDateTime>
<ram:IncludedNote>
<ram:Content>Description of invoice</ram:Content>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Some notes to the customer.</ram:Content>
<ram:ContentCode>1</ram:ContentCode>
</ram:IncludedNote>
<ram:IncludedNote>
<ram:Content>Goes to field comment.</ram:Content>
<ram:ContentCode>22</ram:ContentCode>
</ram:IncludedNote>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name>Name of Product 1</ram:Name>
<ram:Description>Description of Product 1</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount currencyID="EUR">1350.00</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
</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">1350.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>Name of the Comany</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:LineOne>Street of Company No 1</ram:LineOne>
<ram:CityName>Berlin</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
<ram:CountrySubDivisionName>Berlin</ram:CountrySubDivisionName>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Customer Company</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:PostcodeCode>23456</ram:PostcodeCode>
<ram:LineOne>Address Line 1</ram:LineOne>
<ram:LineTwo>Address Line 2</ram:LineTwo>
<ram:CityName>Potsdam</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
<ram:CountrySubDivisionName>Brandenburg</ram:CountrySubDivisionName>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
<ram:BuyerOrderReferencedDocument>
<ram:IssuerAssignedID/>
</ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>RE2024.01234</ram:PaymentReference>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:Information>Wire transfer</ram:Information>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>DE02300209000106531065</ram:IBANID>
<ram:AccountName>mbs</ram:AccountName>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>WELADED1PMB</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount currencyID="EUR">256.5</ram:CalculatedAmount>
<ram:TypeCode>VAT</ram:TypeCode>
<ram:BasisAmount>1350</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:DueDateDateTime>
<udt:DateTimeString format="102">20240701</udt:DateTimeString>
</ram:DueDateDateTime>
<ram:PartialPaymentAmount currencyID="EUR">1606.50</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount currencyID="EUR">1350.00</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount currencyID="EUR">1350.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">256.5</ram:TaxTotalAmount>
<ram:GrandTotalAmount currencyID="EUR">1606.50</ram:GrandTotalAmount>
<ram:DuePayableAmount currencyID="EUR">1606.50</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

20
tests/test_module.py Normal file
View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# This file is part of the document-incoming-invoice-xml-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.tests.test_tryton import ModuleTestCase
from .document import DocumentTestCase
class XmlIncomingTestCase(
DocumentTestCase,
ModuleTestCase):
'Test document incoming xml converter module'
module = 'document_incoming_invoice_xml'
# end XmlIncomingTestCase
del ModuleTestCase

View file

@ -3,3 +3,5 @@ version=7.0.0
depends: depends:
document_incoming_invoice document_incoming_invoice
xml: xml:
message.xml
document.xml

View file

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- This file is part of the document-incoming-invoice-xml-module
from m-ds for Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/field[@name='type']" position="after">
<label name="xsd_type"/>
<field name="xsd_type"/>
</xpath>
</data>