From 79c2227131241397e9df9db266741cc32e605dbe Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Thu, 12 Dec 2024 09:45:12 +0100 Subject: [PATCH 01/20] Remove arguments in super() calls. They are no more needed in Python3 and usually result in unexpected behavior when wrongly used (like e.g. the one introduced in 764cacc091ba081fcd8e0d65667303f3d921ab36 and solved meanwhile by refactorization). --- edocument.py | 4 ++-- mixin.py | 4 ++-- party.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/edocument.py b/edocument.py index d196140..395df5d 100644 --- a/edocument.py +++ b/edocument.py @@ -70,7 +70,7 @@ class XRechnung(EdocumentMixin, Invoice): else: raise ValueError('invalid type-code "%s"' % self.type_code) else: - return super(XRechnung, self)._get_template(version) + return super()._get_template(version) # end XRechnung @@ -92,6 +92,6 @@ class FacturX(EdocumentMixin, Invoice): else: raise ValueError('invalid type-code "%s"' % self.type_code) else: - return super(FacturX, self)._get_template(version) + return super()._get_template(version) # end FacturX diff --git a/mixin.py b/mixin.py index abd7a20..16482e8 100644 --- a/mixin.py +++ b/mixin.py @@ -47,7 +47,7 @@ class EdocumentMixin(object): Returns: record : model party.address """ - result = super(EdocumentMixin, self).seller_trade_address + result = super().seller_trade_address if not result: raise UserError(gettext( 'edocument_xrechnung.msg_no_seller_address', @@ -75,7 +75,7 @@ class EdocumentMixin(object): if self.invoice and self.invoice.party else '-')) - result = super(EdocumentMixin, self).buyer_trade_address + result = super().buyer_trade_address if result and not result.country: raise UserError(gettext( 'edocument_xrechnung.msg_no_address_country', diff --git a/party.py b/party.py index cc020c6..1f09a66 100644 --- a/party.py +++ b/party.py @@ -45,7 +45,7 @@ class Party(metaclass=PoolMeta): Args: records (list): records of party.party """ - super(Party, cls).validate(records) + super().validate(records) for record in records: record.get_xrechnung_route_id() @@ -57,7 +57,7 @@ class PartyConfiguration(metaclass=PoolMeta): @classmethod def __setup__(cls): - super(PartyConfiguration, cls).__setup__() + super().__setup__() cls.identifier_types.selection.append( ('edoc_route_id', 'X-Rechnung Route-ID')) From d6b5893a8bfe47b16097ab4a4fceaf25d0a9c3ee Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 12 Dec 2024 11:52:50 +0100 Subject: [PATCH 02/20] add folder 'view' to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 04b912a..efa68a9 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ setup( info.get('xml', []) + ['tryton.cfg', 'locale/*.po', 'tests/*.py', 'template/*/*.xml', 'versiondep.txt', 'README.rst', - 'tests/*/*/*/*.xsd', + 'tests/*/*/*/*.xsd', 'view/*.xml', 'tests/*/*.xsd', 'tests/*/*.sch', 'tests/*/*.xml', 'tests/*/*/*.xslt', 'tests/*/*/*.xml']), }, From 417726571322ea47334b223efbfb5549fb327382 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 12 Dec 2024 11:56:33 +0100 Subject: [PATCH 03/20] Version 7.0.10 --- COPYRIGHT | 3 ++- README.rst | 5 +++++ tryton.cfg | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/COPYRIGHT b/COPYRIGHT index d99fbe4..0fbd54d 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,6 +1,7 @@ Copyright (C) 2015-2023 Cédric Krier. Copyright (C) 2015-2023 B2CK SPRL. -Copyright (C) 2021-2023 martin-data services. +Copyright (C) 2021-2024 martin-data services. +Copyright (C) 2024 Mathias Behrle This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/README.rst b/README.rst index affb395..9eabda9 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,11 @@ Requires Changes ======= +*7.0.10 - 12.12.2024* + +- fix missing views +- Remove arguments in super() calls. (Mathias Behrle) + *7.0.9 - 11.12.2024* - fix name of party in exceptions diff --git a/tryton.cfg b/tryton.cfg index 88331ef..509c618 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=7.0.9 +version=7.0.10 depends: edocument_uncefact party From 7ddb94f47ea7a2a89406a31795fdd92c09296d0c Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 19 Dec 2024 11:34:36 +0100 Subject: [PATCH 04/20] add docs --- docs/xrechnung.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/xrechnung.txt b/docs/xrechnung.txt index 3cf8cff..f8fbbbd 100644 --- a/docs/xrechnung.txt +++ b/docs/xrechnung.txt @@ -9,3 +9,6 @@ validator: überweisungsdaten https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/zugferd%202.0.1%20-%20facturx%201.03/en%2016931%20%E2%80%93%20facturx%201.03%20%E2%80%93%20zugferd%202.0.1%20-%20basic.scm/html/de/021.htm?https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/zugferd%202.0.1%20-%20facturx%201.03/en%2016931%20%E2%80%93%20facturx%201.03%20%E2%80%93%20zugferd%202.0.1%20-%20basic.scm/html/de/02134.htm + +https://erechnungsvalidator.service-bw.de/ +https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/ From f3b4849e0cd373bc29068277a8b601c931a7406e Mon Sep 17 00:00:00 2001 From: Jan Grasnick Date: Fri, 3 Jan 2025 22:28:58 +0100 Subject: [PATCH 05/20] handle tax childs --- mixin.py | 20 ++++++++++++++----- template/Factur-X-1.07.2-extended/invoice.xml | 4 +--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/mixin.py b/mixin.py index b5eed85..2879910 100644 --- a/mixin.py +++ b/mixin.py @@ -5,6 +5,7 @@ from decimal import Decimal +import datetime import html from trytond.exceptions import UserError from trytond.i18n import gettext @@ -100,21 +101,30 @@ class EdocumentMixin(object): """ get tax of invoice-line, fire exception if no/multiple taxes exists """ - if len(line.invoice_taxes) != 1: + if len(line.taxes) != 1: raise UserError(gettext( 'edocument_xrechnung.msg_linetax_invalid_number', linename=line.rec_name, - numtax=len(line.invoice_taxes))) + numtax=len(line.taxes))) + + tax = line.taxes[0] + date = line.invoice.accounting_date or line.invoice.invoice_date + for child in tax.childs: + start_date = tax.start_date or datetime.date.min + end_date = tax.end_date or datetime.date.max + if start_date <= date <= end_date: + tax = child + break allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B'] - unece_category_code = self.get_category_code(line.invoice_taxes[0].tax) + unece_category_code = self.get_category_code(tax) if unece_category_code not in allowed_cat: raise UserError(gettext( 'edocument_xrechnung.msg_linetax_invalid_catcode', - taxname=line.invoice_taxes[0].tax.rec_name, + taxname=tax.rec_name, allowed=', '.join(allowed_cat))) - return line.invoice_taxes[0].tax + return tax def taxident_data(self, tax_identifier): """ get tax-scheme-id and codes diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index 8a7a664..09b8e4d 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -83,9 +83,7 @@ this repository contains the full copyright notices and license terms. --> ${line.quantity * this.type_sign} - - ${TradeTax(tax.tax)} - + ${TradeTax(this.invoice_line_tax(line))} ${line.amount} From dba1965894f401ca125daf809173124641491fbc Mon Sep 17 00:00:00 2001 From: Jan Grasnick Date: Sat, 4 Jan 2025 21:08:43 +0100 Subject: [PATCH 06/20] fix typo --- mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixin.py b/mixin.py index 2879910..e8762fe 100644 --- a/mixin.py +++ b/mixin.py @@ -110,8 +110,8 @@ class EdocumentMixin(object): tax = line.taxes[0] date = line.invoice.accounting_date or line.invoice.invoice_date for child in tax.childs: - start_date = tax.start_date or datetime.date.min - end_date = tax.end_date or datetime.date.max + start_date = child.start_date or datetime.date.min + end_date = child.end_date or datetime.date.max if start_date <= date <= end_date: tax = child break From 9a13bc325d535b43ae9e9b3a24effc72fef1c831 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 9 Jan 2025 11:50:00 +0100 Subject: [PATCH 07/20] calculation of the tax adjusted --- mixin.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/mixin.py b/mixin.py index e8762fe..8a75aca 100644 --- a/mixin.py +++ b/mixin.py @@ -5,7 +5,6 @@ from decimal import Decimal -import datetime import html from trytond.exceptions import UserError from trytond.i18n import gettext @@ -101,20 +100,19 @@ class EdocumentMixin(object): """ get tax of invoice-line, fire exception if no/multiple taxes exists """ + Tax = Pool().get('account.tax') + if len(line.taxes) != 1: raise UserError(gettext( 'edocument_xrechnung.msg_linetax_invalid_number', linename=line.rec_name, numtax=len(line.taxes))) - tax = line.taxes[0] - date = line.invoice.accounting_date or line.invoice.invoice_date - for child in tax.childs: - start_date = child.start_date or datetime.date.min - end_date = child.end_date or datetime.date.max - if start_date <= date <= end_date: - tax = child - break + taxlines = Tax.compute( + line.taxes, Decimal('1'), 1.0, + line.invoice.accounting_date or line.invoice.invoice_date) + assert len(taxlines) == 1 + tax = taxlines[0]['tax'] allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B'] unece_category_code = self.get_category_code(tax) From e47380f93049594f75f458a1a579a8b5c33b05d8 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 9 Jan 2025 11:50:09 +0100 Subject: [PATCH 08/20] formatting --- template/Factur-X-1.07.2-extended/invoice.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index 09b8e4d..f23354e 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -83,7 +83,7 @@ this repository contains the full copyright notices and license terms. --> ${line.quantity * this.type_sign} - ${TradeTax(this.invoice_line_tax(line))} + ${TradeTax(this.invoice_line_tax(line))} ${line.amount} From ceb2a72fedeadbf517e3ee803e7a803ed3ceec1c Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 9 Jan 2025 11:50:52 +0100 Subject: [PATCH 09/20] tests: drop usage of Mock, generate invoice in db --- tests/test_edocument.py | 304 ++++++++++++++++++++++++---------------- 1 file changed, 183 insertions(+), 121 deletions(-) diff --git a/tests/test_edocument.py b/tests/test_edocument.py index 4ff38b7..5ba67ac 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -5,20 +5,159 @@ 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 trytond.modules.company.tests import create_company, set_company +from trytond.modules.account.tests import create_chart, get_fiscalyear from trytond.exceptions import UserError +def set_invoice_sequences(fiscalyear): + pool = Pool() + Sequence = pool.get('ir.sequence.strict') + SequenceType = pool.get('ir.sequence.type') + InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence') + ModelData = pool.get('ir.model.data') + + sequence = Sequence( + name=fiscalyear.name, + sequence_type=SequenceType(ModelData.get_id( + 'account_invoice', 'sequence_type_account_invoice')), + company=fiscalyear.company) + sequence.save() + fiscalyear.invoice_sequences = [] + invoice_sequence = InvoiceSequence() + invoice_sequence.fiscalyear = fiscalyear + invoice_sequence.in_invoice_sequence = sequence + invoice_sequence.in_credit_note_sequence = sequence + invoice_sequence.out_invoice_sequence = sequence + invoice_sequence.out_credit_note_sequence = sequence + invoice_sequence.save() + return fiscalyear + + class EdocTestCase(ModuleTestCase): 'Test e-rechnung module' module = 'edocument_xrechnung' + def prep_fiscalyear(self, company1): + """ prepare fiscal year, sequences... + """ + pool = Pool() + FiscalYear = pool.get('account.fiscalyear') + + fisc_year = get_fiscalyear(company1, today=date(2024, 1, 15)) + set_invoice_sequences(fisc_year) + self.assertEqual(len(fisc_year.invoice_sequences), 1) + FiscalYear.create_period([fisc_year]) + + def prep_company(self): + """ create company, add country and bank-account + """ + pool = Pool() + Country = pool.get('country.country') + Party = pool.get('party.party') + Bank = pool.get('bank') + BankAccount = pool.get('bank.account') + + country_de, = Country.create([{ + 'name': 'Germany', + 'code': 'DE', + 'code3': 'DEU'}]) + + company = create_company('m-ds') + Party.write(*[[company.party], { + 'addresses': [('write', [company.party.addresses[0]], { + 'country': country_de.id})]}]) + + bank_party, = Party.create([{ + 'name': 'Bank 123', + 'addresses': [('create', [{}])]}]) + bank, = Bank.create([{'party': bank_party.id}]) + BankAccount.create([{ + 'bank': bank.id, + 'owners': [('add', [company.party.id])], + 'numbers': [('create', [{ + 'type': 'iban', + 'number': 'DE02300209000106531065'}])]}]) + return company + + def prep_invoice(self, credit_note=False): + """ add invoice + """ + pool = Pool() + Invoice = pool.get('account.invoice') + Taxes = pool.get('account.tax') + Account = pool.get('account.account') + Journal = pool.get('account.journal') + Currency = pool.get('currency.currency') + Uom = pool.get('product.uom') + Country = pool.get('country.country') + Party = pool.get('party.party') + + country_de, = Country.search([('code', '=', 'DE')]) + customer, = Party.create([{ + 'name': 'Customer', + 'identifiers': [('create', [{ + 'type': 'edoc_route_id', 'code': 'xrechn-route-id-123'}])], + 'addresses': [('create', [{ + 'invoice': True, + 'street': 'Customer Street 1', + 'postal_code': '12345', + 'city': 'Usertown', + 'country': country_de.id, + }])], + }]) + + currency1, = Currency.search([('code', '=', 'usd')]) + + tax, = Taxes.search([('name', '=', '20% VAT')]) + Taxes.write(*[ + [tax], + {'unece_code': 'GST', 'unece_category_code': 'S', + 'legal_notice': 'Legal Notice'}]) + + account_lst = Account.search([ + ('name', 'in', ['Main Revenue', 'Main Receivable']) + ], order=[('name', 'ASC')]) + self.assertEqual(len(account_lst), 2) + self.assertEqual(account_lst[0].name, 'Main Receivable') + + journ_lst = Journal.search([('name', '=', 'Revenue')]) + self.assertEqual(len(journ_lst), 1) + + to_create_invoice = [{ + 'type': 'out', + 'description': 'description of invoice', + 'comment': 'note line 1\nnote line 2', + 'invoice_date': date(2024, 7, 1), + 'party': customer.id, + 'invoice_address': customer.addresses[0].id, + 'account': account_lst[0].id, + 'journal': journ_lst[0].id, + 'currency': currency1.id, + 'lines': [('create', [{ + 'type': 'line', + 'quantity': 2.0 if not credit_note else -2.0, + 'description': 'Product 1', + 'unit': Uom.search([('symbol', '=', 'u')])[0].id, + 'unit_price': Decimal('50.0'), + 'taxes': [('add', [tax.id])], + 'account': account_lst[1].id, + 'currency': currency1.id, + }])], + }] + inv_lst, = Invoice.create(to_create_invoice) + inv_lst.on_change_lines() + inv_lst.save() + Invoice.validate_invoice([inv_lst]) + Invoice.post([inv_lst]) + self.assertEqual(inv_lst.currency.code, 'usd') + self.assertEqual(len(inv_lst.move.lines), 3) + return inv_lst + @with_transaction() def test_xrechn_bank_account_owned(self): """ check field 'company_owned' on bank.account.number @@ -100,45 +239,24 @@ class EdocTestCase(ModuleTestCase): """ 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') - ] + company = self.prep_company() + with set_company(company): + create_chart(company=company, tax=True) + self.prep_fiscalyear(company) + invoice = self.prep_invoice() - template = Template(invoice) + 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') + 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) + 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): @@ -146,45 +264,24 @@ class EdocTestCase(ModuleTestCase): """ pool = Pool() Template = pool.get('edocument.xrechnung.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') - ] + company = self.prep_company() + with set_company(company): + create_chart(company=company, tax=True) + self.prep_fiscalyear(company) + invoice = self.prep_invoice() - template = Template(invoice) + template = Template(invoice) - schema_file = os.path.join( - os.path.dirname(__file__), 'os-UBL-2.1', - 'xsd', 'maindoc', 'UBL-Invoice-2.1.xsd') + 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) + 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): @@ -192,59 +289,24 @@ class EdocTestCase(ModuleTestCase): """ pool = Pool() Template = pool.get('edocument.xrechnung.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() + company = self.prep_company() + with set_company(company): + create_chart(company=company, tax=True) + self.prep_fiscalyear(company) + invoice = self.prep_invoice(credit_note=True) - # credit note - invoice.lines[0].quantity = -1 - invoice.lines[0].amount = Decimal('-100.0') - invoice.taxes[0].base = Decimal('-100.0') - invoice.taxes[0].amount = Decimal('-10.0') - invoice.untaxed_amount = Decimal('-100.0') - invoice.tax_amount = Decimal('-10.0') - invoice.total_amount = Decimal('-110.0') - invoice.lines_to_pay[0].debit = Decimal('-110.0') + template = Template(invoice) - 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') - ] + schema_file = os.path.join( + os.path.dirname(__file__), 'os-UBL-2.1', + 'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd') - template = Template(invoice) - - 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')) + 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) # end EdocTestCase From 02221601e7487385682f9535eb8c9e60d8ce8638 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 28 Jan 2025 12:50:55 +0100 Subject: [PATCH 10/20] round unit_price of invoice-line by price_digits --- mixin.py | 18 +++++++++++++++++- template/Factur-X-1.07.2-extended/invoice.xml | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mixin.py b/mixin.py index 8a75aca..9550384 100644 --- a/mixin.py +++ b/mixin.py @@ -4,12 +4,13 @@ # full copyright notices and license terms. -from decimal import Decimal +from decimal import Decimal, ROUND_HALF_EVEN import html from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.tools import cached_property from trytond.pool import Pool +from trytond.modules.product import price_digits class EdocumentMixin(object): @@ -183,6 +184,21 @@ class EdocumentMixin(object): taxname=tax.rec_name)) return unece_category_code + def round_unitprice(self, value): + """ round value by digits in unit_price of account.invoice.line + + Args: + value (Decimal): unit-price + + Returns: + Decimal: rounded value + """ + if isinstance(value, Decimal): + return value.quantize( + Decimal(str(1/10 ** price_digits[1])), + ROUND_HALF_EVEN) + return value + def quote_text(self, text): """ replace critical chars """ diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index f23354e..db85eb1 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -76,7 +76,7 @@ this repository contains the full copyright notices and license terms. --> - ${this.invoice.currency.round(line.unit_price)} + ${this.round_unitprice(line.unit_price)} From ea5a83d7c1ff41373b99d60db095d37371f4bba0 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 29 Jan 2025 09:33:40 +0100 Subject: [PATCH 11/20] use product.round_price() to round unit_price of invoice-line --- mixin.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mixin.py b/mixin.py index 9550384..c854b7d 100644 --- a/mixin.py +++ b/mixin.py @@ -4,13 +4,13 @@ # full copyright notices and license terms. -from decimal import Decimal, ROUND_HALF_EVEN +from decimal import Decimal import html from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.tools import cached_property from trytond.pool import Pool -from trytond.modules.product import price_digits +from trytond.modules.product import round_price class EdocumentMixin(object): @@ -193,10 +193,8 @@ class EdocumentMixin(object): Returns: Decimal: rounded value """ - if isinstance(value, Decimal): - return value.quantize( - Decimal(str(1/10 ** price_digits[1])), - ROUND_HALF_EVEN) + if value is not None: + return round_price(value) return value def quote_text(self, text): From a5bf930e55c2dbb276b9f626db34436cfa04bbb0 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Fri, 2 May 2025 14:16:47 +0200 Subject: [PATCH 12/20] update license --- COPYRIGHT | 6 ++---- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/COPYRIGHT b/COPYRIGHT index 0fbd54d..27458f9 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,7 +1,5 @@ -Copyright (C) 2015-2023 Cédric Krier. -Copyright (C) 2015-2023 B2CK SPRL. -Copyright (C) 2021-2024 martin-data services. -Copyright (C) 2024 Mathias Behrle +Copyright (C) 2021-2025 martin-data services. +Copyright (C) 2024-2025 Mathias Behrle This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/setup.py b/setup.py index efa68a9..c82d777 100644 --- a/setup.py +++ b/setup.py @@ -13,11 +13,9 @@ here = path.abspath(path.dirname(__file__)) MODULE = 'edocument_xrechnung' PREFIX = 'mds' -# Get the long description from the README file with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() -# tryton.cfg einlesen config = ConfigParser() config.readfp(open('tryton.cfg')) info = dict(config.items('tryton')) @@ -87,6 +85,8 @@ setup( 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], keywords='tryton xrechnung edcoument', From 214cbb086f0ed275249a214b625eaa33d2c46fdd Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Jun 2025 13:45:07 +0200 Subject: [PATCH 13/20] get amount on line as net-value (even if invoice is gross-mode) --- mixin.py | 36 ++++++++++++++ template/Factur-X-1.07.2-extended/invoice.xml | 2 +- tests/test_edocument.py | 47 ++++++++++++++++++- tryton.cfg | 3 ++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/mixin.py b/mixin.py index c854b7d..f7cda1c 100644 --- a/mixin.py +++ b/mixin.py @@ -10,6 +10,7 @@ from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.tools import cached_property from trytond.pool import Pool +from trytond.transaction import Transaction from trytond.modules.product import round_price @@ -160,6 +161,41 @@ class EdocumentMixin(object): taxname=tax.rec_name)) return tax.unece_code + def get_line_amount(self, line): + """ get amount of current invoice-line, + depends on modegross of invoice, set used-modegross to 'net' + + Args: + line (record): model account.invoice.line + """ + if line.modegross == 'net': + return line.amount + elif line.modegross == 'gross': + # get net-amount + # copy from account_invoice/invoice.py:2416-2434 + currency = ( + line.invoice.currency + if line.invoice else line.currency) + + amount = (Decimal(str(line.quantity or 0)) * ( + line.unit_price or Decimal(0))) + invoice_type = ( + line.invoice.type + if line.invoice else line.invoice_type) + + if (invoice_type == 'in' + and line.taxes_deductible_rate is not None + and line.taxes_deductible_rate != 1): + with Transaction().set_context(_deductible_rate=1): + tax_amount = sum( + t['amount'] for t in line._get_taxes().values()) + non_deductible_amount = ( + tax_amount * (1 - line.taxes_deductible_rate)) + amount += non_deductible_amount + if currency: + return currency.round(amount) + return amount + def get_tax_unece_code(self, tax): while tax: if tax.unece_code: diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index db85eb1..bad23a9 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -85,7 +85,7 @@ this repository contains the full copyright notices and license terms. --> ${TradeTax(this.invoice_line_tax(line))} - ${line.amount} + ${this.get_line_amount(line)} diff --git a/tests/test_edocument.py b/tests/test_edocument.py index 5ba67ac..02669f9 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -7,7 +7,8 @@ from lxml import etree import os from decimal import Decimal from datetime import date -from trytond.tests.test_tryton import ModuleTestCase, with_transaction +from trytond.tests.test_tryton import ( + ModuleTestCase, with_transaction, activate_module) from trytond.pool import Pool from trytond.modules.company.tests import create_company, set_company from trytond.modules.account.tests import create_chart, get_fiscalyear @@ -42,6 +43,14 @@ class EdocTestCase(ModuleTestCase): 'Test e-rechnung module' module = 'edocument_xrechnung' + @classmethod + def setUpClass(cls): + super().setUpClass() + activate_module([ + 'edocument_uncefact', 'party', 'bank', + 'account_invoice', 'sale_point_invoice', + 'product_grossprice'], 'en') + def prep_fiscalyear(self, company1): """ prepare fiscal year, sequences... """ @@ -84,7 +93,7 @@ class EdocTestCase(ModuleTestCase): 'number': 'DE02300209000106531065'}])]}]) return company - def prep_invoice(self, credit_note=False): + def prep_invoice(self, credit_note=False, modegross='net'): """ add invoice """ pool = Pool() @@ -130,6 +139,7 @@ class EdocTestCase(ModuleTestCase): to_create_invoice = [{ 'type': 'out', + 'modegross': modegross, 'description': 'description of invoice', 'comment': 'note line 1\nnote line 2', 'invoice_date': date(2024, 7, 1), @@ -149,6 +159,11 @@ class EdocTestCase(ModuleTestCase): 'currency': currency1.id, }])], }] + + if modegross == 'gross': + to_create_invoice[0]['lines'][0][1][0]['unit_gross_price'] = ( + Decimal('50.0') * Decimal('1.2')) + inv_lst, = Invoice.create(to_create_invoice) inv_lst.on_change_lines() inv_lst.save() @@ -233,6 +248,34 @@ class EdocTestCase(ModuleTestCase): {'identifiers': [('delete', [party.identifiers[0].id])]}] ) + @with_transaction() + def test_xrechn_export_facturx_gross(self): + """ run export - factur-x, modegross='gross' + """ + pool = Pool() + Template = pool.get('edocument.facturxext.invoice') + + company = self.prep_company() + with set_company(company): + create_chart(company=company, tax=True) + self.prep_fiscalyear(company) + invoice = self.prep_invoice(modegross='gross') + + 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') + with open('gross_invoice_string.xml', 'wb') as fhdl: + fhdl.write(invoice_string) + + invoice_xml = etree.fromstring(invoice_string) + schema = etree.XMLSchema(etree.parse(schema_file)) + schema.assertValid(invoice_xml) + @with_transaction() def test_xrechn_export_facturx(self): """ run export - factur-x diff --git a/tryton.cfg b/tryton.cfg index 509c618..ecd8916 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -5,6 +5,9 @@ depends: party bank account_invoice +extras_depend: + sale_point_invoice + product_grossprice xml: message.xml configuration.xml From d065f74482f218151452e8593897c987210ab327 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 11:46:53 +0200 Subject: [PATCH 14/20] add legal id of seller --- mixin.py | 26 +++++++++++++++++++ tests/test_edocument.py | 56 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/mixin.py b/mixin.py index f7cda1c..1ffdd6f 100644 --- a/mixin.py +++ b/mixin.py @@ -239,4 +239,30 @@ class EdocumentMixin(object): if text: return html.escape(text) + def _party_legal_types(self): + """ get list of identifier-types to be used as + legal-ids + """ + return ['de_handelsregisternummer'] + + def party_legal_ids(self, party, address): + """ get list of legal-ids of party + + Args: + party (record): model party.party + address (record): model party.address + """ + result = super().party_legal_ids(party, address) + + legal_types = self._party_legal_types() + if party and party.identifiers: + for x in party.identifiers: + if x.type in legal_types: + if x.address: + if x.address == address: + result.append((x.rec_name, {'schemeID': '0002'})) + else: + result.append((x.rec_name, {'schemeID': '0002'})) + return result + # end EdocumentMixin diff --git a/tests/test_edocument.py b/tests/test_edocument.py index 02669f9..a7c9355 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -78,6 +78,11 @@ class EdocTestCase(ModuleTestCase): company = create_company('m-ds') Party.write(*[[company.party], { + 'identifiers': [('create', [ + # post.de + {'type': 'de_handelsregisternummer', 'code': 'Bonn HRB 6792'}, + {'type': 'de_vat', 'code': 'DE 169838187'}, + ])], 'addresses': [('write', [company.party.addresses[0]], { 'country': country_de.id})]}]) @@ -121,6 +126,7 @@ class EdocTestCase(ModuleTestCase): }]) currency1, = Currency.search([('code', '=', 'usd')]) + Currency.write(*[[currency1], {'code': 'USD'}]) tax, = Taxes.search([('name', '=', '20% VAT')]) Taxes.write(*[ @@ -169,7 +175,7 @@ class EdocTestCase(ModuleTestCase): inv_lst.save() Invoice.validate_invoice([inv_lst]) Invoice.post([inv_lst]) - self.assertEqual(inv_lst.currency.code, 'usd') + self.assertEqual(inv_lst.currency.code, 'USD') self.assertEqual(len(inv_lst.move.lines), 3) return inv_lst @@ -273,9 +279,44 @@ class EdocTestCase(ModuleTestCase): fhdl.write(invoice_string) invoice_xml = etree.fromstring(invoice_string) + + # check values in xml + nodes = invoice_xml.xpath(self._readxml_xpath([ + 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction', + 'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty', + 'ram:SpecifiedLegalOrganization', 'ram:ID']), + namespaces=invoice_xml.nsmap) + self.assertEqual(nodes[0].text, 'Bonn HRB 6792') + + nodes = invoice_xml.xpath(self._readxml_xpath([ + 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction', + 'ram:IncludedSupplyChainTradeLineItem', + 'ram:SpecifiedLineTradeSettlement', + 'ram:SpecifiedTradeSettlementLineMonetarySummation', + 'ram:LineTotalAmount']), + namespaces=invoice_xml.nsmap) + self.assertEqual(nodes[0].text, '100.00') + schema = etree.XMLSchema(etree.parse(schema_file)) schema.assertValid(invoice_xml) + 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 + @with_transaction() def test_xrechn_export_facturx(self): """ run export - factur-x @@ -291,6 +332,10 @@ class EdocTestCase(ModuleTestCase): template = Template(invoice) + self.assertEqual( + template.party_legal_ids(invoice.company_party, None), + [('Bonn HRB 6792', {'schemeID': '0002'})]) + schema_file = os.path.join( os.path.dirname(__file__), 'Factur-X_1.07.2_EXTENDED', @@ -298,6 +343,15 @@ class EdocTestCase(ModuleTestCase): invoice_string = template.render('Factur-X-1.07.2-extended') invoice_xml = etree.fromstring(invoice_string) + + # check values in xml + nodes = invoice_xml.xpath(self._readxml_xpath([ + 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction', + 'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty', + 'ram:SpecifiedLegalOrganization', 'ram:ID']), + namespaces=invoice_xml.nsmap) + self.assertEqual(nodes[0].text, 'Bonn HRB 6792') + schema = etree.XMLSchema(etree.parse(schema_file)) schema.assertValid(invoice_xml) From 81f6a3e4a891c6193a7207bceae5e80230a034a9 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 11:48:35 +0200 Subject: [PATCH 15/20] ApplicableTradeTax: disable ExemptionReason if CategoryCode exist --- template/Factur-X-1.07.2-extended/invoice.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index bad23a9..9af1698 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -38,7 +38,7 @@ this repository contains the full copyright notices and license terms. --> ${amount * this.type_sign} ${this.tax_unece_code(tax)} - ${tax.legal_notice} + ${tax.legal_notice} ${base * this.type_sign} ${this.tax_category_code(tax)} ${tax.rate * 100} From 4c2565e15ec3b30e644ba195a58642e15edb2bca Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 11:48:55 +0200 Subject: [PATCH 16/20] add xml-validator --- docs/xrechnung.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/xrechnung.txt b/docs/xrechnung.txt index f8fbbbd..dab13fa 100644 --- a/docs/xrechnung.txt +++ b/docs/xrechnung.txt @@ -12,3 +12,5 @@ https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingp https://erechnungsvalidator.service-bw.de/ https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/ + +https://www.e-rechnungs-checker.de/ From 2dab5b1b438d7db9f21732bd0fff13005c4422a2 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 11:57:16 +0200 Subject: [PATCH 17/20] SpecifiedTradeProduct: export 'description' as 'name' if no product was used in invoice-line --- template/Factur-X-1.07.2-extended/invoice.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index 9af1698..938c38a 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -71,8 +71,8 @@ this repository contains the full copyright notices and license terms. --> ${line.product.code} - ${this.quote_text(line.product.name if line.product else '')} - ${this.quote_text(line.description)} + ${this.quote_text(line.product.name if line.product else line.description if line.description else 'name not set')} + ${this.quote_text(line.description if line.product else '')} From 437f047e44b63d860dd2345f411318ba8113156f Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 12:14:05 +0200 Subject: [PATCH 18/20] test: fix tax-code --- tests/test_edocument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_edocument.py b/tests/test_edocument.py index a7c9355..179957d 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -131,7 +131,7 @@ class EdocTestCase(ModuleTestCase): tax, = Taxes.search([('name', '=', '20% VAT')]) Taxes.write(*[ [tax], - {'unece_code': 'GST', 'unece_category_code': 'S', + {'unece_code': 'VAT', 'unece_category_code': 'S', 'legal_notice': 'Legal Notice'}]) account_lst = Account.search([ From f3375d3d39894eb30f8816694eb8208dc6656a38 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Jun 2025 15:57:35 +0200 Subject: [PATCH 19/20] allow any vat-type to be exported as 'SpecifiedTaxRegistration' --- template/Factur-X-1.07.2-extended/invoice.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index 938c38a..a4620c5 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -19,7 +19,7 @@ this repository contains the full copyright notices and license terms. --> ${TradeAddress(address)} - + ${tax_identifier.code} From a8541f73bdde8d6e3985999cfd6b020750cc3512 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 2 Jul 2025 13:51:57 +0200 Subject: [PATCH 20/20] dont except if no 'modegross' on invoice.line --- mixin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mixin.py b/mixin.py index 1ffdd6f..dc235ac 100644 --- a/mixin.py +++ b/mixin.py @@ -168,6 +168,9 @@ class EdocumentMixin(object): Args: line (record): model account.invoice.line """ + if not hasattr(line, 'modegross'): + return line.amount + if line.modegross == 'net': return line.amount elif line.modegross == 'gross':