diff --git a/docs/xrechnung.txt b/docs/xrechnung.txt
index f8fbbbd..dab13fa 100644
--- a/docs/xrechnung.txt
+++ b/docs/xrechnung.txt
@@ -12,3 +12,5 @@ https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingp
https://erechnungsvalidator.service-bw.de/
https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/
+
+https://www.e-rechnungs-checker.de/
diff --git a/mixin.py b/mixin.py
index c854b7d..1ffdd6f 100644
--- a/mixin.py
+++ b/mixin.py
@@ -10,6 +10,7 @@ from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.tools import cached_property
from trytond.pool import Pool
+from trytond.transaction import Transaction
from trytond.modules.product import round_price
@@ -160,6 +161,41 @@ class EdocumentMixin(object):
taxname=tax.rec_name))
return tax.unece_code
+ def get_line_amount(self, line):
+ """ get amount of current invoice-line,
+ depends on modegross of invoice, set used-modegross to 'net'
+
+ Args:
+ line (record): model account.invoice.line
+ """
+ if line.modegross == 'net':
+ return line.amount
+ elif line.modegross == 'gross':
+ # get net-amount
+ # copy from account_invoice/invoice.py:2416-2434
+ currency = (
+ line.invoice.currency
+ if line.invoice else line.currency)
+
+ amount = (Decimal(str(line.quantity or 0)) * (
+ line.unit_price or Decimal(0)))
+ invoice_type = (
+ line.invoice.type
+ if line.invoice else line.invoice_type)
+
+ if (invoice_type == 'in'
+ and line.taxes_deductible_rate is not None
+ and line.taxes_deductible_rate != 1):
+ with Transaction().set_context(_deductible_rate=1):
+ tax_amount = sum(
+ t['amount'] for t in line._get_taxes().values())
+ non_deductible_amount = (
+ tax_amount * (1 - line.taxes_deductible_rate))
+ amount += non_deductible_amount
+ if currency:
+ return currency.round(amount)
+ return amount
+
def get_tax_unece_code(self, tax):
while tax:
if tax.unece_code:
@@ -203,4 +239,30 @@ class EdocumentMixin(object):
if text:
return html.escape(text)
+ def _party_legal_types(self):
+ """ get list of identifier-types to be used as
+ legal-ids
+ """
+ return ['de_handelsregisternummer']
+
+ def party_legal_ids(self, party, address):
+ """ get list of legal-ids of party
+
+ Args:
+ party (record): model party.party
+ address (record): model party.address
+ """
+ result = super().party_legal_ids(party, address)
+
+ legal_types = self._party_legal_types()
+ if party and party.identifiers:
+ for x in party.identifiers:
+ if x.type in legal_types:
+ if x.address:
+ if x.address == address:
+ result.append((x.rec_name, {'schemeID': '0002'}))
+ else:
+ result.append((x.rec_name, {'schemeID': '0002'}))
+ return result
+
# 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 db85eb1..a4620c5 100644
--- a/template/Factur-X-1.07.2-extended/invoice.xml
+++ b/template/Factur-X-1.07.2-extended/invoice.xml
@@ -19,7 +19,7 @@ this repository contains the full copyright notices and license terms. -->
${TradeAddress(address)}
-
+
${tax_identifier.code}
@@ -38,7 +38,7 @@ this repository contains the full copyright notices and license terms. -->
${amount * this.type_sign}
${this.tax_unece_code(tax)}
- ${tax.legal_notice}
+ ${tax.legal_notice}
${base * this.type_sign}
${this.tax_category_code(tax)}
${tax.rate * 100}
@@ -71,8 +71,8 @@ this repository contains the full copyright notices and license terms. -->
${line.product.code}
- ${this.quote_text(line.product.name if line.product else '')}
- ${this.quote_text(line.description)}
+ ${this.quote_text(line.product.name if line.product else line.description if line.description else 'name not set')}
+ ${this.quote_text(line.description if line.product else '')}
@@ -85,7 +85,7 @@ this repository contains the full copyright notices and license terms. -->
${TradeTax(this.invoice_line_tax(line))}
- ${line.amount}
+ ${this.get_line_amount(line)}
diff --git a/tests/test_edocument.py b/tests/test_edocument.py
index 5ba67ac..179957d 100644
--- a/tests/test_edocument.py
+++ b/tests/test_edocument.py
@@ -7,7 +7,8 @@ from lxml import etree
import os
from decimal import Decimal
from datetime import date
-from trytond.tests.test_tryton import ModuleTestCase, with_transaction
+from trytond.tests.test_tryton import (
+ ModuleTestCase, with_transaction, activate_module)
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
@@ -42,6 +43,14 @@ class EdocTestCase(ModuleTestCase):
'Test e-rechnung module'
module = 'edocument_xrechnung'
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ activate_module([
+ 'edocument_uncefact', 'party', 'bank',
+ 'account_invoice', 'sale_point_invoice',
+ 'product_grossprice'], 'en')
+
def prep_fiscalyear(self, company1):
""" prepare fiscal year, sequences...
"""
@@ -69,6 +78,11 @@ class EdocTestCase(ModuleTestCase):
company = create_company('m-ds')
Party.write(*[[company.party], {
+ 'identifiers': [('create', [
+ # post.de
+ {'type': 'de_handelsregisternummer', 'code': 'Bonn HRB 6792'},
+ {'type': 'de_vat', 'code': 'DE 169838187'},
+ ])],
'addresses': [('write', [company.party.addresses[0]], {
'country': country_de.id})]}])
@@ -84,7 +98,7 @@ class EdocTestCase(ModuleTestCase):
'number': 'DE02300209000106531065'}])]}])
return company
- def prep_invoice(self, credit_note=False):
+ def prep_invoice(self, credit_note=False, modegross='net'):
""" add invoice
"""
pool = Pool()
@@ -112,11 +126,12 @@ class EdocTestCase(ModuleTestCase):
}])
currency1, = Currency.search([('code', '=', 'usd')])
+ Currency.write(*[[currency1], {'code': 'USD'}])
tax, = Taxes.search([('name', '=', '20% VAT')])
Taxes.write(*[
[tax],
- {'unece_code': 'GST', 'unece_category_code': 'S',
+ {'unece_code': 'VAT', 'unece_category_code': 'S',
'legal_notice': 'Legal Notice'}])
account_lst = Account.search([
@@ -130,6 +145,7 @@ class EdocTestCase(ModuleTestCase):
to_create_invoice = [{
'type': 'out',
+ 'modegross': modegross,
'description': 'description of invoice',
'comment': 'note line 1\nnote line 2',
'invoice_date': date(2024, 7, 1),
@@ -149,12 +165,17 @@ class EdocTestCase(ModuleTestCase):
'currency': currency1.id,
}])],
}]
+
+ if modegross == 'gross':
+ to_create_invoice[0]['lines'][0][1][0]['unit_gross_price'] = (
+ Decimal('50.0') * Decimal('1.2'))
+
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(inv_lst.currency.code, 'USD')
self.assertEqual(len(inv_lst.move.lines), 3)
return inv_lst
@@ -233,6 +254,69 @@ class EdocTestCase(ModuleTestCase):
{'identifiers': [('delete', [party.identifiers[0].id])]}]
)
+ @with_transaction()
+ def test_xrechn_export_facturx_gross(self):
+ """ run export - factur-x, modegross='gross'
+ """
+ pool = Pool()
+ Template = pool.get('edocument.facturxext.invoice')
+
+ company = self.prep_company()
+ with set_company(company):
+ create_chart(company=company, tax=True)
+ self.prep_fiscalyear(company)
+ invoice = self.prep_invoice(modegross='gross')
+
+ 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')
+
+ invoice_string = template.render('Factur-X-1.07.2-extended')
+ with open('gross_invoice_string.xml', 'wb') as fhdl:
+ fhdl.write(invoice_string)
+
+ invoice_xml = etree.fromstring(invoice_string)
+
+ # check values in xml
+ nodes = invoice_xml.xpath(self._readxml_xpath([
+ 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
+ 'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty',
+ 'ram:SpecifiedLegalOrganization', 'ram:ID']),
+ namespaces=invoice_xml.nsmap)
+ self.assertEqual(nodes[0].text, 'Bonn HRB 6792')
+
+ nodes = invoice_xml.xpath(self._readxml_xpath([
+ 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
+ 'ram:IncludedSupplyChainTradeLineItem',
+ 'ram:SpecifiedLineTradeSettlement',
+ 'ram:SpecifiedTradeSettlementLineMonetarySummation',
+ 'ram:LineTotalAmount']),
+ namespaces=invoice_xml.nsmap)
+ self.assertEqual(nodes[0].text, '100.00')
+
+ schema = etree.XMLSchema(etree.parse(schema_file))
+ schema.assertValid(invoice_xml)
+
+ def _readxml_xpath(self, tags):
+ """ generate xpath
+
+ Args:
+ tags (list): list of string or integer to build path
+ """
+ parts = []
+ for x in tags:
+ if isinstance(x, str):
+ parts.append(x)
+ elif isinstance(x, int):
+ if parts[-1].endswith(']'):
+ raise ValueError('multiple list selector')
+ parts[-1] += '[%d]' % x
+ result = '/' + '/'.join(parts)
+ return result
+
@with_transaction()
def test_xrechn_export_facturx(self):
""" run export - factur-x
@@ -248,6 +332,10 @@ class EdocTestCase(ModuleTestCase):
template = Template(invoice)
+ self.assertEqual(
+ template.party_legal_ids(invoice.company_party, None),
+ [('Bonn HRB 6792', {'schemeID': '0002'})])
+
schema_file = os.path.join(
os.path.dirname(__file__),
'Factur-X_1.07.2_EXTENDED',
@@ -255,6 +343,15 @@ class EdocTestCase(ModuleTestCase):
invoice_string = template.render('Factur-X-1.07.2-extended')
invoice_xml = etree.fromstring(invoice_string)
+
+ # check values in xml
+ nodes = invoice_xml.xpath(self._readxml_xpath([
+ 'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
+ 'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty',
+ 'ram:SpecifiedLegalOrganization', 'ram:ID']),
+ namespaces=invoice_xml.nsmap)
+ self.assertEqual(nodes[0].text, 'Bonn HRB 6792')
+
schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml)
diff --git a/tryton.cfg b/tryton.cfg
index 62277bd..42646e1 100644
--- a/tryton.cfg
+++ b/tryton.cfg
@@ -5,6 +5,9 @@ depends:
party
bank
account_invoice
+extras_depend:
+ sale_point_invoice
+ product_grossprice
xml:
message.xml
configuration.xml