Merge branch 'main' into 7.0

This commit is contained in:
Frederik Jaeckel 2024-12-10 13:54:02 +01:00
commit d442390154
12 changed files with 319 additions and 17 deletions

View file

@ -5,12 +5,17 @@
from trytond.pool import Pool from trytond.pool import Pool
from .edocument import XRechnung, FacturX from .edocument import XRechnung, FacturX
from .bank import AccountNumber
from .party import PartyConfiguration, Party from .party import PartyConfiguration, Party
from .configuration import Configuration, BankEdocumentRel
def register(): def register():
Pool.register( Pool.register(
AccountNumber,
XRechnung, XRechnung,
Configuration,
BankEdocumentRel,
FacturX, FacturX,
Party, Party,
PartyConfiguration, PartyConfiguration,

97
bank.py Normal file
View file

@ -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

35
configuration.py Normal file
View file

@ -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

15
configuration.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data>
<record model="ir.ui.view" id="configuration_view_form">
<field name="model">account.configuration</field>
<field name="inherit" ref="account.configuration_view_form"/>
<field name="name">configuration_form</field>
</record>
</data>
</tryton>

View file

@ -6,3 +6,6 @@ https://github.com/itplr-kosit
validator: validator:
- https://erechnungsvalidator.service-bw.de/ - 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

View file

@ -113,3 +113,31 @@ msgstr "Allgemeine indirekte Steuer der kanarischen Inseln"
msgctxt "selection:account.tax,xrtax_category:" msgctxt "selection:account.tax,xrtax_category:"
msgid "Tax on production; services and imports in Ceuta and Melilla" msgid "Tax on production; services and imports in Ceuta and Melilla"
msgstr "Steuer für Produktion; Dienstleistungen und Einfuhr in Ceuta und 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"

View file

@ -98,3 +98,23 @@ msgctxt "selection:account.tax,xrtax_category:"
msgid "Tax on production; services and imports in Ceuta and Melilla" msgid "Tax on production; services and imports in Ceuta and Melilla"
msgstr "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"

View file

@ -9,6 +9,7 @@ import html
from trytond.exceptions import UserError from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.tools import cached_property from trytond.tools import cached_property
from trytond.pool import Pool
class EdocumentMixin(object): class EdocumentMixin(object):
@ -16,6 +17,25 @@ class EdocumentMixin(object):
""" """
__slots__ = () __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 @cached_property
def seller_trade_address(self): def seller_trade_address(self):
""" get address of seller, throw exception if incomplete """ get address of seller, throw exception if incomplete
@ -151,6 +171,6 @@ class EdocumentMixin(object):
""" replace critical chars """ replace critical chars
""" """
if text: if text:
return html.quote(text) return html.escape(text)
# end EdocumentMixin # end EdocumentMixin

View file

@ -12,7 +12,7 @@ this repository contains the full copyright notices and license terms. -->
<udt:DateTimeString format="102">${value.strftime('%Y%m%d')}</udt:DateTimeString> <udt:DateTimeString format="102">${value.strftime('%Y%m%d')}</udt:DateTimeString>
</py:def> </py:def>
<py:def function="TradeParty(party, address=None, tax_identifier=None)"> <py:def function="TradeParty(party, address=None, tax_identifier=None)">
<ram:Name>${party.name}</ram:Name> <ram:Name>${this.quote_text(party.name)}</ram:Name>
<ram:SpecifiedLegalOrganization> <ram:SpecifiedLegalOrganization>
<py:for each="id, attrs in this.party_legal_ids(party, address)"> <py:for each="id, attrs in this.party_legal_ids(party, address)">
<ram:ID py:attrs="attrs">${id}</ram:ID> <ram:ID py:attrs="attrs">${id}</ram:ID>
@ -26,13 +26,13 @@ this repository contains the full copyright notices and license terms. -->
<py:def function="TradeAddress(address)"> <py:def function="TradeAddress(address)">
<ram:PostcodeCode py:if="address.postal_code">${address.postal_code}</ram:PostcodeCode> <ram:PostcodeCode py:if="address.postal_code">${address.postal_code}</ram:PostcodeCode>
<py:with vars="lines = (address.street or '').splitlines()"> <py:with vars="lines = (address.street or '').splitlines()">
<ram:LineOne py:if="len(lines) > 0">${lines[0]}</ram:LineOne> <ram:LineOne py:if="len(lines) > 0">${this.quote_text(lines[0])}</ram:LineOne>
<ram:LineTwo py:if="len(lines) > 1">${lines[1]}</ram:LineTwo> <ram:LineTwo py:if="len(lines) > 1">${this.quote_text(lines[1])}</ram:LineTwo>
<ram:LineThree py:if="len(lines) > 2">${lines[2]}</ram:LineThree> <ram:LineThree py:if="len(lines) > 2">${(lines[2])}</ram:LineThree>
</py:with> </py:with>
<ram:CityName py:if="address.city">${address.city}</ram:CityName> <ram:CityName py:if="address.city">${(address.city)}</ram:CityName>
<ram:CountryID py:if="address.country">${address.country.code}</ram:CountryID> <ram:CountryID py:if="address.country">${address.country.code}</ram:CountryID>
<ram:CountrySubDivisionName py:if="address.subdivision">${address.subdivision.name}</ram:CountrySubDivisionName> <ram:CountrySubDivisionName py:if="address.subdivision">${this.quote_text(address.subdivision.name)}</ram:CountrySubDivisionName>
</py:def> </py:def>
<py:def function="TradeTax(tax, amount=None, base=None)"> <py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax> <ram:ApplicableTradeTax>
@ -50,8 +50,8 @@ this repository contains the full copyright notices and license terms. -->
</ram:GuidelineSpecifiedDocumentContextParameter> </ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext> </rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument> <rsm:ExchangedDocument>
<ram:ID>${this.invoice.number}</ram:ID> <ram:ID>${this.quote_text(this.invoice.number)}</ram:ID>
<ram:Name py:if="this.invoice.description">${this.invoice.description}</ram:Name> <ram:Name py:if="this.invoice.description">${this.quote_text(this.invoice.description)}</ram:Name>
<ram:TypeCode>${this.type_code}</ram:TypeCode> <ram:TypeCode>${this.type_code}</ram:TypeCode>
<ram:IssueDateTime> <ram:IssueDateTime>
${DateTime(this.invoice.invoice_date)} ${DateTime(this.invoice.invoice_date)}
@ -71,8 +71,8 @@ this repository contains the full copyright notices and license terms. -->
</ram:AssociatedDocumentLineDocument> </ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct> <ram:SpecifiedTradeProduct>
<ram:ID py:if="line.product and line.product.code">${line.product.code}</ram:ID> <ram:ID py:if="line.product and line.product.code">${line.product.code}</ram:ID>
<ram:Name>${line.product.name if line.product else ''}</ram:Name> <ram:Name>${this.quote_text(line.product.name if line.product else '')}</ram:Name>
<ram:Description py:if="line.description">${line.description}</ram:Description> <ram:Description py:if="line.description">${this.quote_text(line.description)}</ram:Description>
</ram:SpecifiedTradeProduct> </ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement> <ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice> <ram:NetPriceProductTradePrice>
@ -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)} ${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)}
</ram:BuyerTradeParty> </ram:BuyerTradeParty>
<ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'"> <ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'">
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID> <ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID>
</ram:SellerOrderReferencedDocument> </ram:SellerOrderReferencedDocument>
<ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'"> <ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'">
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID> <ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID>
</ram:BuyerOrderReferencedDocument> </ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement> </ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery> <ram:ApplicableHeaderTradeDelivery>
@ -114,11 +114,26 @@ this repository contains the full copyright notices and license terms. -->
</ram:ShipFromTradeParty> </ram:ShipFromTradeParty>
</ram:ApplicableHeaderTradeDelivery> </ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement> <ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>${this.payment_reference}</ram:PaymentReference> <ram:PaymentReference>${this.quote_text(this.payment_reference)}</ram:PaymentReference>
<ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode> <ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode>
<py:if test="len(this.company_bank_accounts()) == 0">
<ram:SpecifiedTradeSettlementPaymentMeans> <ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>1</ram:TypeCode> <!-- Instrument not defined --> <ram:TypeCode>1</ram:TypeCode>
</ram:SpecifiedTradeSettlementPaymentMeans> </ram:SpecifiedTradeSettlementPaymentMeans>
</py:if>
<py:for each="banknumber in this.company_bank_accounts()">
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:Information>Wire transfer</ram:Information>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>${banknumber.number_compact}</ram:IBANID>
<ram:AccountName>${this.quote_text(banknumber.account.bank.party.rec_name)}</ram:AccountName>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>${banknumber.account.bank.bic}</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
</py:for>
<py:for each="tax in this.invoice.taxes"> <py:for each="tax in this.invoice.taxes">
${TradeTax(tax.tax, tax.amount, tax.base)} ${TradeTax(tax.tax, tax.amount, tax.base)}
</py:for> </py:for>

View file

@ -11,6 +11,7 @@ from datetime import date
from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool from trytond.pool import Pool
from trytond.modules.edocument_uncefact.tests.test_module import get_invoice 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 from trytond.exceptions import UserError
@ -18,6 +19,53 @@ class EdocTestCase(ModuleTestCase):
'Test e-rechnung module' 'Test e-rechnung module'
module = 'edocument_xrechnung' 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() @with_transaction()
def test_xrechn_check_validator(self): def test_xrechn_check_validator(self):
""" check validation of optional route-id """ check validation of optional route-id

View file

@ -7,4 +7,5 @@ depends:
account_invoice account_invoice
xml: xml:
message.xml message.xml
configuration.xml
party.xml party.xml

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the account-datev-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<data>
<xpath expr="/form/separator[@id='currency_exchange']" position="before">
<separator id="edocument" colspan="4" string="eDocument"/>
<field name="edocument_bank" colspan="2" height="200"/>
<newline/>
</xpath>
</data>