From 5171cbae093fffecd63035e09017ab704bc58fe8 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 5 Dec 2024 09:47:54 +0100 Subject: [PATCH] export: add xrechnung 2.3 + 3.0 + tests against xsd --- edocument.py | 2 +- template/XRechnung-2.2/XRechnung_invoice.xml | 2 +- template/XRechnung-2.3/XRechnung_credit.xml | 182 ++++++++++++++++++ template/XRechnung-2.3/XRechnung_invoice.xml | 183 +++++++++++++++++++ template/XRechnung-3.0/XRechnung_credit.xml | 182 ++++++++++++++++++ template/XRechnung-3.0/XRechnung_invoice.xml | 183 +++++++++++++++++++ tests/test_edocument.py | 38 +++- 7 files changed, 762 insertions(+), 10 deletions(-) create mode 100644 template/XRechnung-2.3/XRechnung_credit.xml create mode 100644 template/XRechnung-2.3/XRechnung_invoice.xml create mode 100644 template/XRechnung-3.0/XRechnung_credit.xml create mode 100644 template/XRechnung-3.0/XRechnung_invoice.xml diff --git a/edocument.py b/edocument.py index 2dcfd1d..436d40e 100644 --- a/edocument.py +++ b/edocument.py @@ -123,7 +123,7 @@ class Invoice(Invoice): def _get_template(self, version): """ load our own template if 'version' is ours """ - if version == 'XRechnung-2.2': + 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) diff --git a/template/XRechnung-2.2/XRechnung_invoice.xml b/template/XRechnung-2.2/XRechnung_invoice.xml index a1de5bf..396cf24 100644 --- a/template/XRechnung-2.2/XRechnung_invoice.xml +++ b/template/XRechnung-2.2/XRechnung_invoice.xml @@ -90,7 +90,7 @@ urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.1 ${this.invoice.number} ${this.invoice.invoice_date.isoformat()} - ${(getattr(this.invoice, 'payment_term_date', None) or this.invoice.invoice_date).isoformat()} + ${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()} ${this.type_code} ${this.invoice_note()} ${this.invoice.currency.code} diff --git a/template/XRechnung-2.3/XRechnung_credit.xml b/template/XRechnung-2.3/XRechnung_credit.xml new file mode 100644 index 0000000..b487804 --- /dev/null +++ b/template/XRechnung-2.3/XRechnung_credit.xml @@ -0,0 +1,182 @@ + + + + + ${', '.join((getattr(value, 'street', None) or '').split('\n'))} + ${getattr(value, 'city', None) or ''} + ${getattr(value, 'postal_code', None) or ''} + + ${getattr(getattr(value, 'country', None), 'code', None)} + + + + + ${this.taxident_data(value)['code']} + + ${this.taxident_data(value)['id']} + + + + + ${value.name} + + ${value.phone} + + + ${value.email} + + + + + ${value.name} + ${this.taxident_data(this.seller_trade_tax_identifier)['code']} + + + + + ${value.bank_accounts[0].numbers[0].number_compact} + ${value.name} + + + + + ${this.tax_category_code(value)} + ${this.tax_rate(value)} + ${value.legal_notice or '-'} + + VAT + + + + + + ${this.negate_amount(value.base)} + ${this.negate_amount(value.amount)} + + ${TaxCategory(value.tax, True)} + + + + + + + ${value.id} + ${this.negate_amount(value.quantity)} + ${this.negate_amount(value.amount)} + + ${value.description or getattr(value.product, 'name', None)} + + ${value.product.code} + + + ${TaxCategory(this.invoice_line_tax(value))} + + + + ${value.unit_price} + + + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3 + ${this.invoice.number} + ${this.invoice.invoice_date.isoformat()} + ${this.type_code} + ${this.invoice_note()} + ${this.invoice.currency.code} + ${this.invoice.party.get_xrechnung_route_id()} + + ${this.invoice.reference} + ${this.sales_order_nums()} + + + vertrags-nr + + + proj-referenz + + + + + ${PostalAddress(this.seller_trade_address)} + + + ${PartyTaxScheme(this.seller_trade_tax_identifier)} + + + ${PartyLegalEntity(this.seller_trade_party)} + + + ${Contact(this.seller_trade_party)} + + + + + + + ${this.buyer_trade_party.code} + + + ${PostalAddress(this.buyer_trade_address)} + + + ${PartyTaxScheme(this.buyer_trade_tax_identifier)} + + + ${PartyLegalEntity(this.buyer_trade_party, False)} + + + ${Contact(this.buyer_trade_party)} + + + + + 30 + ${this.payment_reference} + + ${PayeeFinancialAccount(this.seller_trade_party)} + + + + ${this.invoice.payment_term.rec_name} + + + false + Neukundenrabatt + 5.00 + + S + 19.00 + + VAT + + + + + ${this.negate_amount(this.invoice.tax_amount)} + + ${TaxSubTotal(taxline)} + + + + ${this.negate_amount(this.invoice.untaxed_amount)} + ${this.negate_amount(this.invoice.untaxed_amount)} + ${this.negate_amount(this.invoice.total_amount)} + 0.00 + 0.00 + ${this.negate_amount(this.prepaid_amount(this.invoice))} + ${this.negate_amount(this.invoice.amount_to_pay)} + + + ${CreditNoteLine(line)} + + diff --git a/template/XRechnung-2.3/XRechnung_invoice.xml b/template/XRechnung-2.3/XRechnung_invoice.xml new file mode 100644 index 0000000..f3599a1 --- /dev/null +++ b/template/XRechnung-2.3/XRechnung_invoice.xml @@ -0,0 +1,183 @@ + + + + + ${', '.join((getattr(value, 'street', None) or '').split('\n'))} + ${getattr(value, 'city', None) or ''} + ${getattr(value, 'postal_code', None) or ''} + + ${getattr(getattr(value, 'country', None), 'code', None)} + + + + + ${this.taxident_data(value)['code']} + + ${this.taxident_data(value)['id']} + + + + + ${value.name} + + ${value.phone} + + + ${value.email} + + + + + ${value.name} + ${this.taxident_data(this.seller_trade_tax_identifier)['code']} + + + + + ${value.bank_accounts[0].numbers[0].number_compact} + ${value.name} + + + + + ${this.tax_category_code(value)} + ${this.tax_rate(value)} + ${value.legal_notice or '-'} + + VAT + + + + + + ${value.base} + ${value.amount} + + ${TaxCategory(value.tax, True)} + + + + + + + ${value.id} + ${value.quantity} + ${value.amount} + + ${value.description or getattr(value.product, 'name', None)} + + ${value.product.code} + + + ${TaxCategory(this.invoice_line_tax(value))} + + + + ${value.unit_price} + + + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3 + ${this.invoice.number} + ${this.invoice.invoice_date.isoformat()} + ${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()} + ${this.type_code} + ${this.invoice_note()} + ${this.invoice.currency.code} + ${this.invoice.party.get_xrechnung_route_id()} + + ${this.invoice.reference} + ${this.sales_order_nums()} + + + vertrags-nr + + + proj-referenz + + + + + ${PostalAddress(this.seller_trade_address)} + + + ${PartyTaxScheme(this.seller_trade_tax_identifier)} + + + ${PartyLegalEntity(this.seller_trade_party)} + + + ${Contact(this.seller_trade_party)} + + + + + + + ${this.buyer_trade_party.code} + + + ${PostalAddress(this.buyer_trade_address)} + + + ${PartyTaxScheme(this.buyer_trade_tax_identifier)} + + + ${PartyLegalEntity(this.buyer_trade_party, False)} + + + ${Contact(this.buyer_trade_party)} + + + + + 30 + ${this.payment_reference} + + ${PayeeFinancialAccount(this.seller_trade_party)} + + + + ${this.invoice.payment_term.rec_name} + + + false + Neukundenrabatt + 5.00 + + S + 19.00 + + VAT + + + + + ${this.invoice.tax_amount} + + ${TaxSubTotal(taxline)} + + + + ${this.invoice.untaxed_amount} + ${this.invoice.untaxed_amount} + ${this.invoice.total_amount} + 0.00 + 0.00 + ${this.prepaid_amount(this.invoice)} + ${this.invoice.amount_to_pay} + + + ${InvoiceLine(line)} + + diff --git a/template/XRechnung-3.0/XRechnung_credit.xml b/template/XRechnung-3.0/XRechnung_credit.xml new file mode 100644 index 0000000..6c3db35 --- /dev/null +++ b/template/XRechnung-3.0/XRechnung_credit.xml @@ -0,0 +1,182 @@ + + + + + ${', '.join((getattr(value, 'street', None) or '').split('\n'))} + ${getattr(value, 'city', None) or ''} + ${getattr(value, 'postal_code', None) or ''} + + ${getattr(getattr(value, 'country', None), 'code', None)} + + + + + ${this.taxident_data(value)['code']} + + ${this.taxident_data(value)['id']} + + + + + ${value.name} + + ${value.phone} + + + ${value.email} + + + + + ${value.name} + ${this.taxident_data(this.seller_trade_tax_identifier)['code']} + + + + + ${value.bank_accounts[0].numbers[0].number_compact} + ${value.name} + + + + + ${this.tax_category_code(value)} + ${this.tax_rate(value)} + ${value.legal_notice or '-'} + + VAT + + + + + + ${this.negate_amount(value.base)} + ${this.negate_amount(value.amount)} + + ${TaxCategory(value.tax, True)} + + + + + + + ${value.id} + ${this.negate_amount(value.quantity)} + ${this.negate_amount(value.amount)} + + ${value.description or getattr(value.product, 'name', None)} + + ${value.product.code} + + + ${TaxCategory(this.invoice_line_tax(value))} + + + + ${value.unit_price} + + + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0 + ${this.invoice.number} + ${this.invoice.invoice_date.isoformat()} + ${this.type_code} + ${this.invoice_note()} + ${this.invoice.currency.code} + ${this.invoice.party.get_xrechnung_route_id()} + + ${this.invoice.reference} + ${this.sales_order_nums()} + + + vertrags-nr + + + proj-referenz + + + + + ${PostalAddress(this.seller_trade_address)} + + + ${PartyTaxScheme(this.seller_trade_tax_identifier)} + + + ${PartyLegalEntity(this.seller_trade_party)} + + + ${Contact(this.seller_trade_party)} + + + + + + + ${this.buyer_trade_party.code} + + + ${PostalAddress(this.buyer_trade_address)} + + + ${PartyTaxScheme(this.buyer_trade_tax_identifier)} + + + ${PartyLegalEntity(this.buyer_trade_party, False)} + + + ${Contact(this.buyer_trade_party)} + + + + + 30 + ${this.payment_reference} + + ${PayeeFinancialAccount(this.seller_trade_party)} + + + + ${this.invoice.payment_term.rec_name} + + + false + Neukundenrabatt + 5.00 + + S + 19.00 + + VAT + + + + + ${this.negate_amount(this.invoice.tax_amount)} + + ${TaxSubTotal(taxline)} + + + + ${this.negate_amount(this.invoice.untaxed_amount)} + ${this.negate_amount(this.invoice.untaxed_amount)} + ${this.negate_amount(this.invoice.total_amount)} + 0.00 + 0.00 + ${this.negate_amount(this.prepaid_amount(this.invoice))} + ${this.negate_amount(this.invoice.amount_to_pay)} + + + ${CreditNoteLine(line)} + + diff --git a/template/XRechnung-3.0/XRechnung_invoice.xml b/template/XRechnung-3.0/XRechnung_invoice.xml new file mode 100644 index 0000000..c25d170 --- /dev/null +++ b/template/XRechnung-3.0/XRechnung_invoice.xml @@ -0,0 +1,183 @@ + + + + + ${', '.join((getattr(value, 'street', None) or '').split('\n'))} + ${getattr(value, 'city', None) or ''} + ${getattr(value, 'postal_code', None) or ''} + + ${getattr(getattr(value, 'country', None), 'code', None)} + + + + + ${this.taxident_data(value)['code']} + + ${this.taxident_data(value)['id']} + + + + + ${value.name} + + ${value.phone} + + + ${value.email} + + + + + ${value.name} + ${this.taxident_data(this.seller_trade_tax_identifier)['code']} + + + + + ${value.bank_accounts[0].numbers[0].number_compact} + ${value.name} + + + + + ${this.tax_category_code(value)} + ${this.tax_rate(value)} + ${value.legal_notice or '-'} + + VAT + + + + + + ${value.base} + ${value.amount} + + ${TaxCategory(value.tax, True)} + + + + + + + ${value.id} + ${value.quantity} + ${value.amount} + + ${value.description or getattr(value.product, 'name', None)} + + ${value.product.code} + + + ${TaxCategory(this.invoice_line_tax(value))} + + + + ${value.unit_price} + + + + + urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0 + ${this.invoice.number} + ${this.invoice.invoice_date.isoformat()} + ${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()} + ${this.type_code} + ${this.invoice_note()} + ${this.invoice.currency.code} + ${this.invoice.party.get_xrechnung_route_id()} + + ${this.invoice.reference} + ${this.sales_order_nums()} + + + vertrags-nr + + + proj-referenz + + + + + ${PostalAddress(this.seller_trade_address)} + + + ${PartyTaxScheme(this.seller_trade_tax_identifier)} + + + ${PartyLegalEntity(this.seller_trade_party)} + + + ${Contact(this.seller_trade_party)} + + + + + + + ${this.buyer_trade_party.code} + + + ${PostalAddress(this.buyer_trade_address)} + + + ${PartyTaxScheme(this.buyer_trade_tax_identifier)} + + + ${PartyLegalEntity(this.buyer_trade_party, False)} + + + ${Contact(this.buyer_trade_party)} + + + + + 30 + ${this.payment_reference} + + ${PayeeFinancialAccount(this.seller_trade_party)} + + + + ${this.invoice.payment_term.rec_name} + + + false + Neukundenrabatt + 5.00 + + S + 19.00 + + VAT + + + + + ${this.invoice.tax_amount} + + ${TaxSubTotal(taxline)} + + + + ${this.invoice.untaxed_amount} + ${this.invoice.untaxed_amount} + ${this.invoice.total_amount} + 0.00 + 0.00 + ${this.prepaid_amount(this.invoice)} + ${this.invoice.amount_to_pay} + + + ${InvoiceLine(line)} + + diff --git a/tests/test_edocument.py b/tests/test_edocument.py index 2b223e4..02648aa 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -3,11 +3,14 @@ # The COPYRIGHT file at the top level of this repository contains the # full copyright notices and license terms. +from lxml import etree +import os +from unittest.mock import Mock +from decimal import Decimal +from datetime import date from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.pool import Pool from trytond.modules.edocument_uncefact.tests.test_module import get_invoice -from unittest.mock import Mock -from decimal import Decimal class EdocTestCase(ModuleTestCase): @@ -27,6 +30,7 @@ class EdocTestCase(ModuleTestCase): 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 = [ @@ -48,9 +52,16 @@ class EdocTestCase(ModuleTestCase): ] template = Template(invoice) - invoice_string = template.render('XRechnung-2.2') - with open('xrechnung-test-invoice.xml', 'wt') as fhdl: - fhdl.write(invoice_string.decode('utf8')) + + schema_file = os.path.join( + os.path.dirname(__file__), 'os-UBL-2.1', + 'xsd', 'maindoc', 'UBL-Invoice-2.1.xsd') + + for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']: + invoice_string = template.render(x) + invoice_xml = etree.fromstring(invoice_string) + schema = etree.XMLSchema(etree.parse(schema_file)) + schema.assertValid(invoice_xml) @with_transaction() def test_xrechn_export_xml_creditnote(self): @@ -97,9 +108,20 @@ class EdocTestCase(ModuleTestCase): ] template = Template(invoice) - invoice_string = template.render('XRechnung-2.2') - with open('xrechnung-test-creditnote.xml', 'wt') as fhdl: - fhdl.write(invoice_string.decode('utf8')) + + schema_file = os.path.join( + os.path.dirname(__file__), 'os-UBL-2.1', + 'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd') + + for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']: + invoice_string = template.render(x) + invoice_xml = etree.fromstring(invoice_string) + schema = etree.XMLSchema(etree.parse(schema_file)) + schema.assertValid(invoice_xml) + + # invoice_string = template.render('XRechnung-2.2') + # with open('xrechnung-test-creditnote.xml', 'wt') as fhdl: + # fhdl.write(invoice_string.decode('utf8')) # end EdocTestCase