From 764cacc091ba081fcd8e0d65667303f3d921ab36 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 5 Dec 2024 15:36:07 +0100 Subject: [PATCH] export: add factur-x 1.07.2 --- __init__.py | 3 +- edocument.py | 58 +++++-- template/Factur-X-1.07.2-extended/invoice.xml | 141 ++++++++++++++++++ tests/test_edocument.py | 46 ++++++ tests/validate_xml.py | 21 +++ 5 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 template/Factur-X-1.07.2-extended/invoice.xml create mode 100644 tests/validate_xml.py diff --git a/__init__.py b/__init__.py index fe1a4a0..cb9e647 100644 --- a/__init__.py +++ b/__init__.py @@ -4,13 +4,14 @@ # full copyright notices and license terms. from trytond.pool import Pool -from .edocument import Invoice +from .edocument import Invoice, FacturX from .party import PartyConfiguration, Party def register(): Pool.register( Invoice, + FacturX, Party, PartyConfiguration, module='edocument_xrechnung', type_='model') diff --git a/edocument.py b/edocument.py index 436d40e..b5e5a4c 100644 --- a/edocument.py +++ b/edocument.py @@ -12,6 +12,42 @@ from trytond.modules.edocument_uncefact.edocument import Invoice from decimal import Decimal +class FacturX(Invoice): + 'Factur-X' + __name__ = 'edocument.facturxext.invoice' + + def get_list_of_comments(self): + """ comment, to export in + + Returns: + _type_: _description_ + """ + result = [] + if self.invoice.comment: + result.append({ + 'content': self.invoice.comment, + 'subject_code': '', + 'content_code': ''}) + return result + + def _get_template(self, version): + """ load our own template if 'version' is ours + """ + loader = genshi.template.TemplateLoader( + os.path.join(os.path.dirname(__file__), 'template'), + auto_reload=True) + + if version == 'Factur-X-1.07.2-extended': + if self.type_code in ['380', '389', '381', '261']: + return loader.load(os.path.join(version, 'invoice.xml')) + else: + raise ValueError('invalid type-code "%s"' % self.type_code) + else: + return super(Invoice, self)._get_template(version) + +# end FacturX + + class Invoice(Invoice): 'EDocument XRechnung' __name__ = 'edocument.xrechnung.invoice' @@ -123,16 +159,20 @@ class Invoice(Invoice): def _get_template(self, version): """ load our own template if 'version' is ours """ + loader = genshi.template.TemplateLoader( + os.path.join(os.path.dirname(__file__), 'template'), + auto_reload=True) + if version in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']: - loader = genshi.template.TemplateLoader( - os.path.join(os.path.dirname(__file__), 'template'), - auto_reload=True) - if self.type_code in ['380', '389']: - return loader.load(os.path.join( - version, 'XRechnung_invoice.xml')) - elif self.type_code in ['381', '261']: - return loader.load(os.path.join( - version, 'XRechnung_credit.xml')) + file_name = { + '380': 'XRechnung_invoice.xml', + '389': 'XRechnung_invoice.xml', + '381': 'XRechnung_credit.xml', + '261': 'XRechnung_credit.xml', + }.get(self.type_code) + + if file_name: + return loader.load(os.path.join(version, file_name)) else: raise ValueError('invalid type-code "%s"' % self.type_code) else: diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml new file mode 100644 index 0000000..c10307f --- /dev/null +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -0,0 +1,141 @@ + + + + + ${value.strftime('%Y%m%d')} + + + ${party.name} + + + ${id} + + + ${TradeAddress(address)} + + ${tax_identifier.code} + + + + ${address.postal_code} + + ${lines[0]} + ${lines[1]} + ${lines[2]} + + ${address.city} + ${address.country.code} + ${address.subdivision.name} + + + + ${amount * this.type_sign} + ${tax.unece_code} + ${tax.legal_notice} + ${base * this.type_sign} + ${tax.unece_category_code} + ${tax.rate * 100} + + + + + urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended + + + + ${this.invoice.number} + ${this.invoice.description} + ${this.type_code} + + ${DateTime(this.invoice.invoice_date)} + + + + ${comment_line['content_code']} + ${comment_line['content']} + ${comment_line['subject_content']} + + + + + + + ${line_id} + + + ${line.product.code} + ${line.product.name} + ${line.description} + + + + ${this.invoice.currency.round(line.unit_price)} + + + + ${line.quantity * this.type_sign} + + + + ${TradeTax(tax.tax)} + + + ${line.amount} + + + + + + ${TradeParty(this.seller_trade_party, this.seller_trade_address, this.seller_trade_tax_identifier)} + + + ${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)} + + + ${this.invoice.reference} + + + ${this.invoice.reference} + + + + + ${TradeParty(this.ship_to_trade_party, this.ship_to_trade_address)} + + + ${TradeParty(this.ship_from_trade_party, this.ship_from_trade_address)} + + + + ${this.payment_reference} + ${this.invoice.currency.code} + + 1 + + + ${TradeTax(tax.tax, tax.amount, tax.base)} + + + ${this.invoice.payment_term.description} + + ${DateTime(line.maturity_date)} + + ${(line.amount_second_currency or (line.debit - line.credit)) * this.type_sign} + + + ${this.invoice.untaxed_amount * this.type_sign} + ${this.invoice.untaxed_amount * this.type_sign} + ${this.invoice.tax_amount * this.type_sign} + ${this.invoice.total_amount * this.type_sign} + ${this.invoice.amount_to_pay * this.type_sign} + + + + diff --git a/tests/test_edocument.py b/tests/test_edocument.py index 73b4695..eaa75df 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -46,6 +46,52 @@ class EdocTestCase(ModuleTestCase): {'identifiers': [('delete', [party.identifiers[0].id])]}] ) + @with_transaction() + def test_xrechn_export_facturx(self): + """ run export - factur-x + """ + pool = Pool() + Template = pool.get('edocument.facturxext.invoice') + Identifier = pool.get('party.identifier') + Party = pool.get('party.party') + Bank = pool.get('bank') + BankAccount = pool.get('bank.account') + BankNumber = pool.get('bank.account.number') + + invoice = get_invoice() + invoice.payment_term_date = date.today() + invoice.party.get_xrechnung_route_id = Mock( + return_value='xrechn-route-id-123') + invoice.company.party.bank_accounts = [ + Mock( + spec=BankAccount, + currency=invoice.currency, + bank=Mock(spec=Bank, party=Mock(spec=Party, name='Bank')), + owners=[invoice.company.party], + numbers=[Mock(spec=BankNumber, type='other', number='123456')], + )] + invoice.description = 'description of invoice' + invoice.comment = 'note line 1\nnote line 2' + invoice.taxes[0].tax.rate = Decimal('0.1') + invoice.identifiers = [ + Mock( + spec=Identifier, + type='edoc_route_id', + code='xrechn-route-id-123') + ] + + template = Template(invoice) + + schema_file = os.path.join( + os.path.dirname(__file__), + 'Factur-X_1.07.2_EXTENDED', + 'Factur-X_1.07.2_EXTENDED.xsd') + + invoice_string = template.render('Factur-X-1.07.2-extended') + invoice_xml = etree.fromstring(invoice_string) + schema = etree.XMLSchema(etree.parse(schema_file)) + schema.assertValid(invoice_xml) + @with_transaction() def test_xrechn_export_xml_invoice(self): """ run export - invoice diff --git a/tests/validate_xml.py b/tests/validate_xml.py new file mode 100644 index 0000000..f99603c --- /dev/null +++ b/tests/validate_xml.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# This file is part of the edocument-module for Tryton from m-ds.de. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +from lxml import etree +import os + +file_name = 'file-to-check.xml' + +schema_file = os.path.join( + os.path.dirname(__file__), + 'Factur-X_1.07.2_EXTENDED', + 'Factur-X_1.07.2_EXTENDED.xsd') + +with open(file_name, 'r') as fhdl: + f_content = fhdl.read() + f_content = f_content.encode('utf8') + invoice_xml = etree.fromstring(f_content) + schema = etree.XMLSchema(etree.parse(schema_file)) + print('Result:', schema.assertValid(invoice_xml))