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/locale/en.po b/locale/en.po
index 95bfadd..e48d214 100644
--- a/locale/en.po
+++ b/locale/en.po
@@ -14,6 +14,10 @@ msgctxt "model:ir.message,text:msg_tax_code_missing"
msgid "The UNECE tax code is not configured for tax '%(taxname)s'."
msgstr "The UNECE tax code is not configured for tax '%(taxname)s'."
+msgctxt "model:ir.message,text:msg_tax_code_missing"
+msgid "The UNECE tax code is not configured for tax '%(taxname)s'."
+msgstr "The UNECE tax code is not configured for tax '%(taxname)s'."
+
msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'."
msgstr "The UNECE uom code is not configured for unit '%(uomname)s'."
@@ -118,3 +122,26 @@ msgctxt "field:edocument_xrechnung.bank_rel,config:"
msgid "Configuration"
msgstr "Configuration"
+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 b2ae93a..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.modules.tryton6_backport.i18n import gettext
-from cached_property import cached_property
+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):
@@ -100,21 +101,29 @@ class EdocumentMixin(object):
""" get tax of invoice-line,
fire exception if no/multiple taxes exists
"""
- if len(line.invoice_taxes) != 1:
+ 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.invoice_taxes)))
+ 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']
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
@@ -175,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 2a7a95f..39e6e3b 100644
--- a/template/Factur-X-1.07.2-extended/invoice.xml
+++ b/template/Factur-X-1.07.2-extended/invoice.xml
@@ -76,16 +76,14 @@ this repository contains the full copyright notices and license terms. -->
- ${this.invoice.currency.round(line.unit_price)}
+ ${this.round_unitprice(line.unit_price)}
${line.quantity * this.type_sign}
-
- ${TradeTax(tax.tax)}
-
+ ${TradeTax(this.invoice_line_tax(line))}
${line.amount}
diff --git a/tests/test_edocument.py b/tests/test_edocument.py
index 8c76e81..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_edocument_uncefact 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,44 +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.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):
@@ -145,44 +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.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):
@@ -190,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