Compare commits

..

9 commits
5.0 ... main

Author SHA1 Message Date
Frederik Jaeckel
a8541f73bd dont except if no 'modegross' on invoice.line 2025-07-02 13:51:57 +02:00
Frederik Jaeckel
f3375d3d39 allow any vat-type to be exported as 'SpecifiedTaxRegistration' 2025-06-11 15:57:35 +02:00
Frederik Jaeckel
437f047e44 test: fix tax-code 2025-06-11 12:14:05 +02:00
Frederik Jaeckel
2dab5b1b43 SpecifiedTradeProduct: export 'description' as 'name' if no product was used in invoice-line 2025-06-11 11:57:16 +02:00
Frederik Jaeckel
4c2565e15e add xml-validator 2025-06-11 11:48:55 +02:00
Frederik Jaeckel
81f6a3e4a8 ApplicableTradeTax: disable ExemptionReason if CategoryCode exist 2025-06-11 11:48:35 +02:00
Frederik Jaeckel
d065f74482 add legal id of seller 2025-06-11 11:46:53 +02:00
Frederik Jaeckel
214cbb086f get amount on line as net-value (even if invoice is gross-mode) 2025-06-10 13:45:07 +02:00
Frederik Jaeckel
a5bf930e55 update license 2025-05-02 14:16:47 +02:00
21 changed files with 252 additions and 105 deletions

View file

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

View file

@ -9,38 +9,37 @@ pip install mds-edocument-xrechnung
Requires
========
- Tryton 5.0
- Tryton 7.0
Changes
=======
- handle tax childs (Jan Grasnick <jan@mittelwind.de>)
- fix: rounding of unit_price at invoice-line
*5.0.14 - 29.01.2025*
- updt: optimize rounding of unit_price
*5.0.11 - 19.12.2024*
- Lookup parent taxes for unece tax codes.
- Improve the help text of xrechnung_routeid. Correct a translation.
(Mathias Behrle <mathiasb@m9s.biz>)
*5.0.10 - 17.12.2024*
*7.0.10 - 12.12.2024*
- fix missing views
- Remove arguments in super() calls. (Mathias Behrle)
*5.0.9 - 11.12.2024*
*7.0.9 - 11.12.2024*
- fix name of party in exceptions
*5.0.8 - 11.12.2024*
*7.0.7 - 10.12.2024*
- fix setup
- add iban to xml-export
*5.0.7 - 10.12.2024*
*7.0.6 - 09.12.2024*
- compatibility to Tryton 5.0
- add: export of factur-x 1.07.2, XRechnung 2.3 + 3.0
- add: check for valid data to generate xml
*7.0.5 - 05.12.2024*
- add: export Factur-X 1.07.2
*7.0.4 - 05.12.2024*
- add: export XRechnung 2.3 + 3.0
- updt: xrechnung-route-id optional
*7.0.3 - 22.12.2023*
- compatibility to Tryton 7.0

View file

@ -6,7 +6,7 @@
from trytond.pool import Pool
from .edocument import XRechnung, FacturX
from .bank import AccountNumber
from .party import Party, PartyIdentifier
from .party import PartyConfiguration, Party
from .configuration import Configuration, BankEdocumentRel
@ -18,5 +18,5 @@ def register():
BankEdocumentRel,
FacturX,
Party,
PartyIdentifier,
PartyConfiguration,
module='edocument_xrechnung', type_='model')

View file

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

View file

@ -46,7 +46,7 @@ msgstr "Für die Adresse der Partei '%(party)s' ist kein Land festgelegt."
#######################
# party.configuration #
#######################
msgctxt "selection:party.identifier,type:"
msgctxt "selection:party.configuration,identifier_types:"
msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Leitweg-ID"

View file

@ -38,7 +38,7 @@ msgctxt "model:ir.message,text:msg_no_address_country"
msgid "No country is specified for the address of the party '%(party)s'."
msgstr "No country is specified for the address of the party '%(party)s'."
msgctxt "selection:party.identifier,type:"
msgctxt "selection:party.configuration,identifier_types:"
msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Route-ID"
@ -50,6 +50,10 @@ msgctxt "field:party.party,xrechnung_routeid:"
msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Route-ID"
msgctxt "help:party.party,xrechnung_routeid:"
msgid "Enables the need for an XRechnung route ID at the party for exporting the XRechnung."
msgstr "Enables the need for an XRechnung route ID at the party for exporting the XRechnung."
msgctxt "field:account.tax,xrtax_category:"
msgid "Tax Category"
msgstr "Tax Category"

View file

@ -7,18 +7,11 @@
from decimal import Decimal
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
def round_price(value, rounding=None):
"Round price using the price digits"
if isinstance(value, int):
return Decimal(value)
return value.quantize(
Decimal(1) / 10 ** price_digits[1], rounding=rounding)
from trytond.transaction import Transaction
from trytond.modules.product import round_price
class EdocumentMixin(object):
@ -168,6 +161,44 @@ 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 not hasattr(line, 'modegross'):
return line.amount
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:
@ -211,4 +242,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

View file

@ -5,22 +5,10 @@
from trytond.pool import PoolMeta
from trytond.exceptions import UserError
from trytond.modules.tryton6_backport.i18n import gettext
from trytond.i18n import gettext
from trytond.model import fields
class PartyIdentifier(metaclass=PoolMeta):
__name__ = 'party.identifier'
@classmethod
def __setup__(cls):
super(PartyIdentifier, cls).__setup__()
cls.type.selection.append(
('edoc_route_id', 'X-Rechnung Route-ID'))
# end PartyIdentifier
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
@ -64,3 +52,15 @@ class Party(metaclass=PoolMeta):
record.get_xrechnung_route_id()
# end Party
class PartyConfiguration(metaclass=PoolMeta):
__name__ = 'party.configuration'
@classmethod
def __setup__(cls):
super().__setup__()
cls.identifier_types.selection.append(
('edoc_route_id', 'X-Rechnung Route-ID'))
# end Configuration

View file

@ -13,11 +13,9 @@ 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'))
@ -36,7 +34,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
modversion[l2[0]] = {'min': l2[1], 'max': l2[2], 'prefix': l2[3]}
# tryton-version
major_version = 5
major_version = 7
minor_version = 0
requires = []
@ -84,11 +82,11 @@ setup(
'Natural Language :: English',
'Operating System :: OS Independent',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'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',

View file

@ -19,12 +19,12 @@ this repository contains the full copyright notices and license terms. -->
</py:for>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress py:if="address">${TradeAddress(address)}</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration py:if="tax_identifier and tax_identifier.type == 'eu_vat'">
<ram:SpecifiedTaxRegistration py:if="tax_identifier and tax_identifier.type.endswith('_vat')">
<ram:ID schemeID='VA'>${tax_identifier.code}</ram:ID>
</ram:SpecifiedTaxRegistration>
</py:def>
<py:def function="TradeAddress(address)">
<ram:PostcodeCode py:if="address.zip">${address.zip}</ram:PostcodeCode>
<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>
@ -38,7 +38,7 @@ this repository contains the full copyright notices and license terms. -->
<ram:ApplicableTradeTax>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${amount * this.type_sign}</ram:CalculatedAmount>
<ram:TypeCode>${this.tax_unece_code(tax)}</ram:TypeCode>
<ram:ExemptionReason py:if="tax.legal_notice">${tax.legal_notice}</ram:ExemptionReason>
<ram:ExemptionReason py:if="tax.legal_notice and not this.tax_category_code(tax)">${tax.legal_notice}</ram:ExemptionReason>
<ram:BasisAmount py:if="base">${base * this.type_sign}</ram:BasisAmount>
<ram:CategoryCode>${this.tax_category_code(tax)}</ram:CategoryCode>
<ram:RateApplicablePercent py:if="tax.type == 'percentage'">${tax.rate * 100}</ram:RateApplicablePercent>
@ -71,8 +71,8 @@ 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>${this.quote_text(line.product.name if line.product else line.description if line.description else 'name not set')}</ram:Name>
<ram:Description py:if="line.description">${this.quote_text(line.description if line.product else '')}</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
@ -85,7 +85,7 @@ this repository contains the full copyright notices and license terms. -->
<ram:SpecifiedLineTradeSettlement>
${TradeTax(this.invoice_line_tax(line))}
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${line.amount}</ram:LineTotalAmount>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.get_line_amount(line)}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.amount}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>
@ -90,7 +90,7 @@
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.1</cbc:CustomizationID>
<cbc:ID>${this.invoice.number}</cbc:ID>
<cbc:IssueDate>${this.invoice.invoice_date.isoformat()}</cbc:IssueDate>
<cbc:DueDate>${this.invoice.invoice_date.isoformat()}</cbc:DueDate>
<cbc:DueDate>${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()}</cbc:DueDate>
<cbc:InvoiceTypeCode>${this.type_code}</cbc:InvoiceTypeCode>
<cbc:Note py:if="this.invoice_note()">${this.invoice_note()}</cbc:Note>
<cbc:DocumentCurrencyCode>${this.invoice.currency.code}</cbc:DocumentCurrencyCode>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.amount}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>
@ -90,7 +90,7 @@
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3</cbc:CustomizationID>
<cbc:ID>${this.invoice.number}</cbc:ID>
<cbc:IssueDate>${this.invoice.invoice_date.isoformat()}</cbc:IssueDate>
<cbc:DueDate>${this.invoice.invoice_date.isoformat()}</cbc:DueDate>
<cbc:DueDate>${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()}</cbc:DueDate>
<cbc:InvoiceTypeCode>${this.type_code}</cbc:InvoiceTypeCode>
<cbc:Note py:if="this.invoice_note()">${this.invoice_note()}</cbc:Note>
<cbc:DocumentCurrencyCode>${this.invoice.currency.code}</cbc:DocumentCurrencyCode>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.base)}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${this.negate_amount(value.amount)}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>

View file

@ -59,8 +59,8 @@
<py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.invoice.currency.code}">${value.amount}</cbc:TaxAmount>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:TaxAmount>
<cac:TaxCategory>
${TaxCategory(value.tax, True)}
</cac:TaxCategory>
@ -90,7 +90,7 @@
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0</cbc:CustomizationID>
<cbc:ID>${this.invoice.number}</cbc:ID>
<cbc:IssueDate>${this.invoice.invoice_date.isoformat()}</cbc:IssueDate>
<cbc:DueDate>${this.invoice.invoice_date.isoformat()}</cbc:DueDate>
<cbc:DueDate>${(this.invoice.payment_term_date or this.invoice.invoice_date).isoformat()}</cbc:DueDate>
<cbc:InvoiceTypeCode>${this.type_code}</cbc:InvoiceTypeCode>
<cbc:Note py:if="this.invoice_note()">${this.invoice_note()}</cbc:Note>
<cbc:DocumentCurrencyCode>${this.invoice.currency.code}</cbc:DocumentCurrencyCode>

View file

@ -2,17 +2,3 @@
# 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.
import trytond.tests.test_tryton
import unittest
from .test_edocument import EdocTestCase
__all__ = ['suite']
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(
EdocTestCase))
return suite

View file

@ -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
@ -17,10 +18,15 @@ 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, code='account.invoice')
sequence.company = fiscalyear.company
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()
@ -37,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...
"""
@ -64,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})]}])
@ -79,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()
@ -100,18 +119,19 @@ class EdocTestCase(ModuleTestCase):
'addresses': [('create', [{
'invoice': True,
'street': 'Customer Street 1',
'zip': '12345',
'postal_code': '12345',
'city': 'Usertown',
'country': country_de.id,
}])],
}])
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([
@ -125,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),
@ -144,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
@ -228,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
@ -243,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',
@ -250,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)

View file

@ -1,11 +1,13 @@
[tryton]
version=5.0.14
version=7.0.10
depends:
edocument_uncefact
party
bank
account_invoice
tryton6_backport
extras_depend:
sale_point_invoice
product_grossprice
xml:
message.xml
configuration.xml

View file

@ -1,2 +1 @@
tryton6_backport;5.0.3;5.0.999;mds
edocument_uncefact;5.0.6;5.0.999;mds

View file

@ -4,7 +4,7 @@
full copyright notices and license terms. -->
<data>
<xpath expr="/form/separator[@id='invoice']" position="before">
<xpath expr="/form/separator[@id='currency_exchange']" position="before">
<separator id="edocument" colspan="4" string="eDocument"/>
<field name="edocument_bank" colspan="2" height="200"/>