Merge branch 'main' into 7.0

This commit is contained in:
Frederik Jaeckel 2024-12-09 15:42:33 +01:00
commit 4881504b2e
7 changed files with 234 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."
@ -26,6 +30,18 @@ msgctxt "model:ir.message,text:msg_linetax_invalid_catcode"
msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgstr "Ungültiger Kategoriecode an der Steuer '%(taxname)s' (erlaubt: %(allowed)s)." msgstr "Ungültiger Kategoriecode an der Steuer '%(taxname)s' (erlaubt: %(allowed)s)."
msgctxt "model:ir.message,text:msg_no_seller_address"
msgid "There is no address for the seller party '%(sellerparty)s'."
msgstr "Für die Verkäuferpartei '%(sellerparty)s' existiert keine Adresse."
msgctxt "model:ir.message,text:msg_no_buyer_address"
msgid "There is no address for the buyer party '%(buyerparty)s'."
msgstr "Für die Käuferpartei '%(sellerparty)s' existiert keine Adresse."
msgctxt "model:ir.message,text:msg_no_address_country"
msgid "No country is specified for the address of the party '%(party)s'."
msgstr "Für die Adresse der Partei '%(party)s' ist kein Land festgelegt."
####################### #######################
# party.configuration # # party.configuration #

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'."
@ -22,6 +26,18 @@ msgctxt "model:ir.message,text:msg_linetax_invalid_catcode"
msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgstr "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgstr "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgctxt "model:ir.message,text:msg_no_seller_address"
msgid "There is no address for the seller party '%(sellerparty)s'."
msgstr "There is no address for the seller party '%(sellerparty)s'."
msgctxt "model:ir.message,text:msg_no_buyer_address"
msgid "There is no address for the buyer party '%(buyerparty)s'."
msgstr "There is no address for the buyer party '%(buyerparty)s'."
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.configuration,identifier_types:" msgctxt "selection:party.configuration,identifier_types:"
msgid "X-Rechnung Route-ID" msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Route-ID" msgstr "X-Rechnung Route-ID"

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>
@ -20,6 +23,15 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_linetax_invalid_catcode"> <record model="ir.message" id="msg_linetax_invalid_catcode">
<field name="text">Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s).</field> <field name="text">Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s).</field>
</record> </record>
<record model="ir.message" id="msg_no_seller_address">
<field name="text">There is no address for the seller party '%(sellerparty)s'.</field>
</record>
<record model="ir.message" id="msg_no_buyer_address">
<field name="text">There is no address for the buyer party '%(buyerparty)s'.</field>
</record>
<record model="ir.message" id="msg_no_address_country">
<field name="text">No country is specified for the address of the party '%(party)s'.</field>
</record>
</data> </data>
</tryton> </tryton>

156
mixin.py Normal file
View file

@ -0,0 +1,156 @@
# -*- 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
from trytond.tools import cached_property
class EdocumentMixin(object):
""" functions to get field values for xml
"""
__slots__ = ()
@cached_property
def seller_trade_address(self):
""" get address of seller, throw exception if incomplete
Raises:
UserError: if no address
UserError: if no country on address
Returns:
record : model party.address
"""
result = super(EdocumentMixin, self).seller_trade_address
if not result:
raise UserError(gettext(
'edocument_xrechnung.msg_no_seller_address',
sellerparty=self.invoice.company.rec_name
if self.invoice and self.invoice.company
else '-'))
if not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party))
return result
@cached_property
def buyer_trade_address(self):
""" exception if no address
Returns:
record: model party.address
"""
if (self.invoice.type == 'out') and (
not self.invoice.invoice_address):
raise UserError(gettext(
'edocument_xrechnung.msg_no_buyer_address',
buyerparty=self.invoice.party.rec_name
if self.invoice and self.invoice.party
else '-'))
result = super(EdocumentMixin, self).buyer_trade_address
if result and not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party))
return result
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>