export: add factur-x 1.07.2
This commit is contained in:
parent
3d703e300e
commit
764cacc091
5 changed files with 259 additions and 10 deletions
|
@ -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')
|
||||
|
|
54
edocument.py
54
edocument.py
|
@ -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:
|
||||
|
|
141
template/Factur-X-1.07.2-extended/invoice.xml
Normal file
141
template/Factur-X-1.07.2-extended/invoice.xml
Normal 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>
|
|
@ -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
21
tests/validate_xml.py
Normal 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))
|
Loading…
Reference in a new issue