facturx: check unece codes at tax

This commit is contained in:
Frederik Jaeckel 2024-12-09 13:24:39 +01:00
parent 70f079a0dc
commit bdd0c36483
7 changed files with 154 additions and 118 deletions

View file

@ -4,13 +4,13 @@
# full copyright notices and license terms. # full copyright notices and license terms.
from trytond.pool import Pool from trytond.pool import Pool
from .edocument import Invoice, FacturX from .edocument import XRechnung, FacturX
from .party import PartyConfiguration, Party from .party import PartyConfiguration, Party
def register(): def register():
Pool.register( Pool.register(
Invoice, XRechnung,
FacturX, FacturX,
Party, Party,
PartyConfiguration, PartyConfiguration,

View file

@ -5,50 +5,12 @@
import genshi.template import genshi.template
import os import os
import html
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.edocument_uncefact.edocument import Invoice
from decimal import Decimal from decimal import Decimal
from trytond.modules.edocument_uncefact.edocument import Invoice
from .mixin import EdocumentMixin
class FacturX(Invoice): class XRechnung(EdocumentMixin, Invoice):
'Factur-X'
__name__ = 'edocument.facturxext.invoice'
def get_list_of_comments(self):
""" comment, to export in <ram:IncludedNote/>
Returns:
_type_: _description_
"""
result = []
if self.invoice.comment:
result.append({
'content': self.invoice.comment,
'subject_code': '',
'content_code': ''})
return result
def _get_template(self, version):
""" load our own template if 'version' is ours
"""
loader = genshi.template.TemplateLoader(
os.path.join(os.path.dirname(__file__), 'template'),
auto_reload=True)
if version == 'Factur-X-1.07.2-extended':
if self.type_code in ['380', '389', '381', '261']:
return loader.load(os.path.join(version, 'invoice.xml'))
else:
raise ValueError('invalid type-code "%s"' % self.type_code)
else:
return super(Invoice, self)._get_template(version)
# end FacturX
class Invoice(Invoice):
'EDocument XRechnung' 'EDocument XRechnung'
__name__ = 'edocument.xrechnung.invoice' __name__ = 'edocument.xrechnung.invoice'
@ -88,74 +50,6 @@ class Invoice(Invoice):
if notes: if notes:
return '; '.join(notes) return '; '.join(notes)
def invoice_line_tax(self, line):
""" get tax of invoice-line,
fire exception if no/multiple taxes exists
"""
if len(line.invoice_taxes) != 1:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_number',
linename=line.rec_name,
numtax=len(line.invoice_taxes)))
allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B']
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=line.invoice_taxes[0].tax.rec_name,
allowed=', '.join(allowed_cat)))
return line.invoice_taxes[0].tax
def taxident_data(self, tax_identifier):
""" get tax-scheme-id and codes
"""
result = {'code': None, 'id': None}
if tax_identifier:
if tax_identifier.type == 'de_vat':
result['code'] = 'DE%s' % tax_identifier.code
result['id'] = 'VAT'
return result
def tax_rate(self, tax):
""" get tax-rate in procent
"""
return (tax.rate * Decimal('100.0')).quantize(Decimal('0.01'))
def uom_unece_code(self, line):
""" 'line': invoice.line
"""
if len(line.unit.unece_code or '') == 0:
raise UserError(gettext(
'edocument_xrechnung.msg_uom_code_missing',
uomname=line.unit.rec_name))
return line.unit.unece_code
def get_category_code(self, tax):
while tax:
if tax.unece_category_code:
return tax.unece_category_code
break
tax = tax.parent
def tax_category_code(self, tax):
""" read tax-category, fire exception if missing
"""
unece_category_code = self.get_category_code(tax)
if not unece_category_code:
raise UserError(gettext(
'edocument_xrechnung.mds_tax_category_missing',
taxname=tax.rec_name))
return unece_category_code
def quote_text(self, text):
""" replace critical chars
"""
if text:
return html.quote(text)
def _get_template(self, version): def _get_template(self, version):
""" load our own template if 'version' is ours """ load our own template if 'version' is ours
""" """
@ -176,6 +70,28 @@ class Invoice(Invoice):
else: else:
raise ValueError('invalid type-code "%s"' % self.type_code) raise ValueError('invalid type-code "%s"' % self.type_code)
else: else:
return super(Invoice, self)._get_template(version) return super(XRechnung, self)._get_template(version)
# end Invoice # end XRechnung
class FacturX(EdocumentMixin, Invoice):
'Factur-X'
__name__ = 'edocument.facturxext.invoice'
def _get_template(self, version):
""" load our own template if 'version' is ours
"""
loader = genshi.template.TemplateLoader(
os.path.join(os.path.dirname(__file__), 'template'),
auto_reload=True)
if version == 'Factur-X-1.07.2-extended':
if self.type_code in ['380', '389', '381', '261']:
return loader.load(os.path.join(version, 'invoice.xml'))
else:
raise ValueError('invalid type-code "%s"' % self.type_code)
else:
return super(FacturX, self)._get_template(version)
# end FacturX

View file

@ -14,6 +14,10 @@ msgctxt "model:ir.message,text:mds_tax_category_missing"
msgid "The UNECE tax category is not configured for tax '%(taxname)s'." msgid "The UNECE tax category is not configured for tax '%(taxname)s'."
msgstr "Für die Steuer '%(taxname)s' ist die UNECE-Steuerkategorie nicht konfiguriert." msgstr "Für die Steuer '%(taxname)s' ist die UNECE-Steuerkategorie nicht konfiguriert."
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-Einheitencode nicht konfiguriert."
msgctxt "model:ir.message,text:msg_uom_code_missing" msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'." msgid "The UNECE uom code is not configured for unit '%(uomname)s'."
msgstr "Für die Einheit '%(uomname)s' ist der UNECE-Einheitencode nicht konfiguriert." msgstr "Für die Einheit '%(uomname)s' ist der UNECE-Einheitencode nicht konfiguriert."

View file

@ -10,6 +10,10 @@ msgctxt "model:ir.message,text:mds_tax_category_missing"
msgid "The UNECE tax category is not configured for tax '%(taxname)s'." msgid "The UNECE tax category is not configured for tax '%(taxname)s'."
msgstr "The UNECE tax category is not configured for tax '%(taxname)s'." msgstr "The UNECE tax category 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" msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'." msgid "The UNECE uom code is not configured for unit '%(uomname)s'."
msgstr "The UNECE uom code is not configured for unit '%(uomname)s'." msgstr "The UNECE uom code is not configured for unit '%(uomname)s'."

View file

@ -11,6 +11,9 @@ full copyright notices and license terms. -->
<record model="ir.message" id="mds_tax_category_missing"> <record model="ir.message" id="mds_tax_category_missing">
<field name="text">The UNECE tax category is not configured for tax '%(taxname)s'.</field> <field name="text">The UNECE tax category is not configured for tax '%(taxname)s'.</field>
</record> </record>
<record model="ir.message" id="msg_tax_code_missing">
<field name="text">The UNECE tax code is not configured for tax '%(taxname)s'.</field>
</record>
<record model="ir.message" id="msg_uom_code_missing"> <record model="ir.message" id="msg_uom_code_missing">
<field name="text">The UNECE uom code is not configured for unit '%(uomname)s'.</field> <field name="text">The UNECE uom code is not configured for unit '%(uomname)s'.</field>
</record> </record>

109
mixin.py Normal file
View file

@ -0,0 +1,109 @@
# -*- 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 decimal import Decimal
import html
from trytond.exceptions import UserError
from trytond.i18n import gettext
class EdocumentMixin(object):
""" functions to get field values for xml
"""
__slots__ = ()
def get_list_of_comments(self):
""" comment, to export in <ram:IncludedNote/>
Returns:
_type_: _description_
"""
result = []
if self.invoice.comment:
result.append({
'content': self.invoice.comment,
'subject_code': '',
'content_code': ''})
return result
def invoice_line_tax(self, line):
""" get tax of invoice-line,
fire exception if no/multiple taxes exists
"""
if len(line.invoice_taxes) != 1:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_number',
linename=line.rec_name,
numtax=len(line.invoice_taxes)))
allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B']
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=line.invoice_taxes[0].tax.rec_name,
allowed=', '.join(allowed_cat)))
return line.invoice_taxes[0].tax
def taxident_data(self, tax_identifier):
""" get tax-scheme-id and codes
"""
result = {'code': None, 'id': None}
if tax_identifier:
if tax_identifier.type == 'de_vat':
result['code'] = 'DE%s' % tax_identifier.code
result['id'] = 'VAT'
return result
def tax_rate(self, tax):
""" get tax-rate in procent
"""
return (tax.rate * Decimal('100.0')).quantize(Decimal('0.01'))
def uom_unece_code(self, line):
""" 'line': invoice.line
"""
if len(line.unit.unece_code or '') == 0:
raise UserError(gettext(
'edocument_xrechnung.msg_uom_code_missing',
uomname=line.unit.rec_name))
return line.unit.unece_code
def tax_unece_code(self, tax):
""" 'tax': invoice.line
"""
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_category_code(self, tax):
while tax:
if tax.unece_category_code:
return tax.unece_category_code
break
tax = tax.parent
def tax_category_code(self, tax):
""" read tax-category, fire exception if missing
"""
unece_category_code = self.get_category_code(tax)
if not unece_category_code:
raise UserError(gettext(
'edocument_xrechnung.mds_tax_category_missing',
taxname=tax.rec_name))
return unece_category_code
def quote_text(self, text):
""" replace critical chars
"""
if text:
return html.quote(text)
# end EdocumentMixin

View file

@ -37,10 +37,10 @@ this repository contains the full copyright notices and license terms. -->
<py:def function="TradeTax(tax, amount=None, base=None)"> <py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax> <ram:ApplicableTradeTax>
<ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${amount * this.type_sign}</ram:CalculatedAmount> <ram:CalculatedAmount py:if="amount" py:attrs="{'currencyID': this.invoice.currency.code}">${amount * this.type_sign}</ram:CalculatedAmount>
<ram:TypeCode py:if="tax.unece_code">${tax.unece_code}</ram:TypeCode> <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">${tax.legal_notice}</ram:ExemptionReason>
<ram:BasisAmount py:if="base">${base * this.type_sign}</ram:BasisAmount> <ram:BasisAmount py:if="base">${base * this.type_sign}</ram:BasisAmount>
<ram:CategoryCode py:if="tax.unece_category_code">${tax.unece_category_code}</ram:CategoryCode> <ram:CategoryCode>${this.tax_category_code(tax)}</ram:CategoryCode>
<ram:RateApplicablePercent py:if="tax.type == 'percentage'">${tax.rate * 100}</ram:RateApplicablePercent> <ram:RateApplicablePercent py:if="tax.type == 'percentage'">${tax.rate * 100}</ram:RateApplicablePercent>
</ram:ApplicableTradeTax> </ram:ApplicableTradeTax>
</py:def> </py:def>
@ -69,9 +69,9 @@ this repository contains the full copyright notices and license terms. -->
<ram:AssociatedDocumentLineDocument> <ram:AssociatedDocumentLineDocument>
<ram:LineID>${line_id}</ram:LineID> <ram:LineID>${line_id}</ram:LineID>
</ram:AssociatedDocumentLineDocument> </ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct py:if="line.product"> <ram:SpecifiedTradeProduct>
<ram:ID py:if="line.product.code">${line.product.code}</ram:ID> <ram:ID py:if="line.product and line.product.code">${line.product.code}</ram:ID>
<ram:Name>${line.product.name}</ram:Name> <ram:Name>${line.product.name if line.product else ''}</ram:Name>
<ram:Description py:if="line.description">${line.description}</ram:Description> <ram:Description py:if="line.description">${line.description}</ram:Description>
</ram:SpecifiedTradeProduct> </ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement> <ram:SpecifiedLineTradeAgreement>