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. # full copyright notices and license terms.
from trytond.pool import Pool from trytond.pool import Pool
from .edocument import Invoice from .edocument import Invoice, FacturX
from .party import PartyConfiguration, Party from .party import PartyConfiguration, Party
def register(): def register():
Pool.register( Pool.register(
Invoice, Invoice,
FacturX,
Party, Party,
PartyConfiguration, PartyConfiguration,
module='edocument_xrechnung', type_='model') module='edocument_xrechnung', type_='model')

View file

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