export xrechnung 2.1 ok + test

This commit is contained in:
Frederik Jaeckel 2022-10-19 17:15:56 +02:00
parent 7e35688043
commit 1306f2a339
6 changed files with 152 additions and 60 deletions

View file

@ -15,25 +15,88 @@ class Invoice(Invoice):
'EDocument XRechnung' 'EDocument XRechnung'
__name__ = 'edocument.xrechnung.invoice' __name__ = 'edocument.xrechnung.invoice'
def sales_order_nums(self):
""" get string of sale-numbers
"""
if getattr(self.invoice, 'sales', None) is not None:
return ', '.join([x.number for x in self.invoice.sales])
def prepaid_amount(self, invoice): def prepaid_amount(self, invoice):
""" compute already paid amount """ compute already paid amount
""" """
return invoice.total_amount - invoice.amount_to_pay return invoice.total_amount - invoice.amount_to_pay
def invoice_note(self):
""" get 'description' + 'comment'
"""
notes = []
if self.invoice.description:
notes.append(self.invoice.description)
if self.invoice.comment:
notes.extend(self.invoice.comment.split('\n'))
if len(notes) > 0:
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']
if not line.invoice_taxes[0].tax.unece_category_code 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): def tax_rate(self, tax):
""" get tax-rate in procent """ get tax-rate in procent
""" """
return (tax.tax.rate * Decimal('100.0')).quantize(Decimal('0.01')) 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_category_code(self, tax): def tax_category_code(self, tax):
""" read tax-category, fire exception if missing """ read tax-category, fire exception if missing
""" """
if len(tax.tax.unece_category_code or '') == 0: if len(tax.unece_category_code or '') == 0:
raise UserError(gettext( raise UserError(gettext(
'edocument_xrechnung.mds_tax_category_missing', 'edocument_xrechnung.mds_tax_category_missing',
taxname = tax.rec_name, taxname = tax.rec_name,
)) ))
return tax.tax.unece_category_code return tax.unece_category_code
def quote_text(self, text): def quote_text(self, text):
""" replace critical chars """ replace critical chars

View file

@ -14,6 +14,18 @@ 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_uom_code_missing"
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."
msgctxt "model:ir.message,text:msg_linetax_invalid_number"
msgid "The invoice line '%(linename)s' must have exactly one tax (number of taxes currently: %(numtax)d)."
msgstr "Die Rechnungszeile '%(linename)s' muß genau eine Steuer haben (Anzahl Steuern derzeit: %(numtax)d)."
msgctxt "model:ir.message,text:msg_linetax_invalid_catcode"
msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgstr "Ungültiger Kategoriecode an der Steuer '%(taxname)s' (erlaubt: %(allowed)s)."
####################### #######################
# party.configuration # # party.configuration #

View file

@ -11,6 +11,15 @@ 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_uom_code_missing">
<field name="text">The UNECE uom code is not configured for unit '%(uomname)s'.</field>
</record>
<record model="ir.message" id="msg_linetax_invalid_number">
<field name="text">The invoice line '%(linename)s' must have exactly one tax (number of taxes currently: %(numtax)d).</field>
</record>
<record model="ir.message" id="msg_linetax_invalid_catcode">
<field name="text">Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s).</field>
</record>
</data> </data>
</tryton> </tryton>

View file

@ -17,56 +17,88 @@
<cbc:IdentificationCode>${getattr(getattr(value, 'country', None), 'code', None)}</cbc:IdentificationCode> <cbc:IdentificationCode>${getattr(getattr(value, 'country', None), 'code', None)}</cbc:IdentificationCode>
</cac:Country> </cac:Country>
</py:def> </py:def>
<py:def function="PartyTaxScheme(value)"> <py:def function="PartyTaxScheme(value)">
<cbc:CompanyID>${getattr(value, 'code', None)}</cbc:CompanyID> <cbc:CompanyID>${this.taxident_data(value)['code']}</cbc:CompanyID>
<cac:TaxScheme> <cac:TaxScheme>
<cbc:ID>VAT</cbc:ID> <cbc:ID>${this.taxident_data(value)['id']}</cbc:ID>
</cac:TaxScheme> </cac:TaxScheme>
</py:def> </py:def>
<py:def function="Contact(value)"> <py:def function="Contact(value)">
<cbc:Name>${value.name}</cbc:Name> <cbc:Name>${value.name}</cbc:Name>
<cbc:Telephone>${value.phone}</cbc:Telephone> <cbc:Telephone>${value.phone}</cbc:Telephone>
<cbc:ElectronicMail>${value.email}</cbc:ElectronicMail> <cbc:ElectronicMail>${value.email}</cbc:ElectronicMail>
</py:def> </py:def>
<py:def function="PartyLegalEntity(value, company_id=True)"> <py:def function="PartyLegalEntity(value, company_id=True)">
<cbc:RegistrationName>${value.name}</cbc:RegistrationName> <cbc:RegistrationName>${value.name}</cbc:RegistrationName>
<cbc:CompanyID py:if="company_id">${getattr(this.seller_trade_tax_identifier, 'code', None)}</cbc:CompanyID> <cbc:CompanyID py:if="company_id">${this.taxident_data(this.seller_trade_tax_identifier)['code']}</cbc:CompanyID>
</py:def> </py:def>
<py:def function="PayeeFinancialAccount(value)"> <py:def function="PayeeFinancialAccount(value)">
<py:if test="len(value.bank_accounts)>0"> <py:if test="len(value.bank_accounts)>0">
<cbc:ID>${value.bank_accounts[0].numbers[0].number_compact}</cbc:ID> <cbc:ID>${value.bank_accounts[0].numbers[0].number_compact}</cbc:ID>
<cbc:Name>${value.name}</cbc:Name> <cbc:Name>${value.name}</cbc:Name>
</py:if> </py:if>
</py:def> </py:def>
<py:def function="TaxCategory(value, incl_reason=False)">
<cbc:ID>${this.tax_category_code(value)}</cbc:ID>
<cbc:Percent>${this.tax_rate(value)}</cbc:Percent>
<cbc:TaxExemptionReason
py:if="(this.tax_category_code(value) in ['E']) and (incl_reason==True)">${value.legal_notice or '-'}</cbc:TaxExemptionReason>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</py:def>
<py:def function="TaxSubTotal(value)"> <py:def function="TaxSubTotal(value)">
<cac:TaxSubtotal> <cac:TaxSubtotal>
<cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${value.base}</cbc:TaxableAmount> <cbc:TaxableAmount py:attrs="{'currencyID': value.currency.code}">${value.base}</cbc:TaxableAmount>
<cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:TaxAmount> <cbc:TaxAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:TaxAmount>
<cac:TaxCategory> <cac:TaxCategory>
<cbc:ID>${this.tax_category_code(value)}</cbc:ID> ${TaxCategory(value.tax, True)}
<cbc:Percent>${this.tax_rate(value)}</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory> </cac:TaxCategory>
</cac:TaxSubtotal> </cac:TaxSubtotal>
</py:def> </py:def>
<py:def function="InvoiceLine(value)">
<cac:InvoiceLine>
<cbc:ID>${value.id}</cbc:ID>
<cbc:InvoicedQuantity py:attrs="{'unitCode': this.uom_unece_code(value)}">${value.quantity}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount py:attrs="{'currencyID': value.currency.code}">${value.amount}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>${value.description or getattr(value.product, 'name', None)}</cbc:Name>
<cac:SellersItemIdentification py:if="getattr(value.product, 'code', None)">
<cbc:ID>${value.product.code}</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
${TaxCategory(this.invoice_line_tax(value))}
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount py:attrs="{'currencyID': value.currency.code}">${value.unit_price}</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</py:def>
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.1</cbc:CustomizationID> <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:ID>${this.invoice.number}</cbc:ID>
<cbc:IssueDate>${this.invoice.invoice_date.isoformat()}</cbc:IssueDate> <cbc:IssueDate>${this.invoice.invoice_date.isoformat()}</cbc:IssueDate>
<cbc:DueDate>${(getattr(this.invoice, 'payment_term_date', None) or this.invoice.invoice_date).isoformat()}</cbc:DueDate> <cbc:DueDate>${(getattr(this.invoice, 'payment_term_date', None) or this.invoice.invoice_date).isoformat()}</cbc:DueDate>
<cbc:InvoiceTypeCode>${this.type_code}</cbc:InvoiceTypeCode> <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> <cbc:DocumentCurrencyCode>${this.invoice.currency.code}</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>${this.invoice.party.get_xrechnung_route_id()}</cbc:BuyerReference> <cbc:BuyerReference>${this.invoice.party.get_xrechnung_route_id()}</cbc:BuyerReference>
<cac:OrderReference> <cac:OrderReference py:if="this.invoice.reference">
<cbc:ID>bestell-nr</cbc:ID> <cbc:ID>${this.invoice.reference}</cbc:ID>
<cbc:SalesOrderID>auftrags-nr</cbc:SalesOrderID> <cbc:SalesOrderID py:if="this.sales_order_nums()">${this.sales_order_nums()}</cbc:SalesOrderID>
</cac:OrderReference> </cac:OrderReference>
<cac:ContractDocumentReference> <cac:ContractDocumentReference py:if="False">
<cbc:ID>vertrags-nr</cbc:ID> <cbc:ID>vertrags-nr</cbc:ID>
</cac:ContractDocumentReference> </cac:ContractDocumentReference>
<cac:ProjectReference> <cac:ProjectReference py:if="False">
<cbc:ID>proj-referenz</cbc:ID> <cbc:ID>proj-referenz</cbc:ID>
</cac:ProjectReference> </cac:ProjectReference>
<cac:AccountingSupplierParty> <cac:AccountingSupplierParty>
@ -137,50 +169,11 @@
<cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.untaxed_amount}</cbc:TaxExclusiveAmount> <cbc:TaxExclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.untaxed_amount}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.total_amount}</cbc:TaxInclusiveAmount> <cbc:TaxInclusiveAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.total_amount}</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount py:if="False" py:attrs="{'currencyID': this.invoice.currency.code}">0.00</cbc:AllowanceTotalAmount> <cbc:AllowanceTotalAmount py:if="False" py:attrs="{'currencyID': this.invoice.currency.code}">0.00</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">0.00</cbc:ChargeTotalAmount> <cbc:ChargeTotalAmount py:if="False" py:attrs="{'currencyID': this.invoice.currency.code}">0.00</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.prepaid_amount(this.invoice)}</cbc:PrepaidAmount> <cbc:PrepaidAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.prepaid_amount(this.invoice)}</cbc:PrepaidAmount>
<cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.amount_to_pay}</cbc:PayableAmount> <cbc:PayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.amount_to_pay}</cbc:PayableAmount>
</cac:LegalMonetaryTotal> </cac:LegalMonetaryTotal>
<cac:InvoiceLine> <py:for each="line in this.lines">
<cbc:ID>1</cbc:ID> ${InvoiceLine(line)}
<cbc:InvoicedQuantity unitCode="C62">0.5</cbc:InvoicedQuantity> </py:for>
<cbc:LineExtensionAmount currencyID="EUR">35.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Reparaturdienstleistung in Stunden</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>REP-012</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.00</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">70.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>2</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">3</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">5.25</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Material</cbc:Name>
<cac:SellersItemIdentification>
<cbc:ID>MAT-987</cbc:ID>
</cac:SellersItemIdentification>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.00</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">1.75</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</ubl:Invoice> </ubl:Invoice>

View file

@ -7,6 +7,7 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool from trytond.pool import Pool
from trytond.modules.edocument_uncefact.tests.test_edocument_uncefact import get_invoice from trytond.modules.edocument_uncefact.tests.test_edocument_uncefact import get_invoice
from unittest.mock import Mock, MagicMock from unittest.mock import Mock, MagicMock
from decimal import Decimal
class EdocTestCase(ModuleTestCase): class EdocTestCase(ModuleTestCase):
@ -21,9 +22,23 @@ class EdocTestCase(ModuleTestCase):
Template = pool.get('edocument.xrechnung.invoice') Template = pool.get('edocument.xrechnung.invoice')
Address = pool.get('party.address') Address = pool.get('party.address')
Identifier = pool.get('party.identifier') Identifier = pool.get('party.identifier')
Party = pool.get('party.party')
Bank = pool.get('bank')
BankAccount = pool.get('bank.account')
BankNumber = pool.get('bank.account.number')
invoice = get_invoice() invoice = get_invoice()
invoice.party.get_xrechnung_route_id = Mock(return_value='xrechn-route-id-123') invoice.party.get_xrechnung_route_id = Mock(return_value='xrechn-route-id-123')
invoice.company.party.bank_accounts = [
Mock(spec=BankAccount,
currency=invoice.currency,
bank=Mock(spec=Bank, party=Mock(spec=Party, name='Bank')),
owners = [invoice.company.party],
numbers = [Mock(spec=BankNumber, type='other', number='123456')],
)]
invoice.description = 'description of invoice'
invoice.comment = 'note line 1\nnote line 2'
invoice.taxes[0].tax.rate = Decimal('0.1')
invoice.identifiers = [ invoice.identifiers = [
Mock(spec=Identifier, Mock(spec=Identifier,
type='edoc_route_id', type='edoc_route_id',

View file

@ -3,7 +3,7 @@ version=6.0.0
depends: depends:
edocument_uncefact edocument_uncefact
party party
#extras_depend: bank
account_invoice account_invoice
xml: xml:
message.xml message.xml