Compare commits

...

17 commits

Author SHA1 Message Date
Frederik Jaeckel
a8541f73bd dont except if no 'modegross' on invoice.line 2025-07-02 13:51:57 +02:00
Frederik Jaeckel
f3375d3d39 allow any vat-type to be exported as 'SpecifiedTaxRegistration' 2025-06-11 15:57:35 +02:00
Frederik Jaeckel
437f047e44 test: fix tax-code 2025-06-11 12:14:05 +02:00
Frederik Jaeckel
2dab5b1b43 SpecifiedTradeProduct: export 'description' as 'name' if no product was used in invoice-line 2025-06-11 11:57:16 +02:00
Frederik Jaeckel
4c2565e15e add xml-validator 2025-06-11 11:48:55 +02:00
Frederik Jaeckel
81f6a3e4a8 ApplicableTradeTax: disable ExemptionReason if CategoryCode exist 2025-06-11 11:48:35 +02:00
Frederik Jaeckel
d065f74482 add legal id of seller 2025-06-11 11:46:53 +02:00
Frederik Jaeckel
214cbb086f get amount on line as net-value (even if invoice is gross-mode) 2025-06-10 13:45:07 +02:00
Frederik Jaeckel
a5bf930e55 update license 2025-05-02 14:16:47 +02:00
Frederik Jaeckel
ea5a83d7c1 use product.round_price() to round unit_price of invoice-line 2025-01-29 09:33:40 +01:00
Frederik Jaeckel
02221601e7 round unit_price of invoice-line by price_digits 2025-01-28 12:50:55 +01:00
Frederik Jaeckel
ceb2a72fed tests: drop usage of Mock, generate invoice in db 2025-01-09 11:50:52 +01:00
Frederik Jaeckel
e47380f930 formatting 2025-01-09 11:50:09 +01:00
Frederik Jaeckel
9a13bc325d calculation of the tax adjusted 2025-01-09 11:50:00 +01:00
e291608373 Merge pull request 'handle tax childs' (#6) from jangras/edocument_xrechnung:wip_jan into main
Reviewed-on: m-ds/edocument_xrechnung#6
2025-01-09 08:33:34 +00:00
dba1965894 fix typo 2025-01-04 21:08:43 +01:00
f3b4849e0c handle tax childs 2025-01-03 22:28:58 +01:00
7 changed files with 389 additions and 142 deletions

View file

@ -1,7 +1,5 @@
Copyright (C) 2015-2023 Cédric Krier. Copyright (C) 2021-2025 martin-data services.
Copyright (C) 2015-2023 B2CK SPRL. Copyright (C) 2024-2025 Mathias Behrle
Copyright (C) 2021-2024 martin-data services.
Copyright (C) 2024 Mathias Behrle
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by

View file

@ -12,3 +12,5 @@ https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingp
https://erechnungsvalidator.service-bw.de/ https://erechnungsvalidator.service-bw.de/
https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/ https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/
https://www.e-rechnungs-checker.de/

View file

@ -10,6 +10,8 @@ from trytond.exceptions import UserError
from trytond.i18n import gettext from trytond.i18n import gettext
from trytond.tools import cached_property from trytond.tools import cached_property
from trytond.pool import Pool from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.modules.product import round_price
class EdocumentMixin(object): class EdocumentMixin(object):
@ -100,21 +102,29 @@ class EdocumentMixin(object):
""" get tax of invoice-line, """ get tax of invoice-line,
fire exception if no/multiple taxes exists fire exception if no/multiple taxes exists
""" """
if len(line.invoice_taxes) != 1: Tax = Pool().get('account.tax')
if len(line.taxes) != 1:
raise UserError(gettext( raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_number', 'edocument_xrechnung.msg_linetax_invalid_number',
linename=line.rec_name, linename=line.rec_name,
numtax=len(line.invoice_taxes))) numtax=len(line.taxes)))
taxlines = Tax.compute(
line.taxes, Decimal('1'), 1.0,
line.invoice.accounting_date or line.invoice.invoice_date)
assert len(taxlines) == 1
tax = taxlines[0]['tax']
allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B'] allowed_cat = ['AE', 'L', 'M', 'E', 'S', 'Z', 'G', 'O', 'K', 'B']
unece_category_code = self.get_category_code(line.invoice_taxes[0].tax) unece_category_code = self.get_category_code(tax)
if unece_category_code not in allowed_cat: if unece_category_code not in allowed_cat:
raise UserError(gettext( raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_catcode', 'edocument_xrechnung.msg_linetax_invalid_catcode',
taxname=line.invoice_taxes[0].tax.rec_name, taxname=tax.rec_name,
allowed=', '.join(allowed_cat))) allowed=', '.join(allowed_cat)))
return line.invoice_taxes[0].tax return tax
def taxident_data(self, tax_identifier): def taxident_data(self, tax_identifier):
""" get tax-scheme-id and codes """ get tax-scheme-id and codes
@ -151,6 +161,44 @@ class EdocumentMixin(object):
taxname=tax.rec_name)) taxname=tax.rec_name))
return tax.unece_code return tax.unece_code
def get_line_amount(self, line):
""" get amount of current invoice-line,
depends on modegross of invoice, set used-modegross to 'net'
Args:
line (record): model account.invoice.line
"""
if not hasattr(line, 'modegross'):
return line.amount
if line.modegross == 'net':
return line.amount
elif line.modegross == 'gross':
# get net-amount
# copy from account_invoice/invoice.py:2416-2434
currency = (
line.invoice.currency
if line.invoice else line.currency)
amount = (Decimal(str(line.quantity or 0)) * (
line.unit_price or Decimal(0)))
invoice_type = (
line.invoice.type
if line.invoice else line.invoice_type)
if (invoice_type == 'in'
and line.taxes_deductible_rate is not None
and line.taxes_deductible_rate != 1):
with Transaction().set_context(_deductible_rate=1):
tax_amount = sum(
t['amount'] for t in line._get_taxes().values())
non_deductible_amount = (
tax_amount * (1 - line.taxes_deductible_rate))
amount += non_deductible_amount
if currency:
return currency.round(amount)
return amount
def get_tax_unece_code(self, tax): def get_tax_unece_code(self, tax):
while tax: while tax:
if tax.unece_code: if tax.unece_code:
@ -175,10 +223,49 @@ class EdocumentMixin(object):
taxname=tax.rec_name)) taxname=tax.rec_name))
return unece_category_code return unece_category_code
def round_unitprice(self, value):
""" round value by digits in unit_price of account.invoice.line
Args:
value (Decimal): unit-price
Returns:
Decimal: rounded value
"""
if value is not None:
return round_price(value)
return value
def quote_text(self, text): def quote_text(self, text):
""" replace critical chars """ replace critical chars
""" """
if text: if text:
return html.escape(text) return html.escape(text)
def _party_legal_types(self):
""" get list of identifier-types to be used as
legal-ids
"""
return ['de_handelsregisternummer']
def party_legal_ids(self, party, address):
""" get list of legal-ids of party
Args:
party (record): model party.party
address (record): model party.address
"""
result = super().party_legal_ids(party, address)
legal_types = self._party_legal_types()
if party and party.identifiers:
for x in party.identifiers:
if x.type in legal_types:
if x.address:
if x.address == address:
result.append((x.rec_name, {'schemeID': '0002'}))
else:
result.append((x.rec_name, {'schemeID': '0002'}))
return result
# end EdocumentMixin # end EdocumentMixin

View file

@ -13,11 +13,9 @@ here = path.abspath(path.dirname(__file__))
MODULE = 'edocument_xrechnung' MODULE = 'edocument_xrechnung'
PREFIX = 'mds' PREFIX = 'mds'
# Get the long description from the README file
with open(path.join(here, 'README.rst'), encoding='utf-8') as f: with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read() long_description = f.read()
# tryton.cfg einlesen
config = ConfigParser() config = ConfigParser()
config.readfp(open('tryton.cfg')) config.readfp(open('tryton.cfg'))
info = dict(config.items('tryton')) info = dict(config.items('tryton'))
@ -87,6 +85,8 @@ setup(
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
], ],
keywords='tryton xrechnung edcoument', keywords='tryton xrechnung edcoument',

View file

@ -19,7 +19,7 @@ this repository contains the full copyright notices and license terms. -->
</py:for> </py:for>
</ram:SpecifiedLegalOrganization> </ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress py:if="address">${TradeAddress(address)}</ram:PostalTradeAddress> <ram:PostalTradeAddress py:if="address">${TradeAddress(address)}</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration py:if="tax_identifier and tax_identifier.type == 'eu_vat'"> <ram:SpecifiedTaxRegistration py:if="tax_identifier and tax_identifier.type.endswith('_vat')">
<ram:ID schemeID='VA'>${tax_identifier.code}</ram:ID> <ram:ID schemeID='VA'>${tax_identifier.code}</ram:ID>
</ram:SpecifiedTaxRegistration> </ram:SpecifiedTaxRegistration>
</py:def> </py:def>
@ -38,7 +38,7 @@ this repository contains the full copyright notices and license terms. -->
<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>${this.tax_unece_code(tax)}</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 and not this.tax_category_code(tax)">${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>${this.tax_category_code(tax)}</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>
@ -71,23 +71,21 @@ this repository contains the full copyright notices and license terms. -->
</ram:AssociatedDocumentLineDocument> </ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct> <ram:SpecifiedTradeProduct>
<ram:ID py:if="line.product and 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>${this.quote_text(line.product.name if line.product else '')}</ram:Name> <ram:Name>${this.quote_text(line.product.name if line.product else line.description if line.description else 'name not set')}</ram:Name>
<ram:Description py:if="line.description">${this.quote_text(line.description)}</ram:Description> <ram:Description py:if="line.description">${this.quote_text(line.description if line.product else '')}</ram:Description>
</ram:SpecifiedTradeProduct> </ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement> <ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice> <ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.currency.round(line.unit_price)}</ram:ChargeAmount> <ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.round_unitprice(line.unit_price)}</ram:ChargeAmount>
</ram:NetPriceProductTradePrice> </ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement> </ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery> <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: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:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement> <ram:SpecifiedLineTradeSettlement>
<py:for each="tax in line.invoice_taxes"> ${TradeTax(this.invoice_line_tax(line))}
${TradeTax(tax.tax)}
</py:for>
<ram:SpecifiedTradeSettlementLineMonetarySummation> <ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${line.amount}</ram:LineTotalAmount> <ram:LineTotalAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.get_line_amount(line)}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation> </ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement> </ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem> </ram:IncludedSupplyChainTradeLineItem>

View file

@ -5,20 +5,180 @@
from lxml import etree from lxml import etree
import os import os
from unittest.mock import Mock
from decimal import Decimal from decimal import Decimal
from datetime import date from datetime import date
from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.tests.test_tryton import (
ModuleTestCase, with_transaction, activate_module)
from trytond.pool import Pool from trytond.pool import Pool
from trytond.modules.edocument_uncefact.tests.test_module import get_invoice
from trytond.modules.company.tests import create_company, set_company from trytond.modules.company.tests import create_company, set_company
from trytond.modules.account.tests import create_chart, get_fiscalyear
from trytond.exceptions import UserError from trytond.exceptions import UserError
def set_invoice_sequences(fiscalyear):
pool = Pool()
Sequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
ModelData = pool.get('ir.model.data')
sequence = Sequence(
name=fiscalyear.name,
sequence_type=SequenceType(ModelData.get_id(
'account_invoice', 'sequence_type_account_invoice')),
company=fiscalyear.company)
sequence.save()
fiscalyear.invoice_sequences = []
invoice_sequence = InvoiceSequence()
invoice_sequence.fiscalyear = fiscalyear
invoice_sequence.in_invoice_sequence = sequence
invoice_sequence.in_credit_note_sequence = sequence
invoice_sequence.out_invoice_sequence = sequence
invoice_sequence.out_credit_note_sequence = sequence
invoice_sequence.save()
return fiscalyear
class EdocTestCase(ModuleTestCase): class EdocTestCase(ModuleTestCase):
'Test e-rechnung module' 'Test e-rechnung module'
module = 'edocument_xrechnung' module = 'edocument_xrechnung'
@classmethod
def setUpClass(cls):
super().setUpClass()
activate_module([
'edocument_uncefact', 'party', 'bank',
'account_invoice', 'sale_point_invoice',
'product_grossprice'], 'en')
def prep_fiscalyear(self, company1):
""" prepare fiscal year, sequences...
"""
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
fisc_year = get_fiscalyear(company1, today=date(2024, 1, 15))
set_invoice_sequences(fisc_year)
self.assertEqual(len(fisc_year.invoice_sequences), 1)
FiscalYear.create_period([fisc_year])
def prep_company(self):
""" create company, add country and bank-account
"""
pool = Pool()
Country = pool.get('country.country')
Party = pool.get('party.party')
Bank = pool.get('bank')
BankAccount = pool.get('bank.account')
country_de, = Country.create([{
'name': 'Germany',
'code': 'DE',
'code3': 'DEU'}])
company = create_company('m-ds')
Party.write(*[[company.party], {
'identifiers': [('create', [
# post.de
{'type': 'de_handelsregisternummer', 'code': 'Bonn HRB 6792'},
{'type': 'de_vat', 'code': 'DE 169838187'},
])],
'addresses': [('write', [company.party.addresses[0]], {
'country': country_de.id})]}])
bank_party, = Party.create([{
'name': 'Bank 123',
'addresses': [('create', [{}])]}])
bank, = Bank.create([{'party': bank_party.id}])
BankAccount.create([{
'bank': bank.id,
'owners': [('add', [company.party.id])],
'numbers': [('create', [{
'type': 'iban',
'number': 'DE02300209000106531065'}])]}])
return company
def prep_invoice(self, credit_note=False, modegross='net'):
""" add invoice
"""
pool = Pool()
Invoice = pool.get('account.invoice')
Taxes = pool.get('account.tax')
Account = pool.get('account.account')
Journal = pool.get('account.journal')
Currency = pool.get('currency.currency')
Uom = pool.get('product.uom')
Country = pool.get('country.country')
Party = pool.get('party.party')
country_de, = Country.search([('code', '=', 'DE')])
customer, = Party.create([{
'name': 'Customer',
'identifiers': [('create', [{
'type': 'edoc_route_id', 'code': 'xrechn-route-id-123'}])],
'addresses': [('create', [{
'invoice': True,
'street': 'Customer Street 1',
'postal_code': '12345',
'city': 'Usertown',
'country': country_de.id,
}])],
}])
currency1, = Currency.search([('code', '=', 'usd')])
Currency.write(*[[currency1], {'code': 'USD'}])
tax, = Taxes.search([('name', '=', '20% VAT')])
Taxes.write(*[
[tax],
{'unece_code': 'VAT', 'unece_category_code': 'S',
'legal_notice': 'Legal Notice'}])
account_lst = Account.search([
('name', 'in', ['Main Revenue', 'Main Receivable'])
], order=[('name', 'ASC')])
self.assertEqual(len(account_lst), 2)
self.assertEqual(account_lst[0].name, 'Main Receivable')
journ_lst = Journal.search([('name', '=', 'Revenue')])
self.assertEqual(len(journ_lst), 1)
to_create_invoice = [{
'type': 'out',
'modegross': modegross,
'description': 'description of invoice',
'comment': 'note line 1\nnote line 2',
'invoice_date': date(2024, 7, 1),
'party': customer.id,
'invoice_address': customer.addresses[0].id,
'account': account_lst[0].id,
'journal': journ_lst[0].id,
'currency': currency1.id,
'lines': [('create', [{
'type': 'line',
'quantity': 2.0 if not credit_note else -2.0,
'description': 'Product 1',
'unit': Uom.search([('symbol', '=', 'u')])[0].id,
'unit_price': Decimal('50.0'),
'taxes': [('add', [tax.id])],
'account': account_lst[1].id,
'currency': currency1.id,
}])],
}]
if modegross == 'gross':
to_create_invoice[0]['lines'][0][1][0]['unit_gross_price'] = (
Decimal('50.0') * Decimal('1.2'))
inv_lst, = Invoice.create(to_create_invoice)
inv_lst.on_change_lines()
inv_lst.save()
Invoice.validate_invoice([inv_lst])
Invoice.post([inv_lst])
self.assertEqual(inv_lst.currency.code, 'USD')
self.assertEqual(len(inv_lst.move.lines), 3)
return inv_lst
@with_transaction() @with_transaction()
def test_xrechn_bank_account_owned(self): def test_xrechn_bank_account_owned(self):
""" check field 'company_owned' on bank.account.number """ check field 'company_owned' on bank.account.number
@ -95,38 +255,17 @@ class EdocTestCase(ModuleTestCase):
) )
@with_transaction() @with_transaction()
def test_xrechn_export_facturx(self): def test_xrechn_export_facturx_gross(self):
""" run export - factur-x """ run export - factur-x, modegross='gross'
""" """
pool = Pool() pool = Pool()
Template = pool.get('edocument.facturxext.invoice') 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() company = self.prep_company()
invoice.payment_term_date = date.today() with set_company(company):
invoice.party.get_xrechnung_route_id = Mock( create_chart(company=company, tax=True)
return_value='xrechn-route-id-123') self.prep_fiscalyear(company)
invoice.company.party.bank_accounts = [ invoice = self.prep_invoice(modegross='gross')
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) template = Template(invoice)
@ -136,7 +275,83 @@ class EdocTestCase(ModuleTestCase):
'Factur-X_1.07.2_EXTENDED.xsd') 'Factur-X_1.07.2_EXTENDED.xsd')
invoice_string = template.render('Factur-X-1.07.2-extended') invoice_string = template.render('Factur-X-1.07.2-extended')
with open('gross_invoice_string.xml', 'wb') as fhdl:
fhdl.write(invoice_string)
invoice_xml = etree.fromstring(invoice_string) invoice_xml = etree.fromstring(invoice_string)
# check values in xml
nodes = invoice_xml.xpath(self._readxml_xpath([
'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty',
'ram:SpecifiedLegalOrganization', 'ram:ID']),
namespaces=invoice_xml.nsmap)
self.assertEqual(nodes[0].text, 'Bonn HRB 6792')
nodes = invoice_xml.xpath(self._readxml_xpath([
'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
'ram:IncludedSupplyChainTradeLineItem',
'ram:SpecifiedLineTradeSettlement',
'ram:SpecifiedTradeSettlementLineMonetarySummation',
'ram:LineTotalAmount']),
namespaces=invoice_xml.nsmap)
self.assertEqual(nodes[0].text, '100.00')
schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml)
def _readxml_xpath(self, tags):
""" generate xpath
Args:
tags (list): list of string or integer to build path
"""
parts = []
for x in tags:
if isinstance(x, str):
parts.append(x)
elif isinstance(x, int):
if parts[-1].endswith(']'):
raise ValueError('multiple list selector')
parts[-1] += '[%d]' % x
result = '/' + '/'.join(parts)
return result
@with_transaction()
def test_xrechn_export_facturx(self):
""" run export - factur-x
"""
pool = Pool()
Template = pool.get('edocument.facturxext.invoice')
company = self.prep_company()
with set_company(company):
create_chart(company=company, tax=True)
self.prep_fiscalyear(company)
invoice = self.prep_invoice()
template = Template(invoice)
self.assertEqual(
template.party_legal_ids(invoice.company_party, None),
[('Bonn HRB 6792', {'schemeID': '0002'})])
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)
# check values in xml
nodes = invoice_xml.xpath(self._readxml_xpath([
'rsm:CrossIndustryInvoice', 'rsm:SupplyChainTradeTransaction',
'ram:ApplicableHeaderTradeAgreement', 'ram:SellerTradeParty',
'ram:SpecifiedLegalOrganization', 'ram:ID']),
namespaces=invoice_xml.nsmap)
self.assertEqual(nodes[0].text, 'Bonn HRB 6792')
schema = etree.XMLSchema(etree.parse(schema_file)) schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml) schema.assertValid(invoice_xml)
@ -146,33 +361,12 @@ class EdocTestCase(ModuleTestCase):
""" """
pool = Pool() pool = Pool()
Template = pool.get('edocument.xrechnung.invoice') Template = pool.get('edocument.xrechnung.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() company = self.prep_company()
invoice.payment_term_date = date.today() with set_company(company):
invoice.party.get_xrechnung_route_id = Mock( create_chart(company=company, tax=True)
return_value='xrechn-route-id-123') self.prep_fiscalyear(company)
invoice.company.party.bank_accounts = [ invoice = self.prep_invoice()
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) template = Template(invoice)
@ -192,43 +386,12 @@ class EdocTestCase(ModuleTestCase):
""" """
pool = Pool() pool = Pool()
Template = pool.get('edocument.xrechnung.invoice') Template = pool.get('edocument.xrechnung.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() company = self.prep_company()
with set_company(company):
# credit note create_chart(company=company, tax=True)
invoice.lines[0].quantity = -1 self.prep_fiscalyear(company)
invoice.lines[0].amount = Decimal('-100.0') invoice = self.prep_invoice(credit_note=True)
invoice.taxes[0].base = Decimal('-100.0')
invoice.taxes[0].amount = Decimal('-10.0')
invoice.untaxed_amount = Decimal('-100.0')
invoice.tax_amount = Decimal('-10.0')
invoice.total_amount = Decimal('-110.0')
invoice.lines_to_pay[0].debit = Decimal('-110.0')
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) template = Template(invoice)
@ -242,10 +405,6 @@ class EdocTestCase(ModuleTestCase):
schema = etree.XMLSchema(etree.parse(schema_file)) schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml) schema.assertValid(invoice_xml)
# invoice_string = template.render('XRechnung-2.2')
# with open('xrechnung-test-creditnote.xml', 'wt') as fhdl:
# fhdl.write(invoice_string.decode('utf8'))
# end EdocTestCase # end EdocTestCase

View file

@ -5,6 +5,9 @@ depends:
party party
bank bank
account_invoice account_invoice
extras_depend:
sale_point_invoice
product_grossprice
xml: xml:
message.xml message.xml
configuration.xml configuration.xml