Compare commits

..

No commits in common. "main" and "ver-7.0.5" have entirely different histories.

18 changed files with 272 additions and 806 deletions

View file

@ -1,5 +1,6 @@
Copyright (C) 2021-2025 martin-data services. Copyright (C) 2015-2023 Cédric Krier.
Copyright (C) 2024-2025 Mathias Behrle Copyright (C) 2015-2023 B2CK SPRL.
Copyright (C) 2021-2023 martin-data services.
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

@ -14,23 +14,6 @@ Requires
Changes Changes
======= =======
*7.0.10 - 12.12.2024*
- fix missing views
- Remove arguments in super() calls. (Mathias Behrle)
*7.0.9 - 11.12.2024*
- fix name of party in exceptions
*7.0.7 - 10.12.2024*
- add iban to xml-export
*7.0.6 - 09.12.2024*
- add: check for valid data to generate xml
*7.0.5 - 05.12.2024* *7.0.5 - 05.12.2024*
- add: export Factur-X 1.07.2 - add: export Factur-X 1.07.2

View file

@ -4,18 +4,13 @@
# 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 XRechnung, FacturX from .edocument import Invoice, FacturX
from .bank import AccountNumber
from .party import PartyConfiguration, Party from .party import PartyConfiguration, Party
from .configuration import Configuration, BankEdocumentRel
def register(): def register():
Pool.register( Pool.register(
AccountNumber, Invoice,
XRechnung,
Configuration,
BankEdocumentRel,
FacturX, FacturX,
Party, Party,
PartyConfiguration, PartyConfiguration,

97
bank.py
View file

@ -1,97 +0,0 @@
# -*- 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 sql.conditionals import Case
from trytond.pool import PoolMeta, Pool
from trytond.model import fields
from trytond.transaction import Transaction
DEF_NONE = None
class AccountNumber(metaclass=PoolMeta):
__name__ = 'bank.account.number'
company_owned = fields.Function(fields.Boolean(
string='Number belongs to Company',
readonly=True),
'get_company_owned',
searcher='searcher_company_owned')
@classmethod
def get_company_owned_sql(cls):
""" get sql to search for bank acconts owned by company-party
"""
pool = Pool()
Account = pool.get('bank.account')
Number = pool.get('bank.account.number')
Owners = pool.get('bank.account-party.party')
Company = pool.get('company.company')
context = Transaction().context
tab_acc = Account.__table__()
tab_owner = Owners.__table__()
tab_num = Number.__table__()
company_id = context.get('company', -1)
party_id = -1
if company_id and company_id > 0:
party_id = Company(company_id).party.id
query = tab_num.join(
tab_acc,
condition=tab_num.account == tab_acc.id,
).join(
tab_owner,
condition=tab_owner.account == tab_acc.id,
).select(
tab_num.id.as_('number'),
Case(
(tab_owner.owner == party_id, True),
else_=False,
).as_('owned'))
return (tab_num, query)
@classmethod
def searcher_company_owned(cls, name, clause):
""" search in owned by party
Args:
name (str): field name
clause (list): search domain
Returns:
list: updated search domain
"""
Operator = fields.SQL_OPERATORS[clause[1]]
(tab_num, query) = cls.get_company_owned_sql()
query = query.select(
query.number,
where=Operator(query.owned, clause[2]))
return [('id', 'in', query)]
@classmethod
def get_company_owned(cls, records, names):
""" get list of bank account numbers owned by company
"""
cursor = Transaction().connection.cursor()
result = {x: {y.id: False for y in records} for x in names}
(tab_num, query) = cls.get_company_owned_sql()
query.where = tab_num.id.in_([x.id for x in records])
cursor.execute(*query)
lines = cursor.fetchall()
for line in lines:
values = {'company_owned': line[1]}
for name in names:
result[name][line[0]] = values.get(name)
return result
# end AccountNumber

View file

@ -1,35 +0,0 @@
# -*- 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 trytond.pool import PoolMeta
from trytond.model import ModelSQL, fields
class Configuration(metaclass=PoolMeta):
__name__ = 'account.configuration'
edocument_bank = fields.Many2Many(
string='Bank accounts',
relation_name='edocument_xrechnung.bank_rel',
origin='config', target='bankaccount',
filter=[('company_owned', '=', True)],
help='The bank accounts listed here are output in the invoice XML.')
# end Configuration
class BankEdocumentRel(ModelSQL):
'Bank - eDocument - Relation'
__name__ = 'edocument_xrechnung.bank_rel'
bankaccount = fields.Many2One(
string='Account', model_name='bank.account.number',
required=True, ondelete='CASCADE')
config = fields.Many2One(
string='Configuration', model_name='account.configuration',
required=True, ondelete='CASCADE')
# end BankEdocumentRel

View file

@ -1,15 +0,0 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data>
<record model="ir.ui.view" id="configuration_view_form">
<field name="model">account.configuration</field>
<field name="inherit" ref="account.configuration_view_form"/>
<field name="name">configuration_form</field>
</record>
</data>
</tryton>

View file

@ -6,9 +6,3 @@ https://github.com/itplr-kosit
validator: validator:
- https://erechnungsvalidator.service-bw.de/ - https://erechnungsvalidator.service-bw.de/
überweisungsdaten
https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/zugferd%202.0.1%20-%20facturx%201.03/en%2016931%20%E2%80%93%20facturx%201.03%20%E2%80%93%20zugferd%202.0.1%20-%20basic.scm/html/de/021.htm?https://portal3.gefeg.com/projectdata/invoice/deliverables/installed/publishingproject/zugferd%202.0.1%20-%20facturx%201.03/en%2016931%20%E2%80%93%20facturx%201.03%20%E2%80%93%20zugferd%202.0.1%20-%20basic.scm/html/de/02134.htm
https://erechnungsvalidator.service-bw.de/
https://ecosio.com/de/peppol-und-xml-dokumente-online-validieren/

View file

@ -5,12 +5,50 @@
import genshi.template import genshi.template
import os import os
from decimal import Decimal import html
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.modules.edocument_uncefact.edocument import Invoice from trytond.modules.edocument_uncefact.edocument import Invoice
from .mixin import EdocumentMixin from decimal import Decimal
class XRechnung(EdocumentMixin, Invoice): 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' 'EDocument XRechnung'
__name__ = 'edocument.xrechnung.invoice' __name__ = 'edocument.xrechnung.invoice'
@ -50,6 +88,74 @@ class XRechnung(EdocumentMixin, Invoice):
if notes: if notes:
return '; '.join(notes) 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']
unece_category_code = self.get_category_code(line.invoice_taxes[0].tax)
if unece_category_code not 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):
""" get tax-rate in procent
"""
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 get_category_code(self, tax):
while tax:
if tax.unece_category_code:
return tax.unece_category_code
break
tax = tax.parent
def tax_category_code(self, tax):
""" read tax-category, fire exception if missing
"""
unece_category_code = self.get_category_code(tax)
if not unece_category_code:
raise UserError(gettext(
'edocument_xrechnung.mds_tax_category_missing',
taxname=tax.rec_name))
return unece_category_code
def quote_text(self, text):
""" replace critical chars
"""
if text:
return html.quote(text)
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
""" """
@ -70,28 +176,6 @@ class XRechnung(EdocumentMixin, Invoice):
else: else:
raise ValueError('invalid type-code "%s"' % self.type_code) raise ValueError('invalid type-code "%s"' % self.type_code)
else: else:
return super()._get_template(version) return super(Invoice, self)._get_template(version)
# end XRechnung # end Invoice
class FacturX(EdocumentMixin, Invoice):
'Factur-X'
__name__ = 'edocument.facturxext.invoice'
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()._get_template(version)
# end FacturX

View file

@ -14,10 +14,6 @@ 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_tax_code_missing"
msgid "The UNECE tax code is not configured for tax '%(taxname)s'."
msgstr "Für die Steuer '%(taxname)s' ist der UNECE Steuercode nicht konfiguriert."
msgctxt "model:ir.message,text:msg_uom_code_missing" msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'." 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." msgstr "Für die Einheit '%(uomname)s' ist der UNECE-Einheitencode nicht konfiguriert."
@ -30,18 +26,6 @@ msgctxt "model:ir.message,text:msg_linetax_invalid_catcode"
msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgstr "Ungültiger Kategoriecode an der Steuer '%(taxname)s' (erlaubt: %(allowed)s)." msgstr "Ungültiger Kategoriecode an der Steuer '%(taxname)s' (erlaubt: %(allowed)s)."
msgctxt "model:ir.message,text:msg_no_seller_address"
msgid "There is no address for the seller party '%(sellerparty)s'."
msgstr "Für die Verkäuferpartei '%(sellerparty)s' existiert keine Adresse."
msgctxt "model:ir.message,text:msg_no_buyer_address"
msgid "There is no address for the buyer party '%(buyerparty)s'."
msgstr "Für die Käuferpartei '%(sellerparty)s' existiert keine Adresse."
msgctxt "model:ir.message,text:msg_no_address_country"
msgid "No country is specified for the address of the party '%(party)s'."
msgstr "Für die Adresse der Partei '%(party)s' ist kein Land festgelegt."
####################### #######################
# party.configuration # # party.configuration #
@ -63,12 +47,9 @@ msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Leitweg-ID" msgstr "X-Rechnung Leitweg-ID"
msgctxt "help:party.party,xrechnung_routeid:" msgctxt "help:party.party,xrechnung_routeid:"
msgid "" msgid "Enables the need for an XRechnung route ID at the party for exporting the XRechnung."
"When activated an XRechnung route ID must be used for this party for X-Rechnung exports.\n" msgstr "Aktiviert die Notwendigkeit einer XRechnung-Leitweg-ID an der Partei für den Export der XRechnung."
"The route ID must be defined as identifier of type \"X-Rechnung Route-ID\"."
msgstr ""
"Bei Aktivierung muss eine XRechnung-Leitweg-ID bei Rechnungsexporten für diese Partei benutzt werden.\n"
"Die Leitweg-ID muss als Identifikator mit Typ \"X-Rechnung Route-ID\" angelegt werden."
############### ###############
# account.tax # # account.tax #
@ -116,31 +97,3 @@ msgstr "Allgemeine indirekte Steuer der kanarischen Inseln"
msgctxt "selection:account.tax,xrtax_category:" msgctxt "selection:account.tax,xrtax_category:"
msgid "Tax on production; services and imports in Ceuta and Melilla" msgid "Tax on production; services and imports in Ceuta and Melilla"
msgstr "Steuer für Produktion; Dienstleistungen und Einfuhr in Ceuta und Melilla" msgstr "Steuer für Produktion; Dienstleistungen und Einfuhr in Ceuta und Melilla"
#########################
# account.configuration #
#########################
msgctxt "field:account.configuration,edocument_bank:"
msgid "Bank accounts"
msgstr "Bankkonten"
msgctxt "help:account.configuration,edocument_bank:"
msgid "The bank accounts listed here are output in the invoice XML."
msgstr "Die hier aufgeführten Bankkonten werden in der Rechnungs-XML ausgegeben."
################################
# edocument_xrechnung.bank_rel #
################################
msgctxt "model:edocument_xrechnung.bank_rel,name:"
msgid "Bank - eDocument - Relation"
msgstr "Bank - eDocument - Verknüpfung"
msgctxt "field:edocument_xrechnung.bank_rel,bankaccount:"
msgid "Account"
msgstr "Konto"
msgctxt "field:edocument_xrechnung.bank_rel,config:"
msgid "Configuration"
msgstr "Konfiguration"

View file

@ -10,10 +10,6 @@ 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 "The UNECE tax category is not configured for tax '%(taxname)s'." msgstr "The UNECE tax category is not configured for tax '%(taxname)s'."
msgctxt "model:ir.message,text:msg_tax_code_missing"
msgid "The UNECE tax code is not configured for tax '%(taxname)s'."
msgstr "The UNECE tax code is not configured for tax '%(taxname)s'."
msgctxt "model:ir.message,text:msg_uom_code_missing" msgctxt "model:ir.message,text:msg_uom_code_missing"
msgid "The UNECE uom code is not configured for unit '%(uomname)s'." msgid "The UNECE uom code is not configured for unit '%(uomname)s'."
msgstr "The UNECE uom code is not configured for unit '%(uomname)s'." msgstr "The UNECE uom code is not configured for unit '%(uomname)s'."
@ -26,18 +22,6 @@ msgctxt "model:ir.message,text:msg_linetax_invalid_catcode"
msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgid "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgstr "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)." msgstr "Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s)."
msgctxt "model:ir.message,text:msg_no_seller_address"
msgid "There is no address for the seller party '%(sellerparty)s'."
msgstr "There is no address for the seller party '%(sellerparty)s'."
msgctxt "model:ir.message,text:msg_no_buyer_address"
msgid "There is no address for the buyer party '%(buyerparty)s'."
msgstr "There is no address for the buyer party '%(buyerparty)s'."
msgctxt "model:ir.message,text:msg_no_address_country"
msgid "No country is specified for the address of the party '%(party)s'."
msgstr "No country is specified for the address of the party '%(party)s'."
msgctxt "selection:party.configuration,identifier_types:" msgctxt "selection:party.configuration,identifier_types:"
msgid "X-Rechnung Route-ID" msgid "X-Rechnung Route-ID"
msgstr "X-Rechnung Route-ID" msgstr "X-Rechnung Route-ID"
@ -98,23 +82,3 @@ msgctxt "selection:account.tax,xrtax_category:"
msgid "Tax on production; services and imports in Ceuta and Melilla" msgid "Tax on production; services and imports in Ceuta and Melilla"
msgstr "Tax on production; services and imports in Ceuta and Melilla" msgstr "Tax on production; services and imports in Ceuta and Melilla"
msgctxt "field:account.configuration,edocument_bank:"
msgid "Bank accounts"
msgstr "Bank accounts"
msgctxt "help:account.configuration,edocument_bank:"
msgid "The bank accounts listed here are output in the invoice XML."
msgstr "The bank accounts listed here are output in the invoice XML."
msgctxt "model:edocument_xrechnung.bank_rel,name:"
msgid "Bank - eDocument - Relation"
msgstr "Bank - eDocument - Relation"
msgctxt "field:edocument_xrechnung.bank_rel,bankaccount:"
msgid "Account"
msgstr "Account"
msgctxt "field:edocument_xrechnung.bank_rel,config:"
msgid "Configuration"
msgstr "Configuration"

View file

@ -11,9 +11,6 @@ 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_tax_code_missing">
<field name="text">The UNECE tax code is not configured for tax '%(taxname)s'.</field>
</record>
<record model="ir.message" id="msg_uom_code_missing"> <record model="ir.message" id="msg_uom_code_missing">
<field name="text">The UNECE uom code is not configured for unit '%(uomname)s'.</field> <field name="text">The UNECE uom code is not configured for unit '%(uomname)s'.</field>
</record> </record>
@ -23,15 +20,6 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_linetax_invalid_catcode"> <record model="ir.message" id="msg_linetax_invalid_catcode">
<field name="text">Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s).</field> <field name="text">Invalid category code at tax '%(taxname)s' (allowed: %(allowed)s).</field>
</record> </record>
<record model="ir.message" id="msg_no_seller_address">
<field name="text">There is no address for the seller party '%(sellerparty)s'.</field>
</record>
<record model="ir.message" id="msg_no_buyer_address">
<field name="text">There is no address for the buyer party '%(buyerparty)s'.</field>
</record>
<record model="ir.message" id="msg_no_address_country">
<field name="text">No country is specified for the address of the party '%(party)s'.</field>
</record>
</data> </data>
</tryton> </tryton>

206
mixin.py
View file

@ -1,206 +0,0 @@
# -*- 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 decimal import Decimal
import html
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.tools import cached_property
from trytond.pool import Pool
from trytond.modules.product import round_price
class EdocumentMixin(object):
""" functions to get field values for xml
"""
__slots__ = ()
def company_bank_accounts(self):
""" get leist of bank account numbers, defined in config
Returns:
list: records of model bank.account.number
"""
Configuration = Pool().get('account.configuration')
result = []
cfg1 = Configuration.get_singleton()
if cfg1 and cfg1.edocument_bank:
result.extend(list(cfg1.edocument_bank))
else:
result.extend([
y
for x in self.invoice.company.party.bank_accounts
for y in x.numbers])
return result
@cached_property
def seller_trade_address(self):
""" get address of seller, throw exception if incomplete
Raises:
UserError: if no address
UserError: if no country on address
Returns:
record : model party.address
"""
result = super().seller_trade_address
if not result:
raise UserError(gettext(
'edocument_xrechnung.msg_no_seller_address',
sellerparty=self.invoice.company.rec_name
if self.invoice and self.invoice.company
else '-'))
if not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party.rec_name if result.party else '-'))
return result
@cached_property
def buyer_trade_address(self):
""" exception if no address
Returns:
record: model party.address
"""
if (self.invoice.type == 'out') and (
not self.invoice.invoice_address):
raise UserError(gettext(
'edocument_xrechnung.msg_no_buyer_address',
buyerparty=self.invoice.party.rec_name
if self.invoice and self.invoice.party
else '-'))
result = super().buyer_trade_address
if result and not result.country:
raise UserError(gettext(
'edocument_xrechnung.msg_no_address_country',
party=result.party.rec_name if result.party else '-'))
return result
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 invoice_line_tax(self, line):
""" get tax of invoice-line,
fire exception if no/multiple taxes exists
"""
Tax = Pool().get('account.tax')
if len(line.taxes) != 1:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_number',
linename=line.rec_name,
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']
unece_category_code = self.get_category_code(tax)
if unece_category_code not in allowed_cat:
raise UserError(gettext(
'edocument_xrechnung.msg_linetax_invalid_catcode',
taxname=tax.rec_name,
allowed=', '.join(allowed_cat)))
return 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):
""" get tax-rate in procent
"""
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_unece_code(self, tax):
""" 'tax': invoice.line
"""
unece_code = self.get_tax_unece_code(tax)
if not unece_code:
raise UserError(gettext(
'edocument_xrechnung.msg_tax_code_missing',
taxname=tax.rec_name))
return tax.unece_code
def get_tax_unece_code(self, tax):
while tax:
if tax.unece_code:
return tax.unece_code
break
tax = tax.parent
def get_category_code(self, tax):
while tax:
if tax.unece_category_code:
return tax.unece_category_code
break
tax = tax.parent
def tax_category_code(self, tax):
""" read tax-category, fire exception if missing
"""
unece_category_code = self.get_category_code(tax)
if not unece_category_code:
raise UserError(gettext(
'edocument_xrechnung.mds_tax_category_missing',
taxname=tax.rec_name))
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):
""" replace critical chars
"""
if text:
return html.escape(text)
# end EdocumentMixin

View file

@ -14,10 +14,8 @@ class Party(metaclass=PoolMeta):
xrechnung_routeid = fields.Boolean( xrechnung_routeid = fields.Boolean(
string='X-Rechnung Route-ID', string='X-Rechnung Route-ID',
help='When activated an XRechnung route ID must be used ' help='Enables the need for an XRechnung route ID at the party ' +
'for this party for X-Rechnung exports.\n' 'for exporting the XRechnung.')
'The route ID must be defined as identifier of type '
'"X-Rechnung Route-ID".')
def get_xrechnung_route_id(self): def get_xrechnung_route_id(self):
""" search for route-id at party, fire-exception if missing """ search for route-id at party, fire-exception if missing
@ -47,7 +45,7 @@ class Party(metaclass=PoolMeta):
Args: Args:
records (list): records of party.party records (list): records of party.party
""" """
super().validate(records) super(Party, cls).validate(records)
for record in records: for record in records:
record.get_xrechnung_route_id() record.get_xrechnung_route_id()
@ -59,7 +57,7 @@ class PartyConfiguration(metaclass=PoolMeta):
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
super().__setup__() super(PartyConfiguration, cls).__setup__()
cls.identifier_types.selection.append( cls.identifier_types.selection.append(
('edoc_route_id', 'X-Rechnung Route-ID')) ('edoc_route_id', 'X-Rechnung Route-ID'))

View file

@ -13,9 +13,11 @@ 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'))
@ -85,8 +87,6 @@ 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',
@ -99,9 +99,7 @@ setup(
info.get('xml', []) info.get('xml', [])
+ ['tryton.cfg', 'locale/*.po', 'tests/*.py', + ['tryton.cfg', 'locale/*.po', 'tests/*.py',
'template/*/*.xml', 'versiondep.txt', 'README.rst', 'template/*/*.xml', 'versiondep.txt', 'README.rst',
'tests/*/*/*/*.xsd', 'view/*.xml', 'tests/*/*/*/*.xsd']),
'tests/*/*.xsd', 'tests/*/*.sch', 'tests/*/*.xml',
'tests/*/*/*.xslt', 'tests/*/*/*.xml']),
}, },
install_requires=requires, install_requires=requires,

View file

@ -12,7 +12,7 @@ this repository contains the full copyright notices and license terms. -->
<udt:DateTimeString format="102">${value.strftime('%Y%m%d')}</udt:DateTimeString> <udt:DateTimeString format="102">${value.strftime('%Y%m%d')}</udt:DateTimeString>
</py:def> </py:def>
<py:def function="TradeParty(party, address=None, tax_identifier=None)"> <py:def function="TradeParty(party, address=None, tax_identifier=None)">
<ram:Name>${this.quote_text(party.name)}</ram:Name> <ram:Name>${party.name}</ram:Name>
<ram:SpecifiedLegalOrganization> <ram:SpecifiedLegalOrganization>
<py:for each="id, attrs in this.party_legal_ids(party, address)"> <py:for each="id, attrs in this.party_legal_ids(party, address)">
<ram:ID py:attrs="attrs">${id}</ram:ID> <ram:ID py:attrs="attrs">${id}</ram:ID>
@ -26,21 +26,21 @@ this repository contains the full copyright notices and license terms. -->
<py:def function="TradeAddress(address)"> <py:def function="TradeAddress(address)">
<ram:PostcodeCode py:if="address.postal_code">${address.postal_code}</ram:PostcodeCode> <ram:PostcodeCode py:if="address.postal_code">${address.postal_code}</ram:PostcodeCode>
<py:with vars="lines = (address.street or '').splitlines()"> <py:with vars="lines = (address.street or '').splitlines()">
<ram:LineOne py:if="len(lines) > 0">${this.quote_text(lines[0])}</ram:LineOne> <ram:LineOne py:if="len(lines) > 0">${lines[0]}</ram:LineOne>
<ram:LineTwo py:if="len(lines) > 1">${this.quote_text(lines[1])}</ram:LineTwo> <ram:LineTwo py:if="len(lines) > 1">${lines[1]}</ram:LineTwo>
<ram:LineThree py:if="len(lines) > 2">${(lines[2])}</ram:LineThree> <ram:LineThree py:if="len(lines) > 2">${lines[2]}</ram:LineThree>
</py:with> </py:with>
<ram:CityName py:if="address.city">${(address.city)}</ram:CityName> <ram:CityName py:if="address.city">${address.city}</ram:CityName>
<ram:CountryID py:if="address.country">${address.country.code}</ram:CountryID> <ram:CountryID py:if="address.country">${address.country.code}</ram:CountryID>
<ram:CountrySubDivisionName py:if="address.subdivision">${this.quote_text(address.subdivision.name)}</ram:CountrySubDivisionName> <ram:CountrySubDivisionName py:if="address.subdivision">${address.subdivision.name}</ram:CountrySubDivisionName>
</py:def> </py:def>
<py:def function="TradeTax(tax, amount=None, base=None)"> <py:def function="TradeTax(tax, amount=None, base=None)">
<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 py:if="tax.unece_code">${tax.unece_code}</ram:TypeCode>
<ram:ExemptionReason py:if="tax.legal_notice">${tax.legal_notice}</ram:ExemptionReason> <ram:ExemptionReason py:if="tax.legal_notice">${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 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:RateApplicablePercent py:if="tax.type == 'percentage'">${tax.rate * 100}</ram:RateApplicablePercent>
</ram:ApplicableTradeTax> </ram:ApplicableTradeTax>
</py:def> </py:def>
@ -50,8 +50,8 @@ this repository contains the full copyright notices and license terms. -->
</ram:GuidelineSpecifiedDocumentContextParameter> </ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext> </rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument> <rsm:ExchangedDocument>
<ram:ID>${this.quote_text(this.invoice.number)}</ram:ID> <ram:ID>${this.invoice.number}</ram:ID>
<ram:Name py:if="this.invoice.description">${this.quote_text(this.invoice.description)}</ram:Name> <ram:Name py:if="this.invoice.description">${this.invoice.description}</ram:Name>
<ram:TypeCode>${this.type_code}</ram:TypeCode> <ram:TypeCode>${this.type_code}</ram:TypeCode>
<ram:IssueDateTime> <ram:IssueDateTime>
${DateTime(this.invoice.invoice_date)} ${DateTime(this.invoice.invoice_date)}
@ -69,21 +69,23 @@ this repository contains the full copyright notices and license terms. -->
<ram:AssociatedDocumentLineDocument> <ram:AssociatedDocumentLineDocument>
<ram:LineID>${line_id}</ram:LineID> <ram:LineID>${line_id}</ram:LineID>
</ram:AssociatedDocumentLineDocument> </ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct> <ram:SpecifiedTradeProduct py:if="line.product">
<ram:ID py:if="line.product and line.product.code">${line.product.code}</ram:ID> <ram:ID py:if="line.product.code">${line.product.code}</ram:ID>
<ram:Name>${this.quote_text(line.product.name if line.product else '')}</ram:Name> <ram:Name>${line.product.name}</ram:Name>
<ram:Description py:if="line.description">${this.quote_text(line.description)}</ram:Description> <ram:Description py:if="line.description">${line.description}</ram:Description>
</ram:SpecifiedTradeProduct> </ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement> <ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice> <ram:NetPriceProductTradePrice>
<ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.round_unitprice(line.unit_price)}</ram:ChargeAmount> <ram:ChargeAmount py:attrs="{'currencyID': this.invoice.currency.code}">${this.invoice.currency.round(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>
${TradeTax(this.invoice_line_tax(line))} <py:for each="tax in line.invoice_taxes">
${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}">${line.amount}</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation> </ram:SpecifiedTradeSettlementLineMonetarySummation>
@ -97,10 +99,10 @@ this repository contains the full copyright notices and license terms. -->
${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)} ${TradeParty(this.buyer_trade_party, this.buyer_trade_address, this.buyer_trade_tax_identifier)}
</ram:BuyerTradeParty> </ram:BuyerTradeParty>
<ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'"> <ram:SellerOrderReferencedDocument py:if="this.invoice.type == 'in'">
<ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID> <ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:SellerOrderReferencedDocument> </ram:SellerOrderReferencedDocument>
<ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'"> <ram:BuyerOrderReferencedDocument py:if="this.invoice.type == 'out'">
<ram:IssuerAssignedID>${this.quote_text(this.invoice.reference)}</ram:IssuerAssignedID> <ram:IssuerAssignedID>${this.invoice.reference}</ram:IssuerAssignedID>
</ram:BuyerOrderReferencedDocument> </ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement> </ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery> <ram:ApplicableHeaderTradeDelivery>
@ -112,26 +114,11 @@ this repository contains the full copyright notices and license terms. -->
</ram:ShipFromTradeParty> </ram:ShipFromTradeParty>
</ram:ApplicableHeaderTradeDelivery> </ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement> <ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>${this.quote_text(this.payment_reference)}</ram:PaymentReference> <ram:PaymentReference>${this.payment_reference}</ram:PaymentReference>
<ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode> <ram:InvoiceCurrencyCode>${this.invoice.currency.code}</ram:InvoiceCurrencyCode>
<py:if test="len(this.company_bank_accounts()) == 0"> <ram:SpecifiedTradeSettlementPaymentMeans>
<ram:SpecifiedTradeSettlementPaymentMeans> <ram:TypeCode>1</ram:TypeCode> <!-- Instrument not defined -->
<ram:TypeCode>1</ram:TypeCode> </ram:SpecifiedTradeSettlementPaymentMeans>
</ram:SpecifiedTradeSettlementPaymentMeans>
</py:if>
<py:for each="banknumber in this.company_bank_accounts()">
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>30</ram:TypeCode>
<ram:Information>Wire transfer</ram:Information>
<ram:PayeePartyCreditorFinancialAccount>
<ram:IBANID>${banknumber.number_compact}</ram:IBANID>
<ram:AccountName>${this.quote_text(banknumber.account.bank.party.rec_name)}</ram:AccountName>
</ram:PayeePartyCreditorFinancialAccount>
<ram:PayeeSpecifiedCreditorFinancialInstitution>
<ram:BICID>${banknumber.account.bank.bic}</ram:BICID>
</ram:PayeeSpecifiedCreditorFinancialInstitution>
</ram:SpecifiedTradeSettlementPaymentMeans>
</py:for>
<py:for each="tax in this.invoice.taxes"> <py:for each="tax in this.invoice.taxes">
${TradeTax(tax.tax, tax.amount, tax.base)} ${TradeTax(tax.tax, tax.amount, tax.base)}
</py:for> </py:for>

View file

@ -5,206 +5,19 @@
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
from trytond.pool import Pool from trytond.pool import Pool
from trytond.modules.company.tests import create_company, set_company from trytond.modules.edocument_uncefact.tests.test_module import get_invoice
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'
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], {
'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):
""" 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')])
tax, = Taxes.search([('name', '=', '20% VAT')])
Taxes.write(*[
[tax],
{'unece_code': 'GST', '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',
'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,
}])],
}]
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()
def test_xrechn_bank_account_owned(self):
""" check field 'company_owned' on bank.account.number
"""
pool = Pool()
BankAccount = pool.get('bank.account')
AccountNumber = pool.get('bank.account.number')
Bank = pool.get('bank')
Party = pool.get('party.party')
company = create_company()
with set_company(company):
bank_party, = Party.create([{
'name': 'Bank 123',
'addresses': [('create', [{}])]}])
customer_party, = Party.create([{
'name': 'Someone',
'addresses': [('create', [{}])]}])
bank, = Bank.create([{'party': bank_party.id}])
acc_company, acc_other, = BankAccount.create([
{
'bank': bank.id,
'owners': [('add', [company.party.id])],
'numbers': [('create', [
{'type': 'iban', 'number': 'DE02300209000106531065'}])]
}, {
'bank': bank.id,
'owners': [('add', [customer_party.id])],
'numbers': [('create', [
{'type': 'iban', 'number': 'DE02200505501015871393'}])]
}])
self.assertEqual(len(acc_company.numbers), 1)
self.assertEqual(acc_company.numbers[0].company_owned, True)
self.assertEqual(len(acc_other.numbers), 1)
self.assertEqual(acc_other.numbers[0].company_owned, False)
company_numbers = AccountNumber.search(
[('company_owned', '=', True)])
self.assertEqual(len(company_numbers), 1)
self.assertEqual(company_numbers[0].id, acc_company.numbers[0].id)
other_numbers = AccountNumber.search(
[('company_owned', '=', False)])
self.assertEqual(len(other_numbers), 1)
self.assertEqual(other_numbers[0].id, acc_other.numbers[0].id)
@with_transaction() @with_transaction()
def test_xrechn_check_validator(self): def test_xrechn_check_validator(self):
""" check validation of optional route-id """ check validation of optional route-id
@ -239,24 +52,45 @@ class EdocTestCase(ModuleTestCase):
""" """
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')
company = self.prep_company() invoice = get_invoice()
with set_company(company): invoice.payment_term_date = date.today()
create_chart(company=company, tax=True) invoice.party.get_xrechnung_route_id = Mock(
self.prep_fiscalyear(company) return_value='xrechn-route-id-123')
invoice = self.prep_invoice() 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)
schema_file = os.path.join( schema_file = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'Factur-X_1.07.2_EXTENDED', 'Factur-X_1.07.2_EXTENDED',
'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')
invoice_xml = etree.fromstring(invoice_string) invoice_xml = etree.fromstring(invoice_string)
schema = etree.XMLSchema(etree.parse(schema_file)) schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml) schema.assertValid(invoice_xml)
@with_transaction() @with_transaction()
def test_xrechn_export_xml_invoice(self): def test_xrechn_export_xml_invoice(self):
@ -264,24 +98,45 @@ 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')
company = self.prep_company() invoice = get_invoice()
with set_company(company): invoice.payment_term_date = date.today()
create_chart(company=company, tax=True) invoice.party.get_xrechnung_route_id = Mock(
self.prep_fiscalyear(company) return_value='xrechn-route-id-123')
invoice = self.prep_invoice() 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)
schema_file = os.path.join( schema_file = os.path.join(
os.path.dirname(__file__), 'os-UBL-2.1', os.path.dirname(__file__), 'os-UBL-2.1',
'xsd', 'maindoc', 'UBL-Invoice-2.1.xsd') 'xsd', 'maindoc', 'UBL-Invoice-2.1.xsd')
for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']: for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']:
invoice_string = template.render(x) invoice_string = template.render(x)
invoice_xml = etree.fromstring(invoice_string) invoice_xml = etree.fromstring(invoice_string)
schema = etree.XMLSchema(etree.parse(schema_file)) schema = etree.XMLSchema(etree.parse(schema_file))
schema.assertValid(invoice_xml) schema.assertValid(invoice_xml)
@with_transaction() @with_transaction()
def test_xrechn_export_xml_creditnote(self): def test_xrechn_export_xml_creditnote(self):
@ -289,24 +144,59 @@ 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')
company = self.prep_company() invoice = get_invoice()
with set_company(company):
create_chart(company=company, tax=True)
self.prep_fiscalyear(company)
invoice = self.prep_invoice(credit_note=True)
template = Template(invoice) # credit note
invoice.lines[0].quantity = -1
invoice.lines[0].amount = Decimal('-100.0')
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')
schema_file = os.path.join( invoice.party.get_xrechnung_route_id = Mock(
os.path.dirname(__file__), 'os-UBL-2.1', return_value='xrechn-route-id-123')
'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd') 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')
]
for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']: template = Template(invoice)
invoice_string = template.render(x)
invoice_xml = etree.fromstring(invoice_string) schema_file = os.path.join(
schema = etree.XMLSchema(etree.parse(schema_file)) os.path.dirname(__file__), 'os-UBL-2.1',
schema.assertValid(invoice_xml) 'xsd', 'maindoc', 'UBL-CreditNote-2.1.xsd')
for x in ['XRechnung-2.2', 'XRechnung-2.3', 'XRechnung-3.0']:
invoice_string = template.render(x)
invoice_xml = etree.fromstring(invoice_string)
schema = etree.XMLSchema(etree.parse(schema_file))
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

@ -1,5 +1,5 @@
[tryton] [tryton]
version=7.0.10 version=7.0.5
depends: depends:
edocument_uncefact edocument_uncefact
party party
@ -7,5 +7,4 @@ depends:
account_invoice account_invoice
xml: xml:
message.xml message.xml
configuration.xml
party.xml party.xml

View file

@ -1,15 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of the account-datev-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<data>
<xpath expr="/form/separator[@id='currency_exchange']" position="before">
<separator id="edocument" colspan="4" string="eDocument"/>
<field name="edocument_bank" colspan="2" height="200"/>
<newline/>
</xpath>
</data>