Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a8541f73bd | ||
![]() |
f3375d3d39 | ||
![]() |
437f047e44 | ||
![]() |
2dab5b1b43 | ||
![]() |
4c2565e15e | ||
![]() |
81f6a3e4a8 | ||
![]() |
d065f74482 | ||
![]() |
214cbb086f | ||
![]() |
a5bf930e55 |
21 changed files with 252 additions and 105 deletions
|
@ -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
|
||||
|
|
41
README.rst
41
README.rst
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
79
mixin.py
79
mixin.py
|
@ -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
|
||||
|
|
26
party.py
26
party.py
|
@ -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
|
||||
|
|
8
setup.py
8
setup.py
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
tryton6_backport;5.0.3;5.0.999;mds
|
||||
edocument_uncefact;5.0.6;5.0.999;mds
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue