Compare commits

..

No commits in common. "main" and "ver-7.0.6" have entirely different histories.

17 changed files with 169 additions and 581 deletions

View file

@ -1,5 +1,6 @@
Copyright (C) 2021-2025 martin-data services.
Copyright (C) 2024-2025 Mathias Behrle
Copyright (C) 2015-2023 Cédric Krier.
Copyright (C) 2015-2023 B2CK SPRL.
Copyright (C) 2021-2023 martin-data services.
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

View file

@ -14,19 +14,6 @@ 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
*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

View file

@ -5,17 +5,12 @@
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,

97
bank.py
View file

@ -1,97 +0,0 @@
# -*- 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

View file

@ -1,35 +0,0 @@
# -*- 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

View file

@ -1,15 +0,0 @@
<?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,9 +6,3 @@ 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
https://erechnungsvalidator.service-bw.de/
https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/

View file

@ -70,7 +70,7 @@ class XRechnung(EdocumentMixin, Invoice):
else:
raise ValueError('invalid type-code "%s"' % self.type_code)
else:
return super()._get_template(version)
return super(XRechnung, self)._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()._get_template(version)
return super(FacturX, self)._get_template(version)
# end FacturX

View file

@ -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 Steuercode nicht konfiguriert."
msgstr "Für die Steuer '%(taxname)s' ist der UNECE-Einheitencode nicht konfiguriert."
msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'."
@ -63,12 +63,9 @@ msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Leitweg-ID"
msgctxt "help:party.party,xrechnung_routeid:"
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."
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."
###############
# account.tax #
@ -116,31 +113,3 @@ 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"

View file

@ -98,23 +98,3 @@ 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"

View file

@ -9,8 +9,6 @@ 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 round_price
class EdocumentMixin(object):
@ -18,25 +16,6 @@ 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
@ -48,7 +27,7 @@ class EdocumentMixin(object):
Returns:
record : model party.address
"""
result = super().seller_trade_address
result = super(EdocumentMixin, self).seller_trade_address
if not result:
raise UserError(gettext(
'edocument_xrechnung.msg_no_seller_address',
@ -58,7 +37,7 @@ class EdocumentMixin(object):
if not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party.rec_name if result.party else '-'))
party=result.party))
return result
@cached_property
@ -76,11 +55,11 @@ class EdocumentMixin(object):
if self.invoice and self.invoice.party
else '-'))
result = super().buyer_trade_address
result = super(EdocumentMixin, self).buyer_trade_address
if result and not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party.rec_name if result.party else '-'))
party=result.party))
return result
def get_list_of_comments(self):
@ -101,29 +80,21 @@ 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:
if len(line.invoice_taxes) != 1:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_number',
linename=line.rec_name,
numtax=len(line.taxes)))
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']
numtax=len(line.invoice_taxes)))
allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B']
unece_category_code = self.get_category_code(tax)
unece_category_code = self.get_category_code(line.invoice_taxes[0].tax)
if unece_category_code not in allowed_cat:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_catcode',
taxname=tax.rec_name,
taxname=line.invoice_taxes[0].tax.rec_name,
allowed=', '.join(allowed_cat)))
return tax
return line.invoice_taxes[0].tax
def taxident_data(self, tax_identifier):
""" get tax-scheme-id and codes
@ -153,20 +124,12 @@ class EdocumentMixin(object):
def tax_unece_code(self, tax):
""" 'tax': invoice.line
"""
unece_code = self.get_tax_unece_code(tax)
if not unece_code:
if not (tax.unece_code or ''):
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:
@ -184,23 +147,10 @@ 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 value is not None:
return round_price(value)
return value
def quote_text(self, text):
""" replace critical chars
"""
if text:
return html.escape(text)
return html.quote(text)
# end EdocumentMixin

View file

@ -14,10 +14,8 @@ class Party(metaclass=PoolMeta):
xrechnung_routeid = fields.Boolean(
string='X-Rechnung Route-ID',
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".')
help='Enables the need for an XRechnung route ID at the party ' +
'for exporting the XRechnung.')
def get_xrechnung_route_id(self):
""" search for route-id at party, fire-exception if missing
@ -47,7 +45,7 @@ class Party(metaclass=PoolMeta):
Args:
records (list): records of party.party
"""
super().validate(records)
super(Party, cls).validate(records)
for record in records:
record.get_xrechnung_route_id()
@ -59,7 +57,7 @@ class PartyConfiguration(metaclass=PoolMeta):
@classmethod
def __setup__(cls):
super().__setup__()
super(PartyConfiguration, cls).__setup__()
cls.identifier_types.selection.append(
('edoc_route_id', 'X-Rechnung Route-ID'))

View file

@ -13,9 +13,11 @@ 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'))
@ -85,8 +87,6 @@ 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',
@ -99,7 +99,7 @@ setup(
info.get('xml', [])
+ ['tryton.cfg', 'locale/*.po', 'tests/*.py',
'template/*/*.xml', 'versiondep.txt', 'README.rst',
'tests/*/*/*/*.xsd', 'view/*.xml',
'tests/*/*/*/*.xsd',
'tests/*/*.xsd', 'tests/*/*.sch', 'tests/*/*.xml',
'tests/*/*/*.xslt', 'tests/*/*/*.xml']),
},

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>
</py:def>
<py:def function="TradeParty(party, address=None, tax_identifier=None)">
<ram:Name>${this.quote_text(party.name)}</ram:Name>
<ram:Name>${party.name}</ram:Name>
<ram:SpecifiedLegalOrganization>
<py:for each="id, attrs in this.party_legal_ids(party, address)">
<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)">
<ram:PostcodeCode py:if="address.postal_code">${address.postal_code}</ram:PostcodeCode>
<py:with vars="lines = (address.street or '').splitlines()">
<ram:LineOne py:if="len(lines) > 0">${this.quote_text(lines[0])}</ram:LineOne>
<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:LineOne py:if="len(lines) > 0">${lines[0]}</ram:LineOne>
<ram:LineTwo py:if="len(lines) > 1">${lines[1]}</ram:LineTwo>
<ram:LineThree py:if="len(lines) > 2">${lines[2]}</ram:LineThree>
</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:CountrySubDivisionName py:if="address.subdivision">${this.quote_text(address.subdivision.name)}</ram:CountrySubDivisionName>
<ram:CountrySubDivisionName py:if="address.subdivision">${address.subdivision.name}</ram:CountrySubDivisionName>
</py:def>
<py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax>
@ -50,8 +50,8 @@ this repository contains the full copyright notices and license terms. -->
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>${this.quote_text(this.invoice.number)}</ram:ID>
<ram:Name py:if="this.invoice.description">${this.quote_text(this.invoice.description)}</ram:Name>
<ram:ID>${this.invoice.number}</ram:ID>
<ram:Name py:if="this.invoice.description">${this.invoice.description}</ram:Name>
<ram:TypeCode>${this.type_code}</ram:TypeCode>
<ram:IssueDateTime>
${DateTime(this.invoice.invoice_date)}
@ -71,19 +71,21 @@ this repository contains the full copyright notices and license terms. -->
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:ID py:if="line.product and line.product.code">${line.product.code}</ram:ID>
<ram:Name>${this.quote_text(line.product.name if line.product else '')}</ram:Name>
<ram:Description py:if="line.description">${this.quote_text(line.description)}</ram:Description>
<ram:Name>${line.product.name if line.product else ''}</ram:Name>
<ram:Description py:if="line.description">${line.description}</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.round_unitprice(line.unit_price)}</ram:ChargeAmount>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.currency.round(line.unit_price)}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${line.quantity * this.type_sign}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
${TradeTax(this.invoice_line_tax(line))}
<py:for each="tax in line.invoice_taxes">
${TradeTax(tax.tax)}
</py:for>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${line.amount}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
@ -97,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)}
</ram:BuyerTradeParty>
<ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'">
<ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID>
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:SellerOrderReferencedDocument>
<ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'">
<ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID>
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
@ -112,26 +114,11 @@ this repository contains the full copyright notices and license terms. -->
</ram:ShipFromTradeParty>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>${this.quote_text(this.payment_reference)}</ram:PaymentReference>
<ram:PaymentReference>${this.payment_reference}</ram:PaymentReference>
<ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode>
<py:if test="len(this.company_bank_accounts()) == 0">
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>1</ram:TypeCode>
</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>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>1</ram:TypeCode> <!-- Instrument not defined -->
</ram:SpecifiedTradeSettlementPaymentMeans>
<py:for each="tax in this.invoice.taxes">
${TradeTax(tax.tax, tax.amount, tax.base)}
</py:for>

View file

@ -5,206 +5,19 @@
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.company.tests import create_company, set_company
from trytond.modules.account.tests import create_chart, get_fiscalyear
from trytond.modules.edocument_uncefact.tests.test_module import get_invoice
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
"""
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
@ -239,24 +52,45 @@ 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')
company = self.prep_company()
with set_company(company):
create_chart(company=company, tax=True)
self.prep_fiscalyear(company)
invoice = self.prep_invoice()
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')
]
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):
@ -264,24 +98,45 @@ 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')
company = self.prep_company()
with set_company(company):
create_chart(company=company, tax=True)
self.prep_fiscalyear(company)
invoice = self.prep_invoice()
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')
]
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):
@ -289,24 +144,59 @@ 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')
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)
invoice = get_invoice()
template = Template(invoice)
# 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')
schema_file = os.path.join(
os.path.dirname(__file__), 'os-UBL-2.1',
'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd')
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')
]
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)
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'))
# end EdocTestCase

View file

@ -1,5 +1,5 @@
[tryton]
version=7.0.10
version=7.0.6
depends:
edocument_uncefact
party
@ -7,5 +7,4 @@ depends:
account_invoice
xml:
message.xml
configuration.xml
party.xml

View file

@ -1,15 +0,0 @@
<?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>