diff --git a/__init__.py b/__init__.py
index d20da9a..a7f7f9c 100644
--- a/__init__.py
+++ b/__init__.py
@@ -5,12 +5,17 @@
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/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/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/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
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/mixin.py b/mixin.py
index f27804f..1e75916 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.invoice.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
@@ -151,6 +171,6 @@ class EdocumentMixin(object):
""" replace critical chars
"""
if text:
- return html.quote(text)
+ return html.escape(text)
# end EdocumentMixin
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)}
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
diff --git a/tryton.cfg b/tryton.cfg
index aeb1d45..2c28cd6 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+