export: add factur-x 1.07.2

This commit is contained in:
Frederik Jaeckel 2024-12-05 15:36:07 +01:00
parent fc29a63969
commit bdc98f25fb
5 changed files with 259 additions and 10 deletions

View file

@ -4,13 +4,14 @@
# full copyright notices and license terms.
from trytond.pool import Pool
from .edocument import Invoice
from .edocument import Invoice, FacturX
from .party import PartyConfiguration, Party
def register():
Pool.register(
Invoice,
FacturX,
Party,
PartyConfiguration,
module='edocument_xrechnung', type_='model')

View file

@ -12,6 +12,42 @@ from trytond.modules.edocument_uncefact.edocument import Invoice
from decimal import Decimal
class FacturX(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'
__name__ = 'edocument.xrechnung.invoice'
@ -123,16 +159,20 @@ class Invoice(Invoice):
def _get_template(self, version):
""" load our own template if 'version' is ours
"""
if version in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']:
loader = genshi.template.TemplateLoader(
os.path.join(os.path.dirname(__file__), 'template'),
auto_reload=True)
if self.type_code in ['380', '389']:
return loader.load(os.path.join(
version, 'XRechnung_invoice.xml'))
elif self.type_code in ['381', '261']:
return loader.load(os.path.join(
version, 'XRechnung_credit.xml'))
if version in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']:
file_name = {
'380': 'XRechnung_invoice.xml',
'389': 'XRechnung_invoice.xml',
'381': 'XRechnung_credit.xml',
'261': 'XRechnung_credit.xml',
}.get(self.type_code)
if file_name:
return loader.load(os.path.join(version, file_name))
else:
raise ValueError('invalid type-code "%s"' % self.type_code)
else:

View file

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<py:def function="DateTime(value)">
<udt:DateTimeString format="102">${value.strftime('%Y%m%d')}</udt:DateTimeString>
</py:def>
<py:def function="TradeParty(party, address=None, tax_identifier=None)">
<ram:Name>${party.name}</ram:Name>
<ram:SpecifiedLegalOrganization>
<py:for each="id, attrs in this.party_legal_ids(party, address)">
<ram:ID py:attrs="attrs">${id}</ram:ID>
</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:ID schemeID='VA'>${tax_identifier.code}</ram:ID>
</ram:SpecifiedTaxRegistration>
</py:def>
<py:def function="TradeAddress(address)">
<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">${lines[0]}</ram:LineOne>
<ram:LineTwo py:if="len(lines) > 1">${lines[1]}</ram:LineTwo>
<ram:LineThree py:if="len(lines) > 2">${lines[2]}</ram:LineThree>
</py:with>
<ram:CityName py:if="address.city">${address.city}</ram:CityName>
<ram:CountryID py:if="address.country">${address.country.code}</ram:CountryID>
<ram:CountrySubDivisionName py:if="address.subdivision">${address.subdivision.name}</ram:CountrySubDivisionName>
</py:def>
<py:def function="TradeTax(tax, amount=None, base=None)">
<ram:ApplicableTradeTax>
<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:ExemptionReason py:if="tax.legal_notice">${tax.legal_notice}</ram:ExemptionReason>
<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:RateApplicablePercent py:if="tax.type == 'percentage'">${tax.rate * 100}</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
</py:def>
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>${this.invoice.number}</ram:ID>
<ram:Name py:if="this.invoice.description">${this.invoice.description}</ram:Name>
<ram:TypeCode>${this.type_code}</ram:TypeCode>
<ram:IssueDateTime>
${DateTime(this.invoice.invoice_date)}
</ram:IssueDateTime>
<py:for each="comment_line in this.get_list_of_comments()">
<ram:IncludedNote>
<ram:ContentCode py:if="comment_line.get('content_code')">${comment_line['content_code']}</ram:ContentCode>
<ram:Content py:if="comment_line.get('content')">${comment_line['content']}</ram:Content>
<ram:SubjectCode py:if="comment_line.get('subject_content')">${comment_line['subject_content']}</ram:SubjectCode>
</ram:IncludedNote>
</py:for>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem py:for="line_id, line in enumerate(this.lines, 1)">
<ram:AssociatedDocumentLineDocument>
<ram:LineID>${line_id}</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct py:if="line.product">
<ram:ID py:if="line.product.code">${line.product.code}</ram:ID>
<ram:Name>${line.product.name}</ram:Name>
<ram:Description py:if="line.description">${line.description}</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.currency.round(line.unit_price)}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity py:attrs="{'unitCode': line.unit.unece_code} if line.unit and line.unit.unece_code else {}">${line.quantity * this.type_sign}</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<py:for each="tax in line.invoice_taxes">
${TradeTax(tax.tax)}
</py:for>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${line.amount}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
${TradeParty(this.seller_trade_party, this.seller_trade_address, this.seller_trade_tax_identifier)}
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)}
</ram:BuyerTradeParty>
<ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'">
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:SellerOrderReferencedDocument>
<ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'">
<ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
<ram:ShipToTradeParty py:if="this.ship_to_trade_party">
${TradeParty(this.ship_to_trade_party, this.ship_to_trade_address)}
</ram:ShipToTradeParty>
<ram:ShipFromTradeParty py:if="this.ship_from_trade_party">
${TradeParty(this.ship_from_trade_party, this.ship_from_trade_address)}
</ram:ShipFromTradeParty>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>${this.payment_reference}</ram:PaymentReference>
<ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>1</ram:TypeCode> <!-- Instrument not defined -->
</ram:SpecifiedTradeSettlementPaymentMeans>
<py:for each="tax in this.invoice.taxes">
${TradeTax(tax.tax, tax.amount, tax.base)}
</py:for>
<ram:SpecifiedTradePaymentTerms py:for="line in this.invoice.lines_to_pay">
<ram:Description py:if="this.invoice.payment_term and this.invoice.payment_term.description">${this.invoice.payment_term.description}</ram:Description>
<ram:DueDateDateTime>
${DateTime(line.maturity_date)}
</ram:DueDateDateTime>
<ram:PartialPaymentAmount py:attrs="{'currencyID': this.invoice.currency.code}">${(line.amount_second_currency or (line.debit - line.credit)) * this.type_sign}</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.untaxed_amount * this.type_sign}</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.untaxed_amount * this.type_sign}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.tax_amount * this.type_sign}</ram:TaxTotalAmount>
<ram:GrandTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.total_amount * this.type_sign}</ram:GrandTotalAmount>
<ram:DuePayableAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.amount_to_pay * this.type_sign}</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>

View file

@ -46,6 +46,52 @@ class EdocTestCase(ModuleTestCase):
{'identifiers': [('delete', [party.identifiers[0].id])]}]
)
@with_transaction()
def test_xrechn_export_facturx(self):
""" run export - factur-x
"""
pool = Pool()
Template = pool.get('edocument.facturxext.invoice')
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.payment_term_date = date.today()
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 = [
Mock(
spec=Identifier,
type='edoc_route_id',
code='xrechn-route-id-123')
]
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')
invoice_xml = etree.fromstring(invoice_string)
schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml)
@with_transaction()
def test_xrechn_export_xml_invoice(self):
""" run export - invoice

21
tests/validate_xml.py Normal file
View file

@ -0,0 +1,21 @@
# -*- 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 lxml import etree
import os
file_name = 'file-to-check.xml'
schema_file = os.path.join(
os.path.dirname(__file__),
'Factur-X_1.07.2_EXTENDED',
'Factur-X_1.07.2_EXTENDED.xsd')
with open(file_name, 'r') as fhdl:
f_content = fhdl.read()
f_content = f_content.encode('utf8')
invoice_xml = etree.fromstring(f_content)
schema = etree.XMLSchema(etree.parse(schema_file))
print('Result:', schema.assertValid(invoice_xml))