Merge branch 'main' into 7.0

This commit is contained in:
Frederik Jaeckel 2024-12-09 15:47:25 +01:00
commit 675e103fd9
17 changed files with 869 additions and 56 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
PYTHONPATH=~/Projekte/tr70/lib/python3.10/site-packages

View file

@ -1,4 +1,4 @@
syntax: glob *.pyc
build/* build/*
dist/* dist/*
mds_account_invoice_xrechnung.egg-info/* mds_account_invoice_xrechnung.egg-info/*

View file

@ -7,10 +7,13 @@ from trytond.pool import Pool
from .wizard_runreport import RunXRechnungReport, RunXRechnungReportStart from .wizard_runreport import RunXRechnungReport, RunXRechnungReportStart
from .invoice import InvoiceLine from .invoice import InvoiceLine
from .xreport import XReport from .xreport import XReport
from .configuration import ConfigurationXRechnungexport, Configuration
def register(): def register():
Pool.register( Pool.register(
Configuration,
ConfigurationXRechnungexport,
InvoiceLine, InvoiceLine,
RunXRechnungReportStart, RunXRechnungReportStart,
module='account_invoice_xrechnung', type_='model') module='account_invoice_xrechnung', type_='model')

62
configuration.py Normal file
View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# This file is part of the account-invoice-xrechnung-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import PoolMeta, Pool
from trytond.model import ModelSQL, fields
from trytond.modules.company.model import CompanyValueMixin
from .wizard_runreport import sel_edocument
class Configuration(metaclass=PoolMeta):
__name__ = 'account.configuration'
xrechn_default = fields.MultiValue(fields.Selection(
string='Export mode', selection=sel_edocument,
help='Pre-set export format for e-invoices.'))
xrechn_zugferd_report = fields.MultiValue(fields.Many2One(
model_name='ir.action.report',
domain=[
('model', '=', 'account.invoice'),
('extension', '=', 'pdf')],
string='ZUGFeRD-Report',
help='Report that is to be used to generate the ZUGFeRD PDF.'))
@classmethod
def multivalue_model(cls, field):
""" select table
"""
pool = Pool()
if field in {'xrechn_zugferd_report', 'xrechn_default'}:
return pool.get('account_invoice_xrechnung.configuration')
return super(Configuration, cls).multivalue_model(field)
@classmethod
def default_xrechn_default(cls, **pattern):
return cls.multivalue_model('xrechn_default').default_xrechn_default()
# end Configuration
class ConfigurationXRechnungexport(ModelSQL, CompanyValueMixin):
"Account Configuration XRechnung Export"
__name__ = 'account_invoice_xrechnung.configuration'
xrechn_default = fields.Selection(
string='Export mode', selection=sel_edocument,
help='Pre-set export format for e-invoices.')
xrechn_zugferd_report = fields.Many2One(
model_name='ir.action.report',
string='ZUGFeRD-Report',
domain=[
('model', '=', 'account.invoice'),
('extension', '=', 'pdf')],
help='Report that is to be used to generate the ZUGFeRD PDF.')
@classmethod
def default_xrechn_default(cls, **pattern):
return 'edocument.facturxext.invoice-ferd'
# end ConfigurationXRechnungexport

15
configuration.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the account-invoice-xrechnung-module
from m-ds for Tryton. 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

@ -4,7 +4,7 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from trytond.pool import PoolMeta from trytond.pool import PoolMeta
from trytond.pyson import Eval, And, Or from trytond.pyson import Eval, And, Or, Bool
class InvoiceLine(metaclass=PoolMeta): class InvoiceLine(metaclass=PoolMeta):
@ -17,10 +17,7 @@ class InvoiceLine(metaclass=PoolMeta):
cls.unit.states['required'], cls.unit.states['required'],
And( And(
Eval('type') == 'line', Eval('type') == 'line',
Eval('quantity', None) != None, Bool(Eval('quantity'))))
), cls.unit.depends.update(['type', 'quantity'])
)
cls.unit.depends.add('type')
cls.unit.depends.add('quantity')
# end Invoice # end Invoice

View file

@ -3,6 +3,22 @@ msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n" msgstr "Content-Type: text/plain; charset=utf-8\n"
##############
# ir.message #
##############
msgctxt "model:ir.message,text:msg_invoice_must_posted"
msgid "Invoice '%(invname)s' must be posted."
msgstr "Rechnung '%(invname)s' muß festgeschrieben sein."
msgctxt "model:ir.message,text:msg_no_report_found"
msgid "No report found for invoices in PDF format."
msgstr "Kein Report für Rechnungen im PDF-Format gefunden."
msgctxt "model:ir.message,text:msg_invalid_cachecontent"
msgid "No PDF has yet been generated for the invoice '%(invoice_name)s' or the saved document has an incorrect format."
msgstr "Für die Rechnung '%(invoice_name)s' wurde noch keine PDF erzeugt oder das gespeicherte Dokument hat ein falsches Format."
############# #############
# ir.action # # ir.action #
############# #############
@ -30,6 +46,30 @@ msgctxt "field:account_invoice_xrechnung.runrep.start,edocument:"
msgid "Type" msgid "Type"
msgstr "Typ" msgstr "Typ"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "field:account_invoice_xrechnung.runrep.start,as_zip:" msgctxt "field:account_invoice_xrechnung.runrep.start,as_zip:"
msgid "ZIP-File" msgid "ZIP-File"
msgstr "ZIP-Datei" msgstr "ZIP-Datei"
@ -57,3 +97,99 @@ msgstr "Export"
msgctxt "model:account_invoice_xrechnung.export,name:" msgctxt "model:account_invoice_xrechnung.export,name:"
msgid "eDocument Export" msgid "eDocument Export"
msgstr "eDocument Export" msgstr "eDocument Export"
#########################
# account.configuration #
#########################
msgctxt "view:account.configuration:"
msgid "ZUGFeRD - e-Invoice"
msgstr "ZUGFeRD - e-Rechnung"
msgctxt "field:account.configuration,xrechn_zugferd_report:"
msgid "ZUGFeRD-Report"
msgstr "ZUGFeRD-Report"
msgctxt "help:account.configuration,xrechn_zugferd_report:"
msgid "Report that is to be used to generate the ZUGFeRD PDF."
msgstr "Report, welcher zum erzeugen der ZUGFeRD-PDF verwendet werden soll."
msgctxt "field:account.configuration,xrechn_default:"
msgid "Export mode"
msgstr "Exportformat"
msgctxt "help:account.configuration,xrechn_default:"
msgid "Pre-set export format for e-invoices."
msgstr "Voreingstelltes Exportformat für die eRechnung."
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"
###########################################
# account_invoice_xrechnung.configuration #
###########################################
msgctxt "model:account_invoice_xrechnung.configuration:"
msgid "Account Configuration XRechnung Export"
msgstr "Buchhaltung Konfiguration XRechnung Export"
msgctxt "field:account_invoice_xrechnung.configuration,xrechn_zugferd_report:"
msgid "ZUGFeRD-Report"
msgstr "ZUGFeRD-Report"
msgctxt "help:account_invoice_xrechnung.configuration,xrechn_zugferd_report:"
msgid "Report that is to be used to generate the ZUGFeRD PDF."
msgstr "Report, welcher zum erzeugen der ZUGFeRD-PDF verwendet werden soll."
msgctxt "field:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Export mode"
msgstr "Exportformat"
msgctxt "help:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Pre-set export format for e-invoices."
msgstr "Voreingstelltes Exportformat für die eRechnung."
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"

View file

@ -2,6 +2,18 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n" msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "model:ir.message,text:msg_invoice_must_posted"
msgid "Invoice '%(invname)s' must be posted."
msgstr "Invoice '%(invname)s' must be posted."
msgctxt "model:ir.message,text:msg_no_report_found"
msgid "No report found for invoices in PDF format."
msgstr "No report found for invoices in PDF format."
msgctxt "model:ir.message,text:msg_invalid_cachecontent"
msgid "No PDF has yet been generated for the invoice '%(invoice_name)s' or the saved document has an incorrect format."
msgstr "No PDF has yet been generated for the invoice '%(invoice_name)s' or the saved document has an incorrect format."
msgctxt "model:ir.action,name:act_wizard_report" msgctxt "model:ir.action,name:act_wizard_report"
msgid "eDocument Export" msgid "eDocument Export"
msgstr "eDocument Export" msgstr "eDocument Export"
@ -22,6 +34,30 @@ msgctxt "field:account_invoice_xrechnung.runrep.start,edocument:"
msgid "Type" msgid "Type"
msgstr "Type" msgstr "Type"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account_invoice_xrechnung.runrep.start,edocument:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "field:account_invoice_xrechnung.runrep.start,as_zip:" msgctxt "field:account_invoice_xrechnung.runrep.start,as_zip:"
msgid "ZIP-File" msgid "ZIP-File"
msgstr "ZIP-File" msgstr "ZIP-File"
@ -38,3 +74,94 @@ msgctxt "wizard_button:account_invoice_xrechnung.runrep,start,export:"
msgid "Export" msgid "Export"
msgstr "Export" msgstr "Export"
msgctxt "model:account_invoice_xrechnung.export,name:"
msgid "eDocument Export"
msgstr "eDocument Export"
msgctxt "view:account.configuration:"
msgid "ZUGFeRD - e-Invoice"
msgstr "ZUGFeRD - e-Invoice"
msgctxt "field:account.configuration,xrechn_zugferd_report:"
msgid "ZUGFeRD-Report"
msgstr "ZUGFeRD-Report"
msgctxt "help:account.configuration,xrechn_zugferd_report:"
msgid "Report that is to be used to generate the ZUGFeRD PDF."
msgstr "Report that is to be used to generate the ZUGFeRD PDF."
msgctxt "field:account.configuration,xrechn_default:"
msgid "Export mode"
msgstr "Export mode"
msgctxt "help:account.configuration,xrechn_default:"
msgid "Pre-set export format for e-invoices."
msgstr "Pre-set export format for e-invoices."
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "selection:account.configuration,xrechn_default:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"
msgctxt "model:account_invoice_xrechnung.configuration:"
msgid "Account Configuration XRechnung Export"
msgstr "Account Configuration XRechnung Export"
msgctxt "field:account_invoice_xrechnung.configuration,xrechn_zugferd_report:"
msgid "ZUGFeRD-Report"
msgstr "ZUGFeRD-Report"
msgctxt "help:account_invoice_xrechnung.configuration,xrechn_zugferd_report:"
msgid "Report that is to be used to generate the ZUGFeRD PDF."
msgstr "Report that is to be used to generate the ZUGFeRD PDF."
msgctxt "field:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Export mode"
msgstr "Export mode"
msgctxt "help:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Pre-set export format for e-invoices."
msgstr "Pre-set export format for e-invoices."
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.2"
msgstr "XRechnung UBL Invoice 2.2"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 2.3"
msgstr "XRechnung UBL Invoice 2.3"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "XRechnung UBL Invoice 3.0"
msgstr "XRechnung UBL Invoice 3.0"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "Factur-X Extended"
msgstr "Factur-X Extended"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "ZUGFeRD 2.3.2"
msgstr "ZUGFeRD 2.3.2"
msgctxt "selection:account_invoice_xrechnung.configuration,xrechn_default:"
msgid "CII CrossIndustryInvoice D16B"
msgstr "CII CrossIndustryInvoice D16B"

View file

@ -8,6 +8,12 @@
<record model="ir.message" id="msg_invoice_must_posted"> <record model="ir.message" id="msg_invoice_must_posted">
<field name="text">Invoice '%(invname)s' must be posted.</field> <field name="text">Invoice '%(invname)s' must be posted.</field>
</record> </record>
<record model="ir.message" id="msg_no_report_found">
<field name="text">No report found for invoices in PDF format.</field>
</record>
<record model="ir.message" id="msg_invalid_cachecontent">
<field name="text">No PDF has yet been generated for the invoice '%(invoice_name)s' or the saved document has an incorrect format.</field>
</record>
</data> </data>
</tryton> </tryton>

View file

@ -1,16 +1,13 @@
""" Tryton module to add xrechnung-export to invoice # -*- coding: utf-8 -*-
""" # This file is part of the account-invoice-xrechnung-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
# Always prefer setuptools over distutils
from setuptools import setup from setuptools import setup
# To use a consistent encoding
from codecs import open from codecs import open
from os import path from os import path
import re import re
try: from configparser import ConfigParser
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
MODULE = 'account_invoice_xrechnung' MODULE = 'account_invoice_xrechnung'
@ -20,7 +17,6 @@ PREFIX = 'mds'
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'))
@ -42,7 +38,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
major_version = 7 major_version = 7
minor_version = 0 minor_version = 0
requires = ['python-slugify'] requires = ['python-slugify', 'pypdf', 'factur-x']
for dep in info.get('depends', []): for dep in info.get('depends', []):
if not re.match(r'(ir|res|webdav)(\W|$)', dep): if not re.match(r'(ir|res|webdav)(\W|$)', dep):
if dep in modversion.keys(): if dep in modversion.keys():
@ -87,9 +83,9 @@ setup(
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'License :: OSI Approved :: GNU General Public License (GPL)', 'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 3.7',
'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',
], ],
keywords='tryton account invoice xrechnung edocument', keywords='tryton account invoice xrechnung edocument',

View file

@ -3,18 +3,235 @@
# from m-ds for Tryton. The COPYRIGHT file at the top level of # from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from decimal import Decimal
from datetime import date
from facturx import get_facturx_xml_from_pdf
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.transaction import Transaction
from trytond.modules.company.tests import create_company, set_company
from trytond.modules.account.tests import create_chart, get_fiscalyear
from .xml_data import xml_from_pdf
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 InvoiceTestCase(ModuleTestCase): class InvoiceTestCase(ModuleTestCase):
'Test invoice module' 'Test invoice module'
module = 'account_invoice_xrechnung' module = 'account_invoice_xrechnung'
@with_transaction() def prep_fiscalyear(self, company1):
def test_xrechnung(self): """ prepare fiscal year, sequences...
""" run default tests
""" """
pass 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_invoice(self, party_customer):
""" 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')
currency1, = Currency.search([('code', '=', 'usd')])
tax_lst = Taxes.search([('name', '=', '20% VAT')])
self.assertEqual(len(tax_lst), 1)
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': 'Parts',
'invoice_date': date(2024, 7, 1),
'party': party_customer.id,
'invoice_address': party_customer.addresses[0].id,
'account': account_lst[0].id,
'journal': journ_lst[0].id,
'currency': currency1.id,
'lines': [('create', [{
'type': 'line',
'quantity': 2.0,
'description': 'Product 1',
'unit': Uom.search([('symbol', '=', 'u')])[0].id,
'unit_price': Decimal('50.0'),
'taxes': [('add', [tax_lst[0].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_xrechnung_configuration(self):
""" test configuration
"""
pool = Pool()
Configuration = pool.get('account.configuration')
Party = pool.get('party.party')
Country = pool.get('country.country')
ActionReport = pool.get('ir.action.report')
ExportWiz = pool.get('account_invoice_xrechnung.runrep', type='wizard')
Tax = pool.get('account.tax')
country_de, = Country.create([{
'name': 'Germany',
'code': 'DE',
'code3': 'DEU'}])
pty1, = Party.create([{
'name': 'Payee',
'addresses': [('create', [{
'invoice': True,
'street': 'Applicant Street 1',
'postal_code': '12345',
'city': 'Usertown',
'country': country_de.id,
}])],
}])
company1 = create_company('m-ds')
Party.write(*[
[company1.party],
{'addresses': [(
'write',
[company1.party.addresses[0]],
{'country': country_de.id})]}])
with set_company(company1):
with Transaction().set_context({'company': company1.id}):
# update report to 'pdf'
inv_report, = ActionReport.search([
('model', '=', 'account.invoice'),
('report_name', '=', 'account.invoice')])
self.assertEqual(inv_report.extension, '')
ActionReport.write(*[
[inv_report], {'extension': 'pdf'}])
cfg1 = Configuration(xrechn_zugferd_report=inv_report)
cfg1.save()
self.assertEqual(cfg1.xrechn_zugferd_report.name, 'Invoice')
create_chart(company=company1, tax=True)
self.prep_fiscalyear(company1)
tax, = Tax.search([('name', '=', '20% VAT')])
Tax.write(*[
[tax],
{'unece_code': 'GST', 'unece_category_code': 'S'}])
invoice = self.prep_invoice(pty1)
# start wizard with two selected records
with Transaction().set_context({
'active_ids': [invoice.id],
'active_id': invoice.id,
'active_model': 'account.invoice'}):
(sess_id, start_state, end_state) = ExportWiz.create()
w_obj = ExportWiz(sess_id)
self.assertEqual(start_state, 'start')
self.assertEqual(end_state, 'end')
result = ExportWiz.execute(sess_id, {}, start_state)
self.assertEqual(
list(result['view']['defaults'].keys()), [
'as_zip', 'edocument', 'invoice', 'state',
'invoice.'])
data = {}
for x in result['view']['defaults'].keys():
if '.' in x:
continue
data[x] = result['view']['defaults'][x]
setattr(w_obj.start, x, data[x])
self.assertEqual(
w_obj.start.edocument,
'edocument.facturxext.invoice-ferd')
self.assertEqual(w_obj.start.invoice, invoice)
self.assertEqual(w_obj.start.as_zip, True)
w_obj.start.as_zip = False
data['as_zip'] = False
# (action, data)
result = ExportWiz.execute(
sess_id, {'start': data}, 'export')
self.assertEqual(len(result['actions']), 1)
(action, data) = result['actions'][0]
self.assertEqual(
action['report_name'],
'account_invoice_xrechnung.export')
self.assertEqual(action['type'], 'ir.action.report')
self.assertEqual(action['records'], 'selected')
# 2nd step, wizard told us which report we must execute
ReportExport = pool.get(
'account_invoice_xrechnung.export',
type='report')
data2 = {}
data2.update(data)
data2['action_id'] = action['id']
data2['model'] = 'account.invoice'
(ext, pdfdata, dprint, fname) = ReportExport.execute(
[data['invoice']], data2)
# extract xml
(xml_fname, xml_frompdf) = get_facturx_xml_from_pdf(
pdfdata)
self.assertEqual(xml_fname, 'factur-x.xml')
self.assertEqual(
xml_frompdf.decode('utf8'),
xml_from_pdf % {
'datetoday': date.today().strftime('%Y%m%d')})
ExportWiz.delete(sess_id)
# end InvoiceTestCase # end InvoiceTestCase

104
tests/xml_data.py Normal file
View file

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# This file is part of the account-invoice-xrechnung-module
# from m-ds for Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
xml_from_pdf = """<?xml version="1.0" encoding="UTF-8"?>
<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:xsi="http://www.w3.org/2001/XMLSchema-instance">
<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>1</ram:ID>
<ram:Name>Parts</ram:Name>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240701</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
<ram:SpecifiedTradeProduct>
<ram:Name></ram:Name>
<ram:Description>Product 1</ram:Description>
</ram:SpecifiedTradeProduct>
<ram:SpecifiedLineTradeAgreement>
<ram:NetPriceProductTradePrice>
<ram:ChargeAmount currencyID="usd">50.00</ram:ChargeAmount>
</ram:NetPriceProductTradePrice>
</ram:SpecifiedLineTradeAgreement>
<ram:SpecifiedLineTradeDelivery>
<ram:BilledQuantity unitCode="C62">2.0</ram:BilledQuantity>
</ram:SpecifiedLineTradeDelivery>
<ram:SpecifiedLineTradeSettlement>
<ram:ApplicableTradeTax>
<ram:TypeCode>GST</ram:TypeCode>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>20.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradeSettlementLineMonetarySummation>
<ram:LineTotalAmount currencyID="usd">100.00</ram:LineTotalAmount>
</ram:SpecifiedTradeSettlementLineMonetarySummation>
</ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>m-ds</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>Payee</ram:Name>
<ram:SpecifiedLegalOrganization>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:PostcodeCode>12345</ram:PostcodeCode>
<ram:LineOne>Applicant Street 1</ram:LineOne>
<ram:CityName>Usertown</ram:CityName>
<ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>
</ram:BuyerTradeParty>
<ram:BuyerOrderReferencedDocument>
<ram:IssuerAssignedID/>
</ram:BuyerOrderReferencedDocument>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery>
</ram:ApplicableHeaderTradeDelivery>
<ram:ApplicableHeaderTradeSettlement>
<ram:PaymentReference>1</ram:PaymentReference>
<ram:InvoiceCurrencyCode>usd</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementPaymentMeans>
<ram:TypeCode>1</ram:TypeCode>
</ram:SpecifiedTradeSettlementPaymentMeans>
<ram:ApplicableTradeTax>
<ram:CalculatedAmount currencyID="usd">20.00</ram:CalculatedAmount>
<ram:TypeCode>GST</ram:TypeCode>
<ram:BasisAmount>100.00</ram:BasisAmount>
<ram:CategoryCode>S</ram:CategoryCode>
<ram:RateApplicablePercent>20.0</ram:RateApplicablePercent>
</ram:ApplicableTradeTax>
<ram:SpecifiedTradePaymentTerms>
<ram:DueDateDateTime>
<udt:DateTimeString format="102">%(datetoday)s</udt:DateTimeString>
</ram:DueDateDateTime>
<ram:PartialPaymentAmount currencyID="usd">120.00</ram:PartialPaymentAmount>
</ram:SpecifiedTradePaymentTerms>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:LineTotalAmount currencyID="usd">100.00</ram:LineTotalAmount>
<ram:TaxBasisTotalAmount currencyID="usd">100.00</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="usd">20.00</ram:TaxTotalAmount>
<ram:GrandTotalAmount currencyID="usd">120.00</ram:GrandTotalAmount>
<ram:DuePayableAmount currencyID="usd">120.00</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>"""

View file

@ -6,5 +6,6 @@ depends:
edocument_xrechnung edocument_xrechnung
xml: xml:
message.xml message.xml
configuration.xml
wizard_runreport.xml wizard_runreport.xml
xreport.xml xreport.xml

View file

@ -1 +1 @@
edocument_xrechnung;7.0.3;7.0.999;mds edocument_xrechnung;7.0.5;7.0.999;mds

View file

@ -0,0 +1,17 @@
<?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="xrechnung" colspan="4" string="ZUGFeRD - e-Invoice"/>
<label name="xrechn_zugferd_report"/>
<field name="xrechn_zugferd_report"/>
<label name="xrechn_default"/>
<field name="xrechn_default"/>
</xpath>
</data>

View file

@ -12,14 +12,21 @@ from trytond.transaction import Transaction
sel_edocument = [ sel_edocument = [
('edocument.xrechnung.invoice', 'XRechnung UBL Invoice 2.1.1'), ('edocument.xrechnung.invoice-2.2', 'XRechnung UBL Invoice 2.2'),
('edocument.xrechnung.invoice-2.3', 'XRechnung UBL Invoice 2.3'),
('edocument.xrechnung.invoice-3.0', 'XRechnung UBL Invoice 3.0'),
('edocument.facturxext.invoice', 'Factur-X Extended'),
('edocument.facturxext.invoice-ferd', 'ZUGFeRD 2.3.2'),
('edocument.uncefact.invoice', 'CII CrossIndustryInvoice D16B'), ('edocument.uncefact.invoice', 'CII CrossIndustryInvoice D16B'),
] ]
edoc_versions = { edoc_versions = {
'edocument.xrechnung.invoice': 'XRechnung-2.2', 'edocument.xrechnung.invoice-2.2': 'XRechnung-2.2',
'edocument.uncefact.invoice': '16B-CII', 'edocument.xrechnung.invoice-2.3': 'XRechnung-2.3',
} 'edocument.xrechnung.invoice-3.0': 'XRechnung-3.0',
'edocument.facturxext.invoice': 'Factur-X-1.07.2-extended',
'edocument.facturxext.invoice-ferd': 'Factur-X-1.07.2-extended',
'edocument.uncefact.invoice': '16B-CII'}
class RunXRechnungReportStart(ModelView): class RunXRechnungReportStart(ModelView):
@ -45,7 +52,7 @@ class RunXRechnungReportStart(ModelView):
def default_edocument(cls): def default_edocument(cls):
""" default xrechnung """ default xrechnung
""" """
return 'edocument.xrechnung.invoice' return 'edocument.xrechnung.invoice-3.0'
# end RunXRechnungReportStart # end RunXRechnungReportStart
@ -62,35 +69,75 @@ class RunXRechnungReport(Wizard):
buttons=[ buttons=[
Button(string='Cancel', state='end', icon='tryton-cancel'), Button(string='Cancel', state='end', icon='tryton-cancel'),
Button(string='Export', state='export', icon='tryton-export'), Button(string='Export', state='export', icon='tryton-export'),
], ])
)
def default_start(self, fields): def default_start(self, fields):
""" set defaults """ set defaults
""" """
context = Transaction().context context = Transaction().context
Invoice = Pool().get('account.invoice') pool = Pool()
Invoice = pool.get('account.invoice')
WizRepStart = pool.get('account_invoice_xrechnung.runrep.start')
Configuration = pool.get('account.configuration')
cfg1 = Configuration.get_singleton()
invoice = Invoice.browse([context.get('active_id', -1)]) invoice = Invoice.browse([context.get('active_id', -1)])
result = { result = {
'edocument': 'edocument.xrechnung.invoice', 'edocument': WizRepStart.default_edocument(),
'invoice': context.get('active_id', -1), 'invoice': context.get('active_id', -1),
'state': invoice[0].state if len(invoice) > 0 else '', 'state': invoice[0].state if invoice else ''}
} if cfg1 and cfg1.xrechn_default:
result['edocument'] = cfg1.xrechn_default
return result return result
def generate_invoice_reports(self, data):
""" generate missing reports and store to db
Args:
data (dict): report-data
"""
pool = Pool()
Invoice = pool.get('account.invoice')
XReport = pool.get('account_invoice_xrechnung.export', type='report')
invoices = Invoice.search([
('id', '=', data['invoice']),
('type', '=', 'out')])
to_generate = [x.id for x in invoices if not x.invoice_report_cache]
if to_generate:
report_action = XReport.get_used_report()
# run selected report on invoices w/o stored report-data
data2 = {}
data2.update(data)
data2['action_id'] = report_action.id
data2['model'] = report_action.model
data2['id'] = to_generate[0]
data2['ids'] = to_generate
RepInvoice = pool.get(report_action.report_name, type='report')
with Transaction().set_context({'with_rec_name': False}):
RepInvoice.execute(to_generate, data2)
def do_export(self, action): def do_export(self, action):
""" run export """ run export
""" """
if self.start.state != 'posted': if self.start.state != 'posted':
raise UserError(gettext( raise UserError(gettext(
'account_invoice_xrechnung.msg_invoice_must_posted', 'account_invoice_xrechnung.msg_invoice_must_posted',
invname=self.start.invoice.rec_name, invname=self.start.invoice.rec_name))
))
return action, { data = {
'invoice': self.start.invoice.id, 'invoice': self.start.invoice.id,
'edocument': self.start.edocument, 'edocument': self.start.edocument,
'as_zip': self.start.as_zip, 'as_zip': self.start.as_zip}
}
# if zugferd - generate missing report-cache-items
if data['edocument'] == 'edocument.facturxext.invoice-ferd':
# pdf is stored to db
self.generate_invoice_reports(data)
return action, data
# end RunXRechnungReport # end RunXRechnungReport

View file

@ -4,10 +4,13 @@
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
import zipfile import zipfile
from facturx import generate_from_binary
from io import BytesIO from io import BytesIO
from slugify import slugify
from trytond.report import Report from trytond.report import Report
from trytond.pool import Pool from trytond.pool import Pool
from slugify import slugify from trytond.exceptions import UserError
from trytond.i18n import gettext
from .wizard_runreport import edoc_versions from .wizard_runreport import edoc_versions
@ -28,31 +31,112 @@ class XReport(Report):
def execute(cls, ids, data): def execute(cls, ids, data):
""" skip export-engine, run edocument-xml-convert """ skip export-engine, run edocument-xml-convert
""" """
def export_data(exp_content, fname, ext, data2):
""" get tuple to return from report.execute,
Args:
exp_content (bytes or str): result data of report
fname (str): file name
ext (str): extension
data2 (dict): data
Returns:
tuple: return value of report
"""
if data2['as_zip'] is True:
return (
'zip',
cls.compress_as_zip(
'%(fname)s.%(ext)s' % {
'fname': fname, 'ext': ext},
exp_content),
False,
file_name)
else:
return (ext, exp_content, False, fname)
pool = Pool() pool = Pool()
IrDate = pool.get('ir.date') IrDate = pool.get('ir.date')
Invoice = pool.get('account.invoice') Invoice = pool.get('account.invoice')
EDocument = pool.get(data['edocument'])
document_para = data['edocument'].split('-')
EDocument = pool.get(document_para[0])
document_var = document_para[1] if len(document_para) > 1 else None
invoice, = Invoice.browse([data['invoice']]) invoice, = Invoice.browse([data['invoice']])
template = EDocument(invoice) template = EDocument(invoice)
invoice_string = template.render(edoc_versions[data['edocument']]) invoice_xml = template.render(edoc_versions[data['edocument']])
file_name = slugify('%(date)s-%(descr)s' % { file_name = slugify('%(date)s-%(descr)s' % {
'date': IrDate.today().isoformat().replace('-', ''), 'date': IrDate.today().isoformat().replace('-', ''),
'descr': invoice.rec_name, 'descr': invoice.rec_name},
}, max_length=100, word_boundary=True, save_order=True)
max_length=100, word_boundary=True, save_order=True)
if data['as_zip'] is True: if document_var and (
return ( document_var == 'ferd') and (
'zip', EDocument.__name__ == 'edocument.facturxext.invoice'):
cls.compress_as_zip('%(fname)s.%(ext)s' % { # convert to zugferd
'fname': file_name, invoice_pdf = cls.get_zugferd_pdf(invoice, invoice_xml)
'ext': 'xml', return export_data(invoice_pdf, file_name, 'pdf', data)
}, invoice_string),
False,
file_name)
else: else:
return ('xml', invoice_string, False, file_name) return export_data(invoice_xml, file_name, 'xml', data)
@classmethod
def get_used_report(cls):
""" get report to use from config
Raises:
UserError: if not report was found
Returns:
record: ir.action.report
"""
pool = Pool()
Configuration = pool.get('account.configuration')
ActionReport = pool.get('ir.action.report')
cfg1 = Configuration.get_singleton()
act_report = None
if cfg1 and cfg1.xrechn_zugferd_report:
act_report = cfg1.xrechn_zugferd_report
else:
# no report defined, use 1st found
act_report = ActionReport.search([
('model', '=', 'account.invoice'),
('extension', '=', 'pdf')], count=1)
if act_report:
act_report = act_report[0]
if not act_report:
raise UserError(gettext(
'account_invoice_xrechnung.msg_no_report_found'))
return act_report
@classmethod
def get_zugferd_pdf(cls, invoice, invoice_xml):
""" generate ZugFeRD-PDF
Args:
invoice (record): model account.invoice
invoice_xml (str): xml-data
"""
# pdf was already stored to db
if not (invoice.invoice_report_cache and (
invoice.invoice_report_format == 'pdf')):
raise UserError(gettext(
'account_invoice_xrechnung.msg_invalid_cachecontent',
invoice_name=invoice.rec_name))
zugferd_pdf = generate_from_binary(
pdf_file=invoice.invoice_report_cache,
xml=invoice_xml,
check_xsd=True,
pdf_metadata={
'author': invoice.company.rec_name,
'keywords': 'Factur-X, Invoice, Tryton',
'title': invoice.number,
'subject': invoice.description},
lang='de-DE')
return zugferd_pdf
# end XReport # end XReport