From bff0f296c3947812ed804ee1d7bc9ffd98c255e2 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:22:33 +0100 Subject: [PATCH 01/32] doks --- docs/xrechnung.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/xrechnung.txt b/docs/xrechnung.txt index 020bd2a..3cf8cff 100644 --- a/docs/xrechnung.txt +++ b/docs/xrechnung.txt @@ -6,3 +6,6 @@ https://github.com/itplr-kosit validator: - https://erechnungsvalidator.service-bw.de/ + +ü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 From f04d8c907b626f884c80567a7dfc152eccaa0ebe Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:24:45 +0100 Subject: [PATCH 02/32] bank accont number: add field 'company_owned' --- __init__.py | 2 + bank.py | 97 +++++++++++++++++++++++++++++++++++++++++ tests/test_edocument.py | 48 ++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 bank.py diff --git a/__init__.py b/__init__.py index d20da9a..9d78ad7 100644 --- a/__init__.py +++ b/__init__.py @@ -5,11 +5,13 @@ from trytond.pool import Pool from .edocument import XRechnung, FacturX +from .bank import AccountNumber from .party import PartyConfiguration, Party def register(): Pool.register( + AccountNumber, XRechnung, FacturX, Party, diff --git a/bank.py b/bank.py new file mode 100644 index 0000000..1d6f923 --- /dev/null +++ b/bank.py @@ -0,0 +1,97 @@ +# -*- 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 sql.conditionals import Case +from trytond.pool import PoolMeta, Pool +from trytond.model import fields +from trytond.transaction import Transaction + +DEF_NONE = None + + +class AccountNumber(metaclass=PoolMeta): + __name__ = 'bank.account.number' + + company_owned = fields.Function(fields.Boolean( + string='Number belongs to Company', + readonly=True), + 'get_company_owned', + searcher='searcher_company_owned') + + @classmethod + def get_company_owned_sql(cls): + """ get sql to search for bank acconts owned by company-party + """ + pool = Pool() + Account = pool.get('bank.account') + Number = pool.get('bank.account.number') + Owners = pool.get('bank.account-party.party') + Company = pool.get('company.company') + context = Transaction().context + + tab_acc = Account.__table__() + tab_owner = Owners.__table__() + tab_num = Number.__table__() + + company_id = context.get('company', -1) + party_id = -1 + if company_id and company_id > 0: + party_id = Company(company_id).party.id + + query = tab_num.join( + tab_acc, + condition=tab_num.account == tab_acc.id, + ).join( + tab_owner, + condition=tab_owner.account == tab_acc.id, + ).select( + tab_num.id.as_('number'), + Case( + (tab_owner.owner == party_id, True), + else_=False, + ).as_('owned')) + return (tab_num, query) + + @classmethod + def searcher_company_owned(cls, name, clause): + """ search in owned by party + + Args: + name (str): field name + clause (list): search domain + + Returns: + list: updated search domain + """ + Operator = fields.SQL_OPERATORS[clause[1]] + (tab_num, query) = cls.get_company_owned_sql() + + query = query.select( + query.number, + where=Operator(query.owned, clause[2])) + return [('id', 'in', query)] + + @classmethod + def get_company_owned(cls, records, names): + """ get list of bank account numbers owned by company + """ + cursor = Transaction().connection.cursor() + + result = {x: {y.id: False for y in records} for x in names} + + (tab_num, query) = cls.get_company_owned_sql() + query.where = tab_num.id.in_([x.id for x in records]) + cursor.execute(*query) + lines = cursor.fetchall() + + for line in lines: + values = {'company_owned': line[1]} + + for name in names: + result[name][line[0]] = values.get(name) + return result + +# end AccountNumber diff --git a/tests/test_edocument.py b/tests/test_edocument.py index eaa75df..4ff38b7 100644 --- a/tests/test_edocument.py +++ b/tests/test_edocument.py @@ -11,6 +11,7 @@ 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.exceptions import UserError @@ -18,6 +19,53 @@ class EdocTestCase(ModuleTestCase): 'Test e-rechnung module' module = 'edocument_xrechnung' + @with_transaction() + def test_xrechn_bank_account_owned(self): + """ check field 'company_owned' on bank.account.number + """ + pool = Pool() + BankAccount = pool.get('bank.account') + AccountNumber = pool.get('bank.account.number') + Bank = pool.get('bank') + Party = pool.get('party.party') + + company = create_company() + with set_company(company): + bank_party, = Party.create([{ + 'name': 'Bank 123', + 'addresses': [('create', [{}])]}]) + customer_party, = Party.create([{ + 'name': 'Someone', + 'addresses': [('create', [{}])]}]) + bank, = Bank.create([{'party': bank_party.id}]) + + acc_company, acc_other, = BankAccount.create([ + { + 'bank': bank.id, + 'owners': [('add', [company.party.id])], + 'numbers': [('create', [ + {'type': 'iban', 'number': 'DE02300209000106531065'}])] + }, { + 'bank': bank.id, + 'owners': [('add', [customer_party.id])], + 'numbers': [('create', [ + {'type': 'iban', 'number': 'DE02200505501015871393'}])] + }]) + self.assertEqual(len(acc_company.numbers), 1) + self.assertEqual(acc_company.numbers[0].company_owned, True) + self.assertEqual(len(acc_other.numbers), 1) + self.assertEqual(acc_other.numbers[0].company_owned, False) + + company_numbers = AccountNumber.search( + [('company_owned', '=', True)]) + self.assertEqual(len(company_numbers), 1) + self.assertEqual(company_numbers[0].id, acc_company.numbers[0].id) + + other_numbers = AccountNumber.search( + [('company_owned', '=', False)]) + self.assertEqual(len(other_numbers), 1) + self.assertEqual(other_numbers[0].id, acc_other.numbers[0].id) + @with_transaction() def test_xrechn_check_validator(self): """ check validation of optional route-id From 81ea9d6ab1484a337a4cdae9e7cbbadc5ac5da54 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:25:51 +0100 Subject: [PATCH 03/32] configuration: add setting for used bank nuumbers --- __init__.py | 3 +++ configuration.py | 35 +++++++++++++++++++++++++++++++++++ configuration.xml | 15 +++++++++++++++ locale/de.po | 28 ++++++++++++++++++++++++++++ locale/en.po | 20 ++++++++++++++++++++ tryton.cfg | 1 + view/configuration_form.xml | 15 +++++++++++++++ 7 files changed, 117 insertions(+) create mode 100644 configuration.py create mode 100644 configuration.xml create mode 100644 view/configuration_form.xml diff --git a/__init__.py b/__init__.py index 9d78ad7..a7f7f9c 100644 --- a/__init__.py +++ b/__init__.py @@ -7,12 +7,15 @@ from trytond.pool import Pool from .edocument import XRechnung, FacturX from .bank import AccountNumber from .party import PartyConfiguration, Party +from .configuration import Configuration, BankEdocumentRel def register(): Pool.register( AccountNumber, XRechnung, + Configuration, + BankEdocumentRel, FacturX, Party, PartyConfiguration, diff --git a/configuration.py b/configuration.py new file mode 100644 index 0000000..d0aa911 --- /dev/null +++ b/configuration.py @@ -0,0 +1,35 @@ +# -*- 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 trytond.pool import PoolMeta +from trytond.model import ModelSQL, fields + + +class Configuration(metaclass=PoolMeta): + __name__ = 'account.configuration' + + edocument_bank = fields.Many2Many( + string='Bank accounts', + relation_name='edocument_xrechnung.bank_rel', + origin='config', target='bankaccount', + filter=[('company_owned', '=', True)], + help='The bank accounts listed here are output in the invoice XML.') + +# end Configuration + + +class BankEdocumentRel(ModelSQL): + 'Bank - eDocument - Relation' + __name__ = 'edocument_xrechnung.bank_rel' + + bankaccount = fields.Many2One( + string='Account', model_name='bank.account.number', + required=True, ondelete='CASCADE') + config = fields.Many2One( + string='Configuration', model_name='account.configuration', + required=True, ondelete='CASCADE') + +# end BankEdocumentRel diff --git a/configuration.xml b/configuration.xml new file mode 100644 index 0000000..630a828 --- /dev/null +++ b/configuration.xml @@ -0,0 +1,15 @@ + + + + + + + account.configuration + + configuration_form + + + + diff --git a/locale/de.po b/locale/de.po index d8156a9..aaaa604 100644 --- a/locale/de.po +++ b/locale/de.po @@ -113,3 +113,31 @@ msgstr "Allgemeine indirekte Steuer der kanarischen Inseln" msgctxt "selection:account.tax,xrtax_category:" msgid "Tax on production; services and imports in Ceuta and Melilla" msgstr "Steuer für Produktion; Dienstleistungen und Einfuhr in Ceuta und Melilla" + + +######################### +# account.configuration # +######################### +msgctxt "field:account.configuration,edocument_bank:" +msgid "Bank accounts" +msgstr "Bankkonten" + +msgctxt "help:account.configuration,edocument_bank:" +msgid "The bank accounts listed here are output in the invoice XML." +msgstr "Die hier aufgeführten Bankkonten werden in der Rechnungs-XML ausgegeben." + + +################################ +# edocument_xrechnung.bank_rel # +################################ +msgctxt "model:edocument_xrechnung.bank_rel,name:" +msgid "Bank - eDocument - Relation" +msgstr "Bank - eDocument - Verknüpfung" + +msgctxt "field:edocument_xrechnung.bank_rel,bankaccount:" +msgid "Account" +msgstr "Konto" + +msgctxt "field:edocument_xrechnung.bank_rel,config:" +msgid "Configuration" +msgstr "Konfiguration" diff --git a/locale/en.po b/locale/en.po index a4978ea..b0353ce 100644 --- a/locale/en.po +++ b/locale/en.po @@ -98,3 +98,23 @@ msgctxt "selection:account.tax,xrtax_category:" msgid "Tax on production; services and imports in Ceuta and Melilla" msgstr "Tax on production; services and imports in Ceuta and Melilla" +msgctxt "field:account.configuration,edocument_bank:" +msgid "Bank accounts" +msgstr "Bank accounts" + +msgctxt "help:account.configuration,edocument_bank:" +msgid "The bank accounts listed here are output in the invoice XML." +msgstr "The bank accounts listed here are output in the invoice XML." + +msgctxt "model:edocument_xrechnung.bank_rel,name:" +msgid "Bank - eDocument - Relation" +msgstr "Bank - eDocument - Relation" + +msgctxt "field:edocument_xrechnung.bank_rel,bankaccount:" +msgid "Account" +msgstr "Account" + +msgctxt "field:edocument_xrechnung.bank_rel,config:" +msgid "Configuration" +msgstr "Configuration" + diff --git a/tryton.cfg b/tryton.cfg index 8552590..4d83b5f 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -7,4 +7,5 @@ depends: account_invoice xml: message.xml + configuration.xml party.xml diff --git a/view/configuration_form.xml b/view/configuration_form.xml new file mode 100644 index 0000000..244d830 --- /dev/null +++ b/view/configuration_form.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + From 813f5a4bcfe78c536a27098ef2b1b59591497790 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:58:45 +0100 Subject: [PATCH 04/32] fix typo --- mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixin.py b/mixin.py index f27804f..10f4fe6 100644 --- a/mixin.py +++ b/mixin.py @@ -151,6 +151,6 @@ class EdocumentMixin(object): """ replace critical chars """ if text: - return html.quote(text) + return html.escape(text) # end EdocumentMixin From 9f2be4e593af372cebce63eb775c0a559e2a6822 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:59:09 +0100 Subject: [PATCH 05/32] add bank account number to xml-export --- mixin.py | 20 ++++++++ template/Factur-X-1.07.2-extended/invoice.xml | 47 ++++++++++++------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/mixin.py b/mixin.py index 10f4fe6..84fce84 100644 --- a/mixin.py +++ b/mixin.py @@ -9,6 +9,7 @@ import html from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.tools import cached_property +from trytond.pool import Pool class EdocumentMixin(object): @@ -16,6 +17,25 @@ class EdocumentMixin(object): """ __slots__ = () + def company_bank_accounts(self): + """ get leist of bank account numbers, defined in config + + Returns: + list: records of model bank.account.number + """ + Configuration = Pool().get('account.configuration') + + result = [] + cfg1 = Configuration.get_singleton() + if cfg1 and cfg1.edocument_bank: + result.extend(list(cfg1.edocument_bank)) + else: + result.extend([ + y + for x in self.company.party.bank_accounts + for y in x.numbers]) + return result + @cached_property def seller_trade_address(self): """ get address of seller, throw exception if incomplete diff --git a/template/Factur-X-1.07.2-extended/invoice.xml b/template/Factur-X-1.07.2-extended/invoice.xml index ff32e24..8a7a664 100644 --- a/template/Factur-X-1.07.2-extended/invoice.xml +++ b/template/Factur-X-1.07.2-extended/invoice.xml @@ -12,7 +12,7 @@ this repository contains the full copyright notices and license terms. --> ${value.strftime('%Y%m%d')} - ${party.name} + ${this.quote_text(party.name)} ${id} @@ -26,13 +26,13 @@ this repository contains the full copyright notices and license terms. --> ${address.postal_code} - ${lines[0]} - ${lines[1]} - ${lines[2]} + ${this.quote_text(lines[0])} + ${this.quote_text(lines[1])} + ${(lines[2])} - ${address.city} + ${(address.city)} ${address.country.code} - ${address.subdivision.name} + ${this.quote_text(address.subdivision.name)} @@ -50,8 +50,8 @@ this repository contains the full copyright notices and license terms. --> - ${this.invoice.number} - ${this.invoice.description} + ${this.quote_text(this.invoice.number)} + ${this.quote_text(this.invoice.description)} ${this.type_code} ${DateTime(this.invoice.invoice_date)} @@ -71,8 +71,8 @@ this repository contains the full copyright notices and license terms. --> ${line.product.code} - ${line.product.name if line.product else ''} - ${line.description} + ${this.quote_text(line.product.name if line.product else '')} + ${this.quote_text(line.description)} @@ -99,10 +99,10 @@ this repository contains the full copyright notices and license terms. --> ${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)} - ${this.invoice.reference} + ${this.quote_text(this.invoice.reference)} - ${this.invoice.reference} + ${this.quote_text(this.invoice.reference)} @@ -114,11 +114,26 @@ this repository contains the full copyright notices and license terms. --> - ${this.payment_reference} + ${this.quote_text(this.payment_reference)} ${this.invoice.currency.code} - - 1 - + + + 1 + + + + + 30 + Wire transfer + + ${banknumber.number_compact} + ${this.quote_text(banknumber.account.bank.party.rec_name)} + + + ${banknumber.account.bank.bic} + + + ${TradeTax(tax.tax, tax.amount, tax.base)} From ab064c43efa3d278627aed3195c871ae997d2dd5 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 13:16:04 +0100 Subject: [PATCH 06/32] fix code --- mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixin.py b/mixin.py index 84fce84..1e75916 100644 --- a/mixin.py +++ b/mixin.py @@ -32,7 +32,7 @@ class EdocumentMixin(object): else: result.extend([ y - for x in self.company.party.bank_accounts + for x in self.invoice.company.party.bank_accounts for y in x.numbers]) return result From 5a0af0be7cddd42279324ff2197e83ad75b05fb1 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 13:54:56 +0100 Subject: [PATCH 07/32] Version 7.0.7 --- README.rst | 4 ++++ tryton.cfg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bb0c979..da8bd11 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,10 @@ Requires Changes ======= +*7.0.7 - 10.12.2024* + +- add iban to xml-export + *7.0.6 - 09.12.2024* - add: check for valid data to generate xml diff --git a/tryton.cfg b/tryton.cfg index 2c28cd6..b71e926 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=7.0.6 +version=7.0.7 depends: edocument_uncefact party From b28ce594d8f4595afe3b8b8e2831c5d86bfffdb1 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Dec 2024 14:34:10 +0100 Subject: [PATCH 08/32] fix name of party in error message --- mixin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixin.py b/mixin.py index 1e75916..abd7a20 100644 --- a/mixin.py +++ b/mixin.py @@ -57,7 +57,7 @@ class EdocumentMixin(object): if not result.country: raise UserError(gettext( 'edocument_xrechnung.msg_no_address_country', - party=result.party)) + party=result.party.rec_name if result.party else '-')) return result @cached_property @@ -79,7 +79,7 @@ class EdocumentMixin(object): if result and not result.country: raise UserError(gettext( 'edocument_xrechnung.msg_no_address_country', - party=result.party)) + party=result.party.rec_name if result.party else '-')) return result def get_list_of_comments(self): From 87f759334066a8cab22d925d2b988bbdf4687620 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Wed, 11 Dec 2024 17:03:29 +0100 Subject: [PATCH 09/32] Version 7.0.9 --- README.rst | 4 ++++ tryton.cfg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index da8bd11..affb395 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,10 @@ Requires Changes ======= +*7.0.9 - 11.12.2024* + +- fix name of party in exceptions + *7.0.7 - 10.12.2024* - add iban to xml-export diff --git a/tryton.cfg b/tryton.cfg index b71e926..88331ef 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=7.0.7 +version=7.0.9 depends: edocument_uncefact party From 79c2227131241397e9df9db266741cc32e605dbe Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Thu, 12 Dec 2024 09:45:12 +0100 Subject: [PATCH 10/32] 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 11/32] 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 12/32] 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 659e78a68656661489876587446ad230ce8a7c9b Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Thu, 19 Dec 2024 10:20:57 +0100 Subject: [PATCH 13/32] Correct a translation. --- locale/de.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/de.po b/locale/de.po index aaaa604..58b677b 100644 --- a/locale/de.po +++ b/locale/de.po @@ -16,7 +16,7 @@ msgstr "Für die Steuer '%(taxname)s' ist die UNECE-Steuerkategorie nicht konfig msgctxt "model:ir.message,text:msg_tax_code_missing" msgid "The UNECE tax code is not configured for tax '%(taxname)s'." -msgstr "Für die Steuer '%(taxname)s' ist der UNECE-Einheitencode nicht konfiguriert." +msgstr "Für die Steuer '%(taxname)s' ist der UNECE Steuercode nicht konfiguriert." msgctxt "model:ir.message,text:msg_uom_code_missing" msgid "The UNECE uom code is not configured for unit '%(uomname)s'." From df4638896795fc281b16859c20666b9226104773 Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Thu, 19 Dec 2024 10:22:12 +0100 Subject: [PATCH 14/32] Lookup parent taxes for unece tax codes. In the same way as for categories the unece tax codes must be searched on parents. --- mixin.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mixin.py b/mixin.py index abd7a20..04e94f4 100644 --- a/mixin.py +++ b/mixin.py @@ -144,12 +144,20 @@ class EdocumentMixin(object): def tax_unece_code(self, tax): """ 'tax': invoice.line """ - if not (tax.unece_code or ''): + unece_code = self.get_tax_unece_code(tax) + if not unece_code: raise UserError(gettext( 'edocument_xrechnung.msg_tax_code_missing', taxname=tax.rec_name)) return tax.unece_code + def get_tax_unece_code(self, tax): + while tax: + if tax.unece_code: + return tax.unece_code + break + tax = tax.parent + def get_category_code(self, tax): while tax: if tax.unece_category_code: From 8eb9c284fb604c8baa57edc1ffd3a040cb7a5663 Mon Sep 17 00:00:00 2001 From: Mathias Behrle Date: Thu, 19 Dec 2024 10:42:41 +0100 Subject: [PATCH 15/32] Improve the help text of xrechnung_routeid. --- locale/de.po | 9 ++++++--- party.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/locale/de.po b/locale/de.po index 58b677b..dc3b72c 100644 --- a/locale/de.po +++ b/locale/de.po @@ -63,9 +63,12 @@ msgid "X-Rechnung Route-ID" msgstr "X-Rechnung Leitweg-ID" msgctxt "help:party.party,xrechnung_routeid:" -msgid "Enables the need for an XRechnung route ID at the party for exporting the XRechnung." -msgstr "Aktiviert die Notwendigkeit einer XRechnung-Leitweg-ID an der Partei für den Export der XRechnung." - +msgid "" +"When activated an XRechnung route ID must be used for this party for X-Rechnung exports.\n" +"The route ID must be defined as identifier of type \"X-Rechnung Route-ID\"." +msgstr "" +"Bei Aktivierung muss eine XRechnung-Leitweg-ID bei Rechnungsexporten für diese Partei benutzt werden.\n" +"Die Leitweg-ID muss als Identifikator mit Typ \"X-Rechnung Route-ID\" angelegt werden." ############### # account.tax # diff --git a/party.py b/party.py index cc020c6..adcd519 100644 --- a/party.py +++ b/party.py @@ -14,8 +14,10 @@ class Party(metaclass=PoolMeta): xrechnung_routeid = fields.Boolean( string='X-Rechnung Route-ID', - help='Enables the need for an XRechnung route ID at the party ' + - 'for exporting the XRechnung.') + help='When activated an XRechnung route ID must be used ' + 'for this party for X-Rechnung exports.\n' + 'The route ID must be defined as identifier of type ' + '"X-Rechnung Route-ID".') def get_xrechnung_route_id(self): """ search for route-id at party, fire-exception if missing From 7ddb94f47ea7a2a89406a31795fdd92c09296d0c Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Thu, 19 Dec 2024 11:34:36 +0100 Subject: [PATCH 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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':