From bff0f296c3947812ed804ee1d7bc9ffd98c255e2 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Tue, 10 Dec 2024 12:22:33 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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