Merge branch '17.0-mig-account_ebics' into '17.0'

[MIG] account_ebics: Migration to 17.0

See merge request Noviat/Noviat_Generic/accounting-ebics!26
This commit is contained in:
Luc De Meyer 2024-01-27 18:47:44 +00:00
commit 9df6dce669
41 changed files with 4815 additions and 0 deletions

219
account_ebics/README.rst Normal file
View File

@ -0,0 +1,219 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: https://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3
======================
EBICS banking protocol
======================
Implementation of the EBICS banking protocol.
This module facilitates the exchange of files with banks via the EBICS protocol.
|
Installation
============
The module depends upon
- https://pypi.python.org/pypi/fintech
- https://pypi.python.org/pypi/cryptography
Remark:
The EBICS 'Test Mode' for uploading orders requires fintech 4.3.4 or higher for EBICS 2.x
and fintech 7.2.7 or higher for EBICS 3.0.
SWIFT 3SKey support requires fintech 6.4 or higher.
|
We also recommend to consider the installation of the following modules:
|
- account_ebics_oe
Required if you are running Odoo Enterprise
Cf. https://github.com/Noviat/account_ebics
|
- account_ebics_batch
This module adds a cron job for the automated import of EBICS files.
Cf. https://github.com/Noviat/account_ebics
|
- account_ebics_batch_payment
Recommended if you are using the Odoo Enterprise account_batch_payment module
Cf. https://github.com/Noviat/account_ebics
|
- account_usability
Recommended if you have multiple financial journals.
This module adds a number of accounting menu entries such as bank statement list view
which allows to see all statements downloaded via the ir.cron automated EBICS download.
Cf. https://github.com/OCA/account-financial-tools
|
- account_ebics_payment_order
Required if you are using the OCA account_payment_order module.
Cf. https://github.com/OCA/bank-payment
|
- account_ebics_oca_statement_import
Required if you are using the OCA Bank Statement import modules.
https://github.com/OCA/bank-statement-import
|
- account_statement_import_fr_cfonb
Required to handle french CFONB files.
Cf. https://github.com/OCA/l10n_france
|
- account_statement_import_camt
Required to handle camt.052 and camt.054 files.
Cf. https://github.com/OCA/bank-statement-import
|
Fintech license
---------------
If you have a valid Fintech.ebics license, you should add the following
licensing parameters to the odoo server configuration file:
- fintech_register_name
The name of the licensee.
- fintech_register_keycode
The keycode of the licensed version.
|
| Example:
|
::
; fintech
fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
|
Cf. https://www.joonis.de/en/fintech/prices/
|
Configuration
=============
Go to **Settings > Users**
Add the users that are authorised to maintain the EBICS configuration to the 'EBICS Manager' Group.
|
Go to **Accounting > Configuration > Miscellaneous > EBICS > EBICS File Formats**
Check if the EBICS File formats that you want to process in Odoo are defined.
Most commonly used formats for which support is available in Odoo should be there already.
Please open an issue on https://github.com/Noviat/account_ebics to report missing EBICS File Formats.
For File Formats of type 'Downloads' you can also specify a 'Download Process Method'.
This is the method that will be executed when hitting the 'Process' button on the downloaded file.
The following methods are currently available:
- cfonb120
- camt.053
- camt.052
- camt.054
All these methods require complimentary modules to be installed (cf. Installation section supra).
You'll get an error message when the required module is not installed on your Odoo instance.
|
Go to **Accounting > Configuration > Miscellaneous > EBICS > EBICS Configuration**
Configure your EBICS configuration according to the contract with your bank.
|
Usage
=====
Go to **Accounting > Bank and Cash > EBICS Processing**
|
Diagnostics
===========
Add the following to your Odoo config file in order to diagnose
issues with the EBICS connection with your bank:
log_handler = fintech.ebics:DEBUG
|
EBICS Return Codes
------------------
During the processing of your EBICS upload/download, your bank may return an Error Code, e.g.
EBICS Functional Error:
EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 90005)
A detailed explanation of the codes can be found on http://www.ebics.org.
You can also find this information in the doc folder of this module (file EBICS_Annex1_ReturnCodes).
|
Electronic Distributed Signature (EDS)
--------------------------------------
This is supported via external signing apps, e.g. BankingVEU:
- https://play.google.com/store/apps/details?id=subsembly.bankingveu
- https://apps.apple.com/de/app/bankingveu/id1578694190
Known Issues / Roadmap
======================
- The end user is currently not able to change his passphrases (only the users with 'EBICS Manager' rights can do so).
- Add support to import externally generated keys & certificates (currently only 3SKey signature certificate).
- Add support for SWIFT 3SKey signing javascript lib (SConnect, cf https://www2.swift.com/3skey/help/sconnect.html).

View File

@ -0,0 +1,9 @@
import logging
_logger = logging.getLogger(__name__)
try:
from . import models
from . import wizards
except Exception:
_logger.warning("Import Error, check if fintech lib has been installed")

View File

@ -0,0 +1,34 @@
# Copyright 2009-2024 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "EBICS banking protocol",
"version": "17.0.1.0.0",
"license": "LGPL-3",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Accounting & Finance",
"depends": ["account"],
"data": [
"security/ebics_security.xml",
"security/ir.model.access.csv",
"data/ebics_file_format.xml",
"views/ebics_config_views.xml",
"views/ebics_file_views.xml",
"views/ebics_userid_views.xml",
"views/ebics_file_format_views.xml",
"wizards/ebics_change_passphrase.xml",
"wizards/ebics_xfer.xml",
"wizards/ebics_admin_order.xml",
"views/menu.xml",
],
"installable": True,
"application": True,
"external_dependencies": {
"python": [
"fintech",
"cryptography",
]
},
"images": ["static/description/cover.png"],
}

View File

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- Download formats -->
<record id="ebics_ff_C52" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.052</field>
<field name="type">down</field>
<field name="order_type">C52</field>
<field name="download_process_method">camt.052</field>
<field name="description">bank to customer account report in format camt.052</field>
<field name="suffix">c52.xml</field>
</record>
<record id="ebics_ff_Z52" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.052</field>
<field name="type">down</field>
<field name="order_type">Z52</field>
<field name="download_process_method">camt.052</field>
<field name="description">bank to customer account report in format camt.052</field>
<field name="suffix">c52.xml</field>
</record>
<record id="ebics_ff_C53" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.053</field>
<field name="type">down</field>
<field name="order_type">C53</field>
<field name="download_process_method">camt.053</field>
<field
name="description"
>Bank to customer statement report in format camt.053</field>
<field name="suffix">c53.xml</field>
</record>
<record id="ebics_ff_Z53" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.053</field>
<field name="type">down</field>
<field name="order_type">Z53</field>
<field name="download_process_method">camt.053</field>
<field
name="description"
>Bank to customer statement report in format camt.053</field>
<field name="suffix">c53.xml</field>
</record>
<record id="ebics_ff_C54" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.054</field>
<field name="type">down</field>
<field name="order_type">C54</field>
<field name="download_process_method">camt.054</field>
<field
name="description"
>Bank to customer debit credit notification in format camt.054</field>
<field name="suffix">c52.xml</field>
</record>
<record id="ebics_ff_Z54" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.054</field>
<field name="type">down</field>
<field name="order_type">Z54</field>
<field name="download_process_method">camt.054</field>
<field
name="description"
>Bank to customer debit credit notification in format camt.054</field>
<field name="suffix">c52.xml</field>
</record>
<record id="ebics_ff_FDL_camt_xxx_cfonb120_stm" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">camt.xxx.cfonb120.stm</field>
<field name="type">down</field>
<field name="order_type">FDL</field>
<field name="download_process_method">cfonb120</field>
<field
name="description"
>Bank to customer statement report in format cfonb120</field>
<field name="suffix">cfonb120.dat</field>
</record>
<record id="ebics_ff_CDZ" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.002</field>
<field name="type">down</field>
<field name="order_type">CDZ</field>
<field
name="description"
>Payment status report for direct debit in format pain.002</field>
<field name="suffix">psr.xml</field>
</record>
<record id="ebics_ff_Z01" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.002</field>
<field name="type">down</field>
<field name="order_type">Z01</field>
<field name="download_process_method">pain.002</field>
<field
name="description"
>Payment status report for direct debit in format pain.002</field>
<field name="suffix">psr.xml</field>
</record>
<record id="ebics_ff_btf_cfonb120" model="ebics.file.format">
<field name="ebics_version">3</field>
<field name="type">down</field>
<field name="order_type">BTD</field>
<field name="download_process_method">cfonb120</field>
<field
name="description"
>Bank to customer statement report in format cfonb120</field>
<field name="suffix">cfonb120.dat</field>
<field name="btf_service">EOP</field>
<field name="btf_message">cfonb120</field>
</record>
<!-- Upload formats -->
<record id="ebics_ff_LCR" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.xxx.cfonb160.dco</field>
<field name="type">up</field>
<field name="order_type">FUL</field>
<field name="description">Remises de LCR</field>
<field name="suffix">txt</field>
</record>
<record id="ebics_ff_CCT" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.001.001.03</field>
<field name="type">up</field>
<field name="order_type">CCT</field>
<field name="description">Payment Order in format pain.001.001.03</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_XE2" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.001.001.03</field>
<field name="type">up</field>
<field name="order_type">XE2</field>
<field name="description">Payment Order in format pain.001.001.03</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_CDD" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.008.001.02.sdd</field>
<field name="type">up</field>
<field name="order_type">CDD</field>
<field
name="description"
>Sepa Core Direct Debit Order in format pain.008.001.02</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_XE3" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.008.001.02.sdd</field>
<field name="type">up</field>
<field name="order_type">XE3</field>
<field
name="description"
>Sepa Core Direct Debit Order in format pain.008.001.02</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_CDB" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.008.001.02.sbb</field>
<field name="type">up</field>
<field name="order_type">CDB</field>
<field
name="description"
>Sepa Direct Debit (B2B) Order in format pain.008.001.02</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_XE4" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.008.001.02.sbb</field>
<field name="type">up</field>
<field name="order_type">XE4</field>
<field
name="description"
>Sepa Direct Debit (B2B) Order in format pain.008.001.02</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_FUL_pain_001_001_02_sct" model="ebics.file.format">
<field name="ebics_version">2</field>
<field name="name">pain.001.001.02.sct</field>
<field name="type">up</field>
<field name="order_type">FUL</field>
<field name="description">Payment Order in format pain.001.001.02</field>
<field name="suffix">xml</field>
</record>
<record id="ebics_ff_btf_SCT" model="ebics.file.format">
<field name="ebics_version">3</field>
<field name="type">up</field>
<field name="order_type">BTU</field>
<field name="description">SEPA credit transfer</field>
<field name="suffix">txt</field>
<field name="btf_service">SCT</field>
<field name="btf_message">pain.001</field>
<field name="btf_scope">GLB</field>
</record>
</odoo>

Binary file not shown.

View File

@ -0,0 +1,22 @@
<?xml version='1.0' encoding='utf-8' ?>
<odoo>
<record id="ebics_config_comp_rule" model="ir.rule">
<field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
</odoo>

View File

@ -0,0 +1,82 @@
# Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import os
from openupgradelib import openupgrade # pylint: disable=W7936
@openupgrade.migrate()
def migrate(env, version):
_ebics_config_upgrade(env, version)
_noupdate_changes(env, version)
def _ebics_config_upgrade(env, version):
env.cr.execute("SELECT * FROM ebics_config")
cfg_datas = env.cr.dictfetchall()
for cfg_data in cfg_datas:
cfg = env["ebics.config"].browse(cfg_data["id"])
journal = env["account.journal"].search(
[("bank_account_id", "=", cfg_data["bank_id"])]
)
keys_fn_old = cfg_data["ebics_keys"]
ebics_keys_root = os.path.dirname(keys_fn_old)
if os.path.isfile(keys_fn_old):
keys_fn = ebics_keys_root + "/" + cfg_data["ebics_user"] + "_keys"
os.rename(keys_fn_old, keys_fn)
state = cfg_data["state"] == "active" and "confirm" or "draft"
cfg.write(
{
"company_ids": [(6, 0, [cfg_data["company_id"]])],
"journal_ids": [(6, 0, journal.ids)],
"ebics_keys": ebics_keys_root,
"state": state,
}
)
user_vals = {
"ebics_config_id": cfg_data["id"],
"name": cfg_data["ebics_user"],
}
for fld in [
"signature_class",
"ebics_passphrase",
"ebics_ini_letter_fn",
"ebics_public_bank_keys_fn",
"ebics_key_x509",
"ebics_key_x509_dn_cn",
"ebics_key_x509_dn_o",
"ebics_key_x509_dn_ou",
"ebics_key_x509_dn_c",
"ebics_key_x509_dn_st",
"ebics_key_x509_dn_l",
"ebics_key_x509_dn_e",
"ebics_file_format_ids",
"state",
]:
if cfg_data.get(fld):
if fld == "ebics_file_format_ids":
user_vals[fld] = [(6, 0, cfg_data[fld])]
elif fld == "state" and cfg_data["state"] == "active":
user_vals["state"] = "active_keys"
else:
user_vals[fld] = cfg_data[fld]
ebics_userid = env["ebics.userid"].create(user_vals)
env.cr.execute(
"""
UPDATE ir_attachment
SET res_model = 'ebics.userid', res_id = %s
WHERE name in ('ebics_ini_letter', 'ebics_public_bank_keys');
"""
% ebics_userid.id
)
if len(cfg_datas) == 1:
env.cr.execute("UPDATE ebics_file SET ebics_userid_id = %s" % ebics_userid.id)
def _noupdate_changes(env, version):
openupgrade.load_data(
env.cr, "account_ebics", "migrations/13.0.1.1/noupdate_changes.xml"
)

View File

@ -0,0 +1,9 @@
# Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def migrate(cr, version):
if not version:
return
cr.execute("DELETE FROM ebics_xfer;")

View File

@ -0,0 +1,42 @@
# Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
_FILE_FORMATS = [
{
"xml_id_name": "ebics_ff_C52",
"download_process_method": "camt.052",
},
{
"xml_id_name": "ebics_ff_C53",
"download_process_method": "camt.053",
},
{
"xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
"download_process_method": "cfonb120",
},
]
def migrate(cr, version):
for ff in _FILE_FORMATS:
_update_file_format(cr, ff)
def _update_file_format(cr, ff):
cr.execute( # pylint: disable=E8103
"""
SELECT res_id FROM ir_model_data
WHERE module='account_ebics' AND name='{}'
""".format(ff["xml_id_name"])
)
res = cr.fetchone()
if res:
cr.execute( # pylint: disable=E8103
"""
UPDATE ebics_file_format
SET download_process_method='{download_process_method}'
WHERE id={ff_id};
""".format(
download_process_method=ff["download_process_method"], ff_id=res[0]
)
)

View File

@ -0,0 +1,75 @@
# Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
_FILE_FORMATS = [
{
"old_xml_id_name": "ebics_ff_camt_052_001_02_stm",
"new_xml_id_name": "ebics_ff_C52",
"new_name": "camt.052",
},
{
"old_xml_id_name": "ebics_ff_camt_053_001_02_stm",
"new_xml_id_name": "ebics_ff_C53",
"new_name": "camt.053",
},
{
"old_xml_id_name": "ebics_ff_camt_xxx_cfonb120_stm",
"new_xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
},
{
"old_xml_id_name": "ebics_ff_pain_001_001_03_sct",
"new_xml_id_name": "ebics_ff_CCT",
},
{
"old_xml_id_name": "ebics_ff_pain_001",
"new_xml_id_name": "ebics_ff_XE2",
"new_name": "pain.001.001.03",
},
{
"old_xml_id_name": "ebics_ff_pain_008_001_02_sdd",
"new_xml_id_name": "ebics_ff_CDD",
},
{
"old_xml_id_name": "ebics_ff_pain_008",
"new_xml_id_name": "ebics_ff_XE3",
},
{
"old_xml_id_name": "ebics_ff_pain_008_001_02_sbb",
"new_xml_id_name": "ebics_ff_CDB",
},
{
"old_xml_id_name": "ebics_ff_pain_001_001_02_sct",
"new_xml_id_name": "ebics_ff_FUL_pain_001_001_02_sct",
},
]
def migrate(cr, version):
if not version:
return
for ff in _FILE_FORMATS:
_update_file_format(cr, ff)
def _update_file_format(cr, ff):
cr.execute( # pylint: disable=E8103
"""
SELECT id, res_id FROM ir_model_data
WHERE module='account_ebics' AND name='{}'
""".format(ff["old_xml_id_name"])
)
res = cr.fetchone()
if res:
query = """
UPDATE ir_model_data
SET name='{new_xml_id_name}'
WHERE id={xml_id};
""".format(new_xml_id_name=ff["new_xml_id_name"], xml_id=res[0])
if ff.get("new_name"):
query += """
UPDATE ebics_file_format
SET name='{new_name}'
WHERE id={ff_id};
""".format(new_name=ff["new_name"], ff_id=res[1])
cr.execute(query) # pylint: disable=E8103

View File

@ -0,0 +1,54 @@
# Copyright 2009-2022 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def migrate(cr, version):
if not version:
return
cr.execute("select id from ebics_config")
cfg_ids = [x[0] for x in cr.fetchall()]
for cfg_id in cfg_ids:
cr.execute(
"""
SELECT DISTINCT aj.company_id
FROM account_journal_ebics_config_rel rel
JOIN account_journal aj ON rel.account_journal_id = aj.id
WHERE ebics_config_id = %s
""",
(cfg_id,),
)
new_cpy_ids = [x[0] for x in cr.fetchall()]
cr.execute(
"""
SELECT DISTINCT res_company_id
FROM ebics_config_res_company_rel
WHERE ebics_config_id = %s
""",
(cfg_id,),
)
old_cpy_ids = [x[0] for x in cr.fetchall()]
to_add = []
for cid in new_cpy_ids:
if cid in old_cpy_ids:
old_cpy_ids.remove(cid)
else:
to_add.append(cid)
if old_cpy_ids:
cr.execute(
"""
DELETE FROM ebics_config_res_company_rel
WHERE res_company_id IN %s
""",
(tuple(old_cpy_ids),),
)
if to_add:
for cid in to_add:
cr.execute(
"""
INSERT INTO ebics_config_res_company_rel(ebics_config_id, res_company_id)
VALUES (%s, %s);
""",
(cfg_id, cid),
)

View File

@ -0,0 +1,6 @@
from . import fintech_ebics_register
from . import account_bank_statement
from . import ebics_config
from . import ebics_file
from . import ebics_file_format
from . import ebics_userid

View File

@ -0,0 +1,11 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models
class AccountBankStatement(models.Model):
_inherit = "account.bank.statement"
ebics_file_id = fields.Many2one(comodel_name="ebics.file", string="EBICS Data File")
import_format = fields.Char(readonly=True)

View File

@ -0,0 +1,219 @@
# Copyright 2009-2024 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import logging
import os
import re
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class EbicsConfig(models.Model):
"""
EBICS configuration is stored in a separate object in order to
allow extra security policies on this object.
"""
_name = "ebics.config"
_description = "EBICS Configuration"
_order = "name"
name = fields.Char(
required=True,
)
journal_ids = fields.Many2many(
comodel_name="account.journal",
relation="account_journal_ebics_config_rel",
string="Bank Accounts",
domain="[('type', '=', 'bank')]",
)
ebics_host = fields.Char(
string="EBICS HostID",
required=True,
help="Contact your bank to get the EBICS HostID."
"\nIn France the BIC is usually allocated to the HostID "
"whereas in Germany it tends to be an institute specific string "
"of 8 characters.",
)
ebics_url = fields.Char(
string="EBICS URL",
required=True,
help="Contact your bank to get the EBICS URL.",
)
ebics_version = fields.Selection(
selection=[
("H003", "H003 (2.4)"),
("H004", "H004 (2.5)"),
("H005", "H005 (3.0)"),
],
string="EBICS protocol version",
required=True,
default="H004",
)
ebics_partner = fields.Char(
string="EBICS PartnerID",
required=True,
help="Organizational unit (company or individual) "
"that concludes a contract with the bank. "
"\nIn this contract it will be agreed which order types "
"(file formats) are used, which accounts are concerned, "
"which of the customer's users (subscribers) "
"communicate with the EBICS bank server and the authorisations "
"that these users will possess. "
"\nIt is identified by the PartnerID.",
)
ebics_userid_ids = fields.One2many(
comodel_name="ebics.userid",
inverse_name="ebics_config_id",
string="EBICS UserID",
help="Human users or a technical system that is/are "
"assigned to a customer. "
"\nOn the EBICS bank server it is identified "
"by the combination of UserID and PartnerID. "
"The technical subscriber serves only for the data exchange "
"between customer and financial institution. "
"The human user also can authorise orders.",
)
# We store the EBICS keys in a separate directory in the file system.
# This directory requires special protection to reduce fraude.
ebics_keys = fields.Char(
string="EBICS Keys Root",
required=True,
default=lambda self: self._default_ebics_keys(),
help="Root Directory for storing the EBICS Keys.",
)
ebics_key_version = fields.Selection(
selection=[("A005", "A005 (RSASSA-PKCS1-v1_5)"), ("A006", "A006 (RSASSA-PSS)")],
string="EBICS key version",
default="A006",
help="The key version of the electronic signature.",
)
ebics_key_bitlength = fields.Integer(
string="EBICS key bitlength",
default=2048,
help="The bit length of the generated keys. "
"\nThe value must be between 1536 and 4096.",
)
ebics_file_format_ids = fields.Many2many(
comodel_name="ebics.file.format",
column1="config_id",
column2="format_id",
string="EBICS File Format",
)
state = fields.Selection(
selection=[("draft", "Draft"), ("confirm", "Confirmed")],
default="draft",
required=True,
readonly=True,
)
order_number = fields.Char(
size=4,
help="Specify the number for the next order."
"\nThis number should match the following pattern : "
"[A-Z]{1}[A-Z0-9]{3}",
)
active = fields.Boolean(default=True)
company_ids = fields.Many2many(
comodel_name="res.company",
relation="ebics_config_res_company_rel",
string="Companies",
readonly=True,
help="Companies sharing this EBICS contract.",
)
@api.model
def _default_ebics_keys(self):
return "/".join(["/etc/odoo/ebics_keys", self._cr.dbname])
@api.constrains("ebics_key_bitlength")
def _check_ebics_key_bitlength(self):
for cfg in self:
if cfg.ebics_version == "H005" and cfg.ebics_key_bitlength < 2048:
raise UserError(_("EBICS key bitlength must be >= 2048."))
@api.constrains("order_number")
def _check_order_number(self):
for cfg in self:
nbr = cfg.order_number
ok = True
if nbr:
if len(nbr) != 4:
ok = False
else:
pattern = re.compile("[A-Z]{1}[A-Z0-9]{3}")
if not pattern.match(nbr):
ok = False
if not ok:
raise UserError(
_(
"Order Number should comply with the following pattern:"
"\n[A-Z]{1}[A-Z0-9]{3}"
)
)
def write(self, vals):
"""
Due to the multi-company nature of the EBICS config we
need to adapt the company_ids in the write method.
"""
if "journal_ids" not in vals:
return super().write(vals)
for rec in self:
old_company_ids = rec.journal_ids.mapped("company_id").ids
super(EbicsConfig, rec).write(vals)
new_company_ids = rec.journal_ids.mapped("company_id").ids
updates = []
for cid in new_company_ids:
if cid in old_company_ids:
old_company_ids.remove(cid)
else:
updates += [(4, cid)]
updates += [(3, x) for x in old_company_ids]
super(EbicsConfig, rec).write({"company_ids": updates})
return True
def unlink(self):
for ebics_config in self:
if ebics_config.state == "active":
raise UserError(_("You cannot remove active EBICS configurations."))
return super().unlink()
def set_to_draft(self):
return self.write({"state": "draft"})
def set_to_confirm(self):
return self.write({"state": "confirm"})
def _get_order_number(self):
return self.order_number
def _update_order_number(self, OrderID):
o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1):
if c == "9":
o_list[-i] = "A"
break
if c == "Z":
o_list[-i] = "0"
continue
else:
o_list[-i] = chr(ord(c) + 1)
break
next_order_number = "".join(o_list)
if next_order_number == "ZZZZ":
next_order_number = "A000"
self.order_number = next_order_number
def _check_ebics_keys(self):
dirname = self.ebics_keys or ""
if not os.path.exists(dirname):
raise UserError(
_(
"EBICS Keys Root Directory %s is not available."
"\nPlease contact your system administrator."
)
% dirname
)

View File

@ -0,0 +1,619 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import base64
import logging
from copy import deepcopy
from sys import exc_info
from traceback import format_exception
from lxml import etree
from odoo import _, fields, models
from odoo.exceptions import UserError
from odoo.addons.base.models.res_bank import sanitize_account_number
_logger = logging.getLogger(__name__)
DUP_CHECK_FORMATS = ["cfonb120", "camt053"]
class EbicsFile(models.Model):
_name = "ebics.file"
_description = "Object to store EBICS Data Files"
_order = "date desc"
_sql_constraints = [
(
"name_uniq",
"unique (name, format_id)",
"This File has already been down- or uploaded !",
)
]
name = fields.Char(string="Filename")
data = fields.Binary(string="File", readonly=True)
format_id = fields.Many2one(
comodel_name="ebics.file.format", string="EBICS File Formats", readonly=True
)
type = fields.Selection(related="format_id.type", readonly=True)
date_from = fields.Date(
readonly=True, help="'Date From' as entered in the download wizard."
)
date_to = fields.Date(
readonly=True, help="'Date To' as entered in the download wizard."
)
date = fields.Datetime(
required=True, readonly=True, help="File Upload/Download date"
)
bank_statement_ids = fields.One2many(
comodel_name="account.bank.statement",
inverse_name="ebics_file_id",
string="Generated Bank Statements",
readonly=True,
)
state = fields.Selection(
[("draft", "Draft"), ("done", "Done")],
default="draft",
required=True,
readonly=True,
)
user_id = fields.Many2one(
comodel_name="res.users",
string="User",
default=lambda self: self.env.user,
readonly=True,
)
ebics_userid_id = fields.Many2one(
comodel_name="ebics.userid",
string="EBICS UserID",
ondelete="restrict",
readonly=True,
)
note = fields.Text(string="Notes")
note_process = fields.Text(
string="Process Notes",
readonly=True,
)
company_ids = fields.Many2many(
comodel_name="res.company",
string="Companies",
readonly=True,
help="Companies sharing this EBICS file.",
)
def unlink(self):
ff_methods = self._file_format_methods()
for ebics_file in self:
if ebics_file.state == "done":
raise UserError(_("You can only remove EBICS files in state 'Draft'."))
# execute format specific actions
ff = ebics_file.format_id.download_process_method
if ff in ff_methods:
if ff_methods[ff].get("unlink"):
ff_methods[ff]["unlink"]()
# remove bank statements
ebics_file.bank_statement_ids.unlink()
return super().unlink()
def set_to_draft(self):
return self.write({"state": "draft"})
def set_to_done(self):
return self.write({"state": "done"})
def process(self):
self.ensure_one()
self = self.with_context(allowed_company_ids=self.env.user.company_ids.ids)
self.note_process = ""
ff_methods = self._file_format_methods()
ff = self.format_id.download_process_method
if ff in ff_methods:
if ff_methods[ff].get("process"):
res = ff_methods[ff]["process"]()
self.state = "done"
return res
else:
return self._process_undefined_format()
def action_open_bank_statements(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
domain = [("id", "in", self.env.context.get("statement_ids"))]
action["domain"] = domain
return action
def button_close(self):
self.ensure_one()
return {"type": "ir.actions.act_window_close"}
def _file_format_methods(self):
"""
Extend this dictionary in order to add support
for extra file formats.
"""
res = {
"cfonb120": {
"process": self._process_cfonb120,
"unlink": self._unlink_cfonb120,
},
"camt.052": {
"process": self._process_camt052,
"unlink": self._unlink_camt052,
},
"camt.053": {
"process": self._process_camt053,
"unlink": self._unlink_camt053,
},
"camt.054": {
"process": self._process_camt054,
"unlink": self._unlink_camt054,
},
"pain.002": {
"process": self._process_pain002,
"unlink": self._unlink_pain002,
},
}
return res
def _check_import_module(self, module, raise_if_not_found=True):
mod = (
self.env["ir.module.module"]
.sudo()
.search([("name", "=like", module), ("state", "=", "installed")])
)
if not mod:
if raise_if_not_found:
raise UserError(
_(
"The module to process the '%(ebics_format)s' format is not installed "
"on your system. "
"\nPlease install module '%(module)s'",
ebics_format=self.format_id.name,
module=module,
)
)
return False
return True
def _lookup_journal(self, res, acc_number, currency_code):
currency = self.env["res.currency"].search(
[("name", "=ilike", currency_code)], limit=1
)
journal = self.env["account.journal"]
if not currency:
message = _("Currency %(cc)s not found.", cc=currency_code)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
journals = self.env["account.journal"].search(
[
("type", "=", "bank"),
(
"bank_account_id.sanitized_acc_number",
"ilike",
acc_number,
),
]
)
if not journals:
message = _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
for jrnl in journals:
journal_currency = jrnl.currency_id or jrnl.company_id.currency_id
if journal_currency != currency:
continue
else:
journal = jrnl
break
if not journal:
message = _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
def _process_download_result(self, res, file_format=None):
"""
We perform a duplicate statement check after the creation of the bank
statements since we rely on Odoo Enterprise or OCA modules for the
bank statement creation.
From a development standpoint (code creation/maintenance) a check after
creation is the easiest way.
"""
statement_ids = res["statement_ids"]
notifications = res["notifications"]
statements = self.env["account.bank.statement"].sudo().browse(statement_ids)
if statements:
statements.write({"import_format": file_format})
statements = self._statement_duplicate_check(res, statements)
else:
notifications.append(
{
"type": "warning",
"message": _("This file doesn't contain any transaction."),
}
)
st_cnt = len(statements)
warning_cnt = error_cnt = 0
if notifications:
errors = []
warnings = []
for notif in notifications:
if notif["type"] == "error":
error_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")]
errors.append("\n".join(parts))
elif notif["type"] == "warning":
warning_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")]
warnings.append("\n".join(parts))
self.note_process += _("Process file %(fn)s results:", fn=self.name)
if error_cnt:
self.note_process += "\n\n" + _("Errors") + ":\n"
self.note_process += "\n".join(errors)
self.note_process += "\n\n"
self.note_process += _("Number of errors: %(nr)s", nr=error_cnt)
if warning_cnt:
self.note_process += "\n\n" + _("Warnings") + ":\n"
self.note_process += "\n".join(warnings)
self.note_process += "\n\n"
self.note_process += _("Number of warnings: %(nr)s", nr=warning_cnt)
self.note_process += "\n"
if st_cnt:
self.note_process += "\n\n"
self.note_process += _(
"%(st_cnt)s bank statement%(sp)s been imported: ",
st_cnt=st_cnt,
sp=st_cnt == 1 and _(" has") or _("s have"),
)
self.note_process += "\n"
for statement in statements:
self.note_process += "\n" + _(
"Statement %(st)s dated %(date)s (Company: %(cpy)s)",
st=statement.name,
date=statement.date,
cpy=statement.company_id.name,
)
if statements:
self.sudo().bank_statement_ids = [(4, x) for x in statements.ids]
company_ids = self.sudo().bank_statement_ids.mapped("company_id").ids
self.company_ids = [(6, 0, company_ids)]
ctx = dict(self.env.context, statement_ids=statements.ids)
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
return {
"name": _("Import EBICS File"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": self._name,
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def _statement_duplicate_check(self, res, statements):
"""
This check is required for import modules that do not
set the 'unique_import_id' on the statement lines.
E.g. OCA camt import
"""
to_unlink = self.env["account.bank.statement"]
for statement in statements.filtered(
lambda r: r.import_format in DUP_CHECK_FORMATS
):
dup = self.env["account.bank.statement"].search_count(
[
("id", "!=", statement.id),
("name", "=", statement.name),
("company_id", "=", statement.company_id.id),
("date", "=", statement.date),
("import_format", "=", statement.import_format),
]
)
if dup:
message = _(
"Statement %(st_name)s dated %(date)s has already been imported.",
st_name=statement.name,
date=statement.date,
)
res["notifications"].append({"type": "warning", "message": message})
to_unlink += statement
res["statement_ids"] = [
x for x in res["statement_ids"] if x not in to_unlink.ids
]
statements -= to_unlink
to_unlink.unlink()
return statements
def _process_cfonb120(self):
import_module = "account_statement_import_fr_cfonb"
self._check_import_module(import_module)
res = {"statement_ids": [], "notifications": []}
st_datas = self._split_cfonb(res)
self._process_bank_statement_oca(res, st_datas)
return self._process_download_result(res, file_format="cfonb120")
def _unlink_cfonb120(self):
"""
Placeholder for cfonb120 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _split_cfonb(self, res):
"""
Split CFONB file received via EBICS per statement.
Statements without transactions are removed.
"""
datas = []
file_data = base64.b64decode(self.data)
lines = file_data.split(b"\n")
st_lines = b""
transactions = False
for line in lines:
rec_type = line[0:2]
currency_code = line[16:19].decode()
acc_number = line[21:32].decode()
st_lines += line + b"\n"
if rec_type == b"04":
transactions = True
if rec_type == b"07":
if transactions:
currency, journal = self._lookup_journal(
res, acc_number, currency_code
)
if currency and journal:
datas.append(
{
"acc_number": acc_number,
"journal_id": journal.id,
"company_id": journal.company_id.id,
"data": base64.b64encode(st_lines),
}
)
st_lines = b""
transactions = False
return datas
def _process_camt052(self):
import_module = "account_statement_import_camt"
self._check_import_module(import_module)
return self._process_camt053(file_format="camt052")
def _unlink_camt052(self):
"""
Placeholder for camt052 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _process_camt054(self):
import_module = "account_statement_import_camt"
self._check_import_module(import_module)
return self._process_camt053(file_format="camt054")
def _unlink_camt054(self):
"""
Placeholder for camt054 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _process_camt053(self, file_format=None):
"""
The Odoo standard statement import is based on manual selection
of a financial journal before importing the electronic statement file.
An EBICS download may return a single file containing a large number of
statements from different companies/journals.
Hence we need to split the CAMT file into
single statement CAMT files before we can call the logic
implemented by the Odoo OE or Community CAMT parsers.
"""
modules = [
("oca", "account_statement_import_camt"),
("oe", "account_bank_statement_import_camt"),
]
author = False
for entry in modules:
if self._check_import_module(entry[1], raise_if_not_found=False):
author = entry[0]
break
if not author:
raise UserError(
_(
"The module to process the '%(ebics_format)s' format is "
"not installed on your system. "
"\nPlease install one of the following modules: \n%(modules)s.",
ebics_format=self.format_id.name,
modules=", ".join([x[1] for x in modules]),
)
)
res = {"statement_ids": [], "notifications": []}
st_datas = self._split_camt(res)
if author == "oca":
self._process_bank_statement_oca(res, st_datas)
else:
self._process_bank_statement_oe(res, st_datas)
file_format = file_format or "camt053"
return self._process_download_result(res, file_format=file_format)
def _process_bank_statement_oca(self, res, st_datas):
for st_data in st_datas:
try:
with self.env.cr.savepoint():
self._create_bank_statement_oca(res, st_data)
except UserError as e:
res["notifications"].append(
{"type": "error", "message": "".join(e.args)}
)
except Exception:
tb = "".join(format_exception(*exc_info()))
res["notifications"].append({"type": "error", "message": tb})
def _create_bank_statement_oca(self, res, st_data):
wiz = (
self.env["account.statement.import"]
.with_company(st_data["company_id"])
.with_context(active_model="ebics.file")
.create({"statement_filename": self.name})
)
wiz.import_single_file(base64.b64decode(st_data["data"]), res)
def _process_bank_statement_oe(self, res, st_datas):
"""
We execute a cr.commit() after every statement import since we get a
'savepoint does not exist' error when using 'with self.env.cr.savepoint()'.
"""
for st_data in st_datas:
try:
self._create_bank_statement_oe(res, st_data)
self.env.cr.commit() # pylint: disable=E8102
except UserError as e:
msg = "".join(e.args)
msg += "\n"
msg += _(
"Statement for Account Number %(nr)s has not been processed.",
nr=st_data["acc_number"],
)
res["notifications"].append({"type": "error", "message": msg})
except Exception:
tb = "".join(format_exception(*exc_info()))
res["notifications"].append({"type": "error", "message": tb})
def _create_bank_statement_oe(self, res, st_data):
attachment = (
self.env["ir.attachment"]
.with_company(st_data["company_id"])
.create(
{
"name": self.name,
"datas": st_data["data"],
"store_fname": self.name,
}
)
)
journal = (
self.env["account.journal"]
.with_company(st_data["company_id"])
.browse(st_data["journal_id"])
)
act = journal._import_bank_statement(attachment)
for entry in act["domain"]:
if (
isinstance(entry, tuple)
and entry[0] == "statement_id"
and entry[1] == "in"
):
res["statement_ids"].extend(entry[2])
break
notifications = act["context"]["notifications"]
if notifications:
res["notifications"].append(act["context"]["notifications"])
def _unlink_camt053(self):
"""
Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _split_camt(self, res):
"""
Split CAMT file received via EBICS per statement.
Statements without transactions are removed.
"""
datas = []
file_data = base64.b64decode(self.data)
root = etree.fromstring(file_data, parser=etree.XMLParser(recover=True))
if root is None:
message = _("Invalid XML file.")
res["notifications"].append({"type": "error", "message": message})
ns = {k or "ns": v for k, v in root.nsmap.items()}
camt_variant = ns["ns"].split("camt.")[1][:3]
variant_tags = {
"052": "Rpt",
"053": "Stmt",
"054": "Ntfctn",
}
camt_tag = variant_tags[camt_variant]
stmts = root[0].findall(f"ns:{camt_tag}", ns)
for i, stmt in enumerate(stmts):
acc_number = sanitize_account_number(
stmt.xpath(
"ns:Acct/ns:Id/ns:IBAN/text() | ns:Acct/ns:Id/ns:Othr/ns:Id/text()",
namespaces=ns,
)[0]
)
if not acc_number:
message = _("No bank account number found.")
res["notifications"].append({"type": "error", "message": message})
continue
currency_code = stmt.xpath(
"ns:Acct/ns:Ccy/text() | ns:Bal/ns:Amt/@Ccy", namespaces=ns
)[0]
# some banks (e.g. COMMERZBANK) add the currency as the last 3 digits
# of the bank account number hence we need to remove this since otherwise
# the journal matching logic fails
if acc_number[-3:] == currency_code:
acc_number = acc_number[:-3]
root_new = deepcopy(root)
entries = False
for j, el in enumerate(root_new[0].findall(f"ns:{camt_tag}", ns)):
if j != i:
el.getparent().remove(el)
else:
entries = el.findall("ns:Ntry", ns)
if not entries:
continue
else:
currency, journal = self._lookup_journal(res, acc_number, currency_code)
if not (currency and journal):
continue
datas.append(
{
"acc_number": acc_number,
"journal_id": journal.id,
"company_id": journal.company_id.id,
"data": base64.b64encode(etree.tostring(root_new)),
}
)
return datas
def _process_pain002(self):
"""
Placeholder for processing pain.002 files.
TODO:
add import logic based upon OCA 'account_payment_return_import'
"""
def _unlink_pain002(self):
"""
Placeholder for pain.002 specific actions before removing the
EBICS data file.
"""
raise NotImplementedError
def _process_undefined_format(self):
raise UserError(
_(
"The current version of the 'account_ebics' module "
"has no support to automatically process EBICS files "
"with format %s."
)
% self.format_id.name
)

View File

@ -0,0 +1,119 @@
# Copyright 2009-2024 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, fields, models
class EbicsFileFormat(models.Model):
_name = "ebics.file.format"
_description = "EBICS File Formats"
_order = "type,name,order_type"
ebics_version = fields.Selection(
selection=[
("2", "2"),
("3", "3"),
],
string="EBICS protocol version",
required=True,
default="2",
)
name = fields.Char(
string="Request Type",
help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
"Specify camt.052, camt.053, camt.054 for camt "
"Order Types such as C53, Z53, C54, Z54.\n"
"This name has to match the 'Request Type' in your "
"EBICS contract for Order Type 'FDL' or 'FUL'.\n",
)
type = fields.Selection(
selection=[("down", "Download"), ("up", "Upload")], required=True
)
order_type = fields.Char(
required=True,
help="EBICS 3.0: BTD (download) or BTU (upload).\n"
"EBICS 2.0: E.g. C53 (check your EBICS contract). "
"For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.",
)
download_process_method = fields.Selection(
selection="_selection_download_process_method",
help="Enable processing within Odoo of the downloaded file "
"via the 'Process' button."
"E.g. specify camt.053 to import a camt.053 file and create "
"a bank statement.",
)
# TODO:
# move signature_class parameter so that it can be set per EBICS config
signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")],
help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments."
"\nLeave this field empty to use the default "
"defined for your EBICS UserID.",
)
description = fields.Char()
suffix = fields.Char(
help="Specify the filename suffix for this File Format.\nE.g. c53.xml",
)
# EBICS 3.0 BTF
btf_service = fields.Char(
string="BTF Service",
help="BTF Service Name)\n"
"The service code name consisting of 3 alphanumeric characters "
"[A-Z0-9] (e.g. SCT, SDD, STM, EOP)",
)
btf_message = fields.Char(
string="BTF Message Name",
help="BTF Message Name\n"
"The message name consisting of up to 10 alphanumeric characters "
"[a-z0-9.] (eg. pain.001, pain.008, camt.053)",
)
btf_scope = fields.Char(
string="BTF Scope",
help="Scope of service.\n"
"Either an ISO-3166 ALPHA 2 country code or an issuer code "
"of 3 alphanumeric characters [A-Z0-9].",
)
btf_option = fields.Char(
string="BTF Option",
help="The service option code consisting of 3-10 alphanumeric "
"characters [A-Z0-9] (eg. COR, B2B)",
)
btf_container = fields.Char(
string="BTF Container",
help="Type of container consisting of 3 characters [A-Z] (eg. XML, ZIP).",
)
btf_version = fields.Char(
string="BTF Version",
help="Message version consisting of 2 numeric characters [0-9] (eg. 03).",
)
btf_variant = fields.Char(
string="BTF Variant",
help="Message variant consisting of 3 numeric characters [0-9] (eg. 001).",
)
btf_format = fields.Char(
string="BTF Format",
help="Message format consisting of 1-4 alphanumeric characters [A-Z0-9] "
"(eg. XML, JSON, PDF).",
)
@api.model
def _selection_download_process_method(self):
methods = self.env["ebics.file"]._file_format_methods().keys()
return [(x, x) for x in methods]
@api.onchange("type")
def _onchange_type(self):
if self.type == "up":
self.download_process_method = False
@api.depends("ebics_version", "name", "btf_message", "description")
def _compute_display_name(self):
for rec in self:
name = rec.ebics_version == "2" and rec.name or rec.btf_message
if rec.description:
name += " - " + rec.description
rec.display_name = name

View File

@ -0,0 +1,598 @@
# Copyright 2009-2024 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import base64
import logging
import os
from sys import exc_info
from traceback import format_exception
from urllib.error import URLError
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import (
EbicsBank,
EbicsClient,
EbicsFunctionalError,
EbicsKeyRing,
EbicsTechnicalError,
EbicsUser,
)
fintech.cryptolib = "cryptography"
except ImportError:
_logger.warning("Failed to import fintech")
class EbicsBank(EbicsBank):
def _next_order_id(self, partnerid):
"""
EBICS protocol version H003 requires generation of the OrderID.
The OrderID must be a string between 'A000' and 'ZZZZ' and
unique for each partner id.
"""
return hasattr(self, "_order_number") and self._order_number or "A000"
class EbicsUserID(models.Model):
_name = "ebics.userid"
_description = "EBICS UserID"
_order = "name"
name = fields.Char(
string="EBICS UserID",
required=True,
help="Human users or a technical system that is/are "
"assigned to a customer. "
"\nOn the EBICS bank server it is identified "
"by the combination of UserID and PartnerID. "
"The technical subscriber serves only for the data exchange "
"between customer and financial institution. "
"The human user also can authorise orders.",
)
ebics_config_id = fields.Many2one(
comodel_name="ebics.config",
string="EBICS Configuration",
ondelete="cascade",
required=True,
)
ebics_version = fields.Selection(related="ebics_config_id.ebics_version")
user_ids = fields.Many2many(
comodel_name="res.users",
string="Users",
required=True,
help="Users who are allowed to use this EBICS UserID for "
" bank transactions.",
)
signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")],
required=True,
default="T",
help="Default signature class."
"This default can be overriden for specific "
"EBICS transactions (cf. File Formats).",
)
transaction_rights = fields.Selection(
selection=[
("both", "Download and Upload"),
("down", "Download Only"),
("up", "Upload Only"),
],
string="Allowed Transactions",
default="both",
required=True,
help="Use this parameter to limit the transactions for this User "
"to downloads or uploads.",
)
ebics_keys_fn = fields.Char(compute="_compute_ebics_keys_fn")
ebics_keys_found = fields.Boolean(compute="_compute_ebics_keys_found")
ebics_passphrase = fields.Char(string="EBICS Passphrase")
ebics_passphrase_store = fields.Boolean(
string="Store EBICS Passphrase",
default=True,
help="When you uncheck this option the passphrase to unlock "
"your private key will not be stored in the database. "
"We recommend to use this if you want to upload signed "
"payment orders via EBICS.\nYou will be prompted to enter the "
"passphrase for every EBICS transaction, hence do not uncheck this "
"option on a userid for automated EBICS downloads.",
)
ebics_passphrase_required = fields.Boolean(
compute="_compute_ebics_passphrase_view_modifiers"
)
ebics_passphrase_invisible = fields.Boolean(
compute="_compute_ebics_passphrase_view_modifiers"
)
ebics_passphrase_store_readonly = fields.Boolean(
compute="_compute_ebics_passphrase_view_modifiers"
)
ebics_sig_passphrase = fields.Char(
string="EBICS Signature Passphrase",
help="You can set here a different passphrase for the EBICS "
"signing key. This passphrase will never be stored hence "
"you'll need to specify your passphrase for each transaction that "
"requires a digital signature.",
)
ebics_sig_passphrase_invisible = fields.Boolean(
compute="_compute_ebics_sig_passphrase_invisible"
)
ebics_ini_letter = fields.Binary(
string="EBICS INI Letter",
readonly=True,
help="INI-letter PDF document to be sent to your bank.",
)
ebics_ini_letter_fn = fields.Char(string="INI-letter Filename", readonly=True)
ebics_public_bank_keys = fields.Binary(
string="EBICS Public Bank Keys",
readonly=True,
help="EBICS Public Bank Keys to be checked for consistency.",
)
ebics_public_bank_keys_fn = fields.Char(
string="EBICS Public Bank Keys Filename", readonly=True
)
swift_3skey = fields.Boolean(
string="Enable 3SKey support",
help="Transactions for this user will be signed "
"by means of the SWIFT 3SKey token.",
)
swift_3skey_certificate = fields.Binary(string="3SKey Certficate")
swift_3skey_certificate_fn = fields.Char(string="3SKey Certificate Filename")
# X.509 Distinguished Name attributes used to
# create self-signed X.509 certificates
ebics_key_x509 = fields.Boolean(
string="X509 support",
help="Set this flag in order to work with " "self-signed X.509 certificates",
)
ebics_key_x509_dn_cn = fields.Char(
string="Common Name [CN]",
)
ebics_key_x509_dn_o = fields.Char(
string="Organization Name [O]",
)
ebics_key_x509_dn_ou = fields.Char(
string="Organizational Unit Name [OU]",
)
ebics_key_x509_dn_c = fields.Char(
string="Country Name [C]",
)
ebics_key_x509_dn_st = fields.Char(
string="State Or Province Name [ST]",
)
ebics_key_x509_dn_l = fields.Char(
string="Locality Name [L]",
)
ebics_key_x509_dn_e = fields.Char(
string="Email Address",
)
state = fields.Selection(
[
("draft", "Draft"),
("init", "Initialisation"),
("get_bank_keys", "Get Keys from Bank"),
("to_verify", "Verification"),
("active_keys", "Active Keys"),
],
default="draft",
required=True,
readonly=True,
)
active = fields.Boolean(default=True)
company_ids = fields.Many2many(
comodel_name="res.company",
string="Companies",
required=True,
help="Companies sharing this EBICS contract.",
)
@api.depends("name", "ebics_config_id.ebics_keys")
def _compute_ebics_keys_fn(self):
for rec in self:
keys_dir = rec.ebics_config_id.ebics_keys
rec.ebics_keys_fn = (
rec.name
and keys_dir
and (keys_dir + "/" + rec.name.replace(" ", "_") + "_keys")
)
@api.depends("ebics_keys_fn")
def _compute_ebics_keys_found(self):
for rec in self:
rec.ebics_keys_found = rec.ebics_keys_fn and os.path.isfile(
rec.ebics_keys_fn
)
@api.depends("state", "ebics_passphrase")
def _compute_ebics_passphrase_view_modifiers(self):
for rec in self:
rec.ebics_passphrase_required = False
rec.ebics_passphrase_invisible = True
rec.ebics_passphrase_store_readonly = True
if rec.state == "draft":
rec.ebics_passphrase_required = True
rec.ebics_passphrase_store_readonly = False
elif rec.state == "init":
rec.ebics_passphrase_required = False
rec.ebics_passphrase_invisible = True
elif rec.state in ("get_bank_keys", "to_verify"):
rec.ebics_passphrase_required = not rec.ebics_passphrase
rec.ebics_passphrase_invisible = rec.ebics_passphrase
@api.depends("state")
def _compute_ebics_sig_passphrase_invisible(self):
for rec in self:
rec.ebics_sig_passphrase_invisible = True
if fintech.__version_info__ < (7, 3, 1):
continue
if rec.transaction_rights != "down" and rec.state == "draft":
rec.ebics_sig_passphrase_invisible = False
@api.constrains("ebics_key_x509")
def _check_ebics_key_x509(self):
for cfg in self:
if cfg.ebics_version == "H005" and not cfg.ebics_key_x509:
raise UserError(_("X.509 certificates must be used with EBICS 3.0."))
@api.constrains("ebics_passphrase")
def _check_ebics_passphrase(self):
for rec in self:
if rec.ebics_passphrase and len(rec.ebics_passphrase) < 8:
raise UserError(_("The Passphrase must be at least 8 characters long"))
@api.constrains("ebics_sig_passphrase")
def _check_ebics_sig_passphrase(self):
for rec in self:
if rec.ebics_sig_passphrase and len(rec.ebics_sig_passphrase) < 8:
raise UserError(
_("The Signature Passphrase must be at least 8 characters long")
)
@api.onchange("ebics_version")
def _onchange_ebics_version(self):
if self.ebics_version == "H005":
self.ebics_key_x509 = True
@api.onchange("signature_class")
def _onchange_signature_class(self):
if self.signature_class == "T":
self.swift_3skey = False
@api.onchange("ebics_passphrase_store", "ebics_passphrase")
def _onchange_ebics_passphrase_store(self):
if self.ebics_passphrase_store:
if self.ebics_passphrase:
# check passphrase before db store
keyring_params = {
"keys": self.ebics_keys_fn,
"passphrase": self.ebics_passphrase,
}
keyring = EbicsKeyRing(**keyring_params)
try:
# fintech <= 7.4.3 does not have a call to check if a
# passphrase matches with the value stored in the keyfile.
# We get around this limitation as follows:
# Get user keys to check for valid passphrases
# It will raise a ValueError on invalid passphrases
keyring["#USER"]
except ValueError as err: # noqa: F841
raise UserError(_("Passphrase mismatch.")) # noqa: B904
else:
if self.state != "draft":
self.ebics_passphrase = False
@api.onchange("swift_3skey")
def _onchange_swift_3skey(self):
if self.swift_3skey:
self.ebics_key_x509 = True
def set_to_draft(self):
return self.write({"state": "draft"})
def set_to_active_keys(self):
vals = {"state": "active_keys"}
self._update_passphrase_vals(vals)
return self.write(vals)
def set_to_get_bank_keys(self):
self.ensure_one()
if self.ebics_config_id.state != "draft":
raise UserError(
_(
"Set the EBICS Configuation record to 'Draft' "
"before starting the Key Renewal process."
)
)
return self.write({"state": "get_bank_keys"})
def ebics_init_1(self): # noqa: C901
"""
Initialization of bank keys - Step 1:
Create new keys and certificates for this user
"""
self.ensure_one()
if self.state != "draft":
raise UserError(
_("Set state to 'draft' before Bank Key (re)initialisation.")
)
if not self.ebics_passphrase:
raise UserError(_("Set a passphrase."))
if self.swift_3skey and not self.swift_3skey_certificate:
raise UserError(_("3SKey certificate missing."))
ebics_version = self.ebics_config_id.ebics_version
try:
keyring_params = {
"keys": self.ebics_keys_fn,
"passphrase": self.ebics_passphrase,
}
if self.ebics_sig_passphrase:
keyring_params["sig_passphrase"] = self.ebics_sig_passphrase
keyring = EbicsKeyRing(**keyring_params)
bank = EbicsBank(
keyring=keyring,
hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url,
)
user = EbicsUser(
keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner,
userid=self.name,
)
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) from err
self.ebics_config_id._check_ebics_keys()
if not os.path.isfile(self.ebics_keys_fn):
try:
# TODO:
# enable import of all type of certicates: A00x, X002, E002
if self.swift_3skey:
kwargs = {
self.ebics_config_id.ebics_key_version: base64.decodebytes(
self.swift_3skey_certificate
),
}
user.import_certificates(**kwargs)
user.create_keys(
keyversion=self.ebics_config_id.ebics_key_version,
bitlength=self.ebics_config_id.ebics_key_bitlength,
)
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) from err
if self.swift_3skey and not self.ebics_key_x509:
raise UserError(
_(
"The current version of this module "
"requires to X509 support when enabling 3SKey"
)
)
if self.ebics_key_x509:
dn_attrs = {
"commonName": self.ebics_key_x509_dn_cn,
"organizationName": self.ebics_key_x509_dn_o,
"organizationalUnitName": self.ebics_key_x509_dn_ou,
"countryName": self.ebics_key_x509_dn_c,
"stateOrProvinceName": self.ebics_key_x509_dn_st,
"localityName": self.ebics_key_x509_dn_l,
"emailAddress": self.ebics_key_x509_dn_e,
}
kwargs = {k: v for k, v in dn_attrs.items() if v}
user.create_certificates(**kwargs)
try:
client = EbicsClient(bank, user, version=ebics_version)
except RuntimeError as err:
e = exc_info()
error = _("EBICS Initialization Error:")
error += "\n"
error += err.args[0]
raise UserError(error) from err
# Send the public electronic signature key to the bank.
ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
if not ebics_config_bank:
raise UserError(
_("No bank defined for the financial journal " "of the EBICS Config")
)
try:
supported_versions = client.HEV()
if supported_versions and ebics_version not in supported_versions:
err_msg = _("EBICS version mismatch.") + "\n"
err_msg += _("Versions supported by your bank:")
for k in supported_versions:
err_msg += f"\n{k}: {supported_versions[k]} "
raise UserError(err_msg)
if ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number()
OrderID = client.INI()
_logger.info("%s, EBICS INI command, OrderID=%s", self._name, OrderID)
if ebics_version == "H003":
self.ebics_config_id._update_order_number(OrderID)
except URLError as err:
exctype, value = exc_info()[:2]
tb = "".join(format_exception(*exc_info()))
_logger.error(
"EBICS INI command error\nUserID: %s\n%s",
self.name,
tb,
)
raise UserError(
_(
"urlopen error:\n url '%(url)s' - %(val)s",
url=self.ebics_config_id.ebics_url,
val=str(value),
)
) from err
except EbicsFunctionalError as err:
e = exc_info()
error = _("EBICS Functional Error:")
error += "\n"
error += f"{e[1].message} (code: {e[1].code})"
raise UserError(error) from err
except EbicsTechnicalError as err:
e = exc_info()
error = _("EBICS Technical Error:")
error += "\n"
error += f"{e[1].message} (code: {e[1].code})"
raise UserError(error) from err
# Send the public authentication and encryption keys to the bank.
if ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number()
OrderID = client.HIA()
_logger.info("%s, EBICS HIA command, OrderID=%s", self._name, OrderID)
if ebics_version == "H003":
self.ebics_config_id._update_order_number(OrderID)
# Create an INI-letter which must be printed and sent to the bank.
ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
cc = ebics_config_bank.country.code
if cc in ["FR", "DE"]:
lang = cc
else:
lang = self.env.user.lang or self.env["res.lang"].search([])[0].code
lang = lang[:2]
fn_date = fields.Date.today().isoformat()
fn = "_".join([self.ebics_config_id.ebics_host, "ini_letter", fn_date]) + ".pdf"
letter = user.create_ini_letter(bankname=ebics_config_bank.name, lang=lang)
vals = {
"ebics_ini_letter": base64.encodebytes(letter),
"ebics_ini_letter_fn": fn,
"state": "init",
}
self._update_passphrase_vals(vals)
return self.write(vals)
def ebics_init_2(self):
"""
Initialization of bank keys - Step 2:
Activation of the account by the bank.
"""
self.ensure_one()
if self.state != "init":
raise UserError(_("Set state to 'Initialisation'."))
vals = {"state": "get_bank_keys"}
self._update_passphrase_vals(vals)
return self.write(vals)
def ebics_init_3(self):
"""
Initialization of bank keys - Step 3:
After the account has been activated the public bank keys
must be downloaded and checked for consistency.
"""
self.ensure_one()
if self.state != "get_bank_keys":
raise UserError(_("Set state to 'Get Keys from Bank'."))
try:
keyring = EbicsKeyRing(
keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
)
bank = EbicsBank(
keyring=keyring,
hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url,
)
user = EbicsUser(
keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner,
userid=self.name,
)
client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) from err
try:
public_bank_keys = client.HPB()
except EbicsFunctionalError as err:
e = exc_info()
error = _("EBICS Functional Error:")
error += "\n"
error += f"{e[1].message} (code: {e[1].code})"
raise UserError(error) from err
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) from err
public_bank_keys = public_bank_keys.encode()
fn_date = fields.Date.today().isoformat()
fn = (
"_".join([self.ebics_config_id.ebics_host, "public_bank_keys", fn_date])
+ ".txt"
)
vals = {
"ebics_public_bank_keys": base64.encodebytes(public_bank_keys),
"ebics_public_bank_keys_fn": fn,
"state": "to_verify",
}
self._update_passphrase_vals(vals)
return self.write(vals)
def ebics_init_4(self):
"""
Initialization of bank keys - Step 2:
Confirm Verification of the public bank keys
and activate the bank keys.
"""
self.ensure_one()
if self.state != "to_verify":
raise UserError(_("Set state to 'Verification'."))
keyring = EbicsKeyRing(
keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
)
bank = EbicsBank(
keyring=keyring,
hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url,
)
bank.activate_keys()
vals = {"state": "active_keys"}
self._update_passphrase_vals(vals)
return self.write(vals)
def change_passphrase(self):
self.ensure_one()
ctx = dict(self.env.context, default_ebics_userid_id=self.id)
module = __name__.split("addons.")[1].split(".")[0]
view = self.env.ref("%s.ebics_change_passphrase_view_form" % module)
return {
"name": _("EBICS keys change passphrase"),
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.change.passphrase",
"view_id": view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def _update_passphrase_vals(self, vals):
"""
Remove non-stored passphrases from db after e.g. successfull init_1
"""
if vals["state"] in ("init", "get_bank_keys", "to_verify", "active_keys"):
if not self.ebics_passphrase_store:
vals["ebics_passphrase"] = False
if self.ebics_sig_passphrase:
vals["ebics_sig_passphrase"] = False

View File

@ -0,0 +1,46 @@
# Copyright 2009-2020 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import logging
from sys import exc_info
from traceback import format_exception
from odoo.tools import config
_logger = logging.getLogger(__name__)
try:
import fintech
except ImportError:
fintech = None
_logger.warning("Failed to import fintech")
fintech_register_name = config.get("fintech_register_name")
fintech_register_keycode = config.get("fintech_register_keycode")
fintech_register_users = config.get("fintech_register_users")
try:
if fintech:
fintech_register_users = (
fintech_register_users
and [x.strip() for x in fintech_register_users.split(",")]
or None
)
fintech.cryptolib = "cryptography"
fintech.register(
name=fintech_register_name,
keycode=fintech_register_keycode,
users=fintech_register_users,
)
except RuntimeError as e:
if str(e) == "'register' can be called only once":
pass
else:
_logger.error(str(e))
fintech.register()
except Exception:
msg = "fintech.register error"
tb = "".join(format_exception(*exc_info()))
msg += "\n%s" % tb
_logger.error(msg)
fintech.register()

View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="res.groups" id="group_ebics_manager">
<field name="name">EBICS Manager</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<data noupdate="1">
<record id="ebics_config_comp_rule" model="ir.rule">
<field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_userid_comp_rule" model="ir.rule">
<field name="name">EBICS UserID model company rule</field>
<field name="model_id" ref="model_ebics_userid" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ebics_config_manager,ebics_config manager,model_ebics_config,group_ebics_manager,1,1,1,1
access_ebics_config_user,ebics_config user,model_ebics_config,account.group_account_invoice,1,0,0,0
access_ebics_userid_manager,ebics_userid manager,model_ebics_userid,group_ebics_manager,1,1,1,1
access_ebics_userid_user,ebics_userid user,model_ebics_userid,account.group_account_invoice,1,0,0,0
access_ebics_file_format_manager,ebics_file_format manager,model_ebics_file_format,group_ebics_manager,1,1,1,1
access_ebics_file_format_user,ebics_file_format user,model_ebics_file_format,account.group_account_invoice,1,0,0,0
access_ebics_file_manager,ebics_file manager,model_ebics_file,group_ebics_manager,1,1,1,1
access_ebics_file_user,ebics_file user,model_ebics_file,account.group_account_invoice,1,1,1,0
access_ebics_change_passphrase,access_ebics_change_passphrase,model_ebics_change_passphrase,group_ebics_manager,1,1,1,0
access_ebics_xfer,access_ebics_xfer,model_ebics_xfer,account.group_account_invoice,1,1,1,0
access_ebics_admin_order,access_ebics_admin_order,model_ebics_admin_order,group_ebics_manager,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ebics_config_manager ebics_config manager model_ebics_config group_ebics_manager 1 1 1 1
3 access_ebics_config_user ebics_config user model_ebics_config account.group_account_invoice 1 0 0 0
4 access_ebics_userid_manager ebics_userid manager model_ebics_userid group_ebics_manager 1 1 1 1
5 access_ebics_userid_user ebics_userid user model_ebics_userid account.group_account_invoice 1 0 0 0
6 access_ebics_file_format_manager ebics_file_format manager model_ebics_file_format group_ebics_manager 1 1 1 1
7 access_ebics_file_format_user ebics_file_format user model_ebics_file_format account.group_account_invoice 1 0 0 0
8 access_ebics_file_manager ebics_file manager model_ebics_file group_ebics_manager 1 1 1 1
9 access_ebics_file_user ebics_file user model_ebics_file account.group_account_invoice 1 1 1 0
10 access_ebics_change_passphrase access_ebics_change_passphrase model_ebics_change_passphrase group_ebics_manager 1 1 1 0
11 access_ebics_xfer access_ebics_xfer model_ebics_xfer account.group_account_invoice 1 1 1 0
12 access_ebics_admin_order access_ebics_admin_order model_ebics_admin_order group_ebics_manager 1 1 1 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,573 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://www.gnu.org/licenses/lgpl"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a>
<div class="section" id="ebics-banking-protocol">
<h1>EBICS banking protocol</h1>
<p>Implementation of the EBICS banking protocol.</p>
<p>This module facilitates the exchange of files with banks via the EBICS protocol.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="installation">
<h2>Installation</h2>
<p>The module depends upon</p>
<ul class="simple">
<li><a class="reference external" href="https://pypi.python.org/pypi/fintech">https://pypi.python.org/pypi/fintech</a></li>
<li><a class="reference external" href="https://pypi.python.org/pypi/cryptography">https://pypi.python.org/pypi/cryptography</a></li>
</ul>
<p>Remark:</p>
<p>The EBICS 'Test Mode' for uploading orders requires fintech 4.3.4 or higher for EBICS 2.x
and fintech 7.2.7 or higher for EBICS 3.0.</p>
<p>SWIFT 3SKey support requires fintech 6.4 or higher.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>We also recommend to consider the installation of the following modules:</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_oe</p>
<p>Required if you are running Odoo Enterprise</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_batch</p>
<p>This module adds a cron job for the automated import of EBICS files.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_batch_payment</p>
<p>Recommended if you are using the Odoo Enterprise account_batch_payment module</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_usability</p>
<p>Recommended if you have multiple financial journals.
This module adds a number of accounting menu entries such as bank statement list view
which allows to see all statements downloaded via the ir.cron automated EBICS download.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/account-financial-tools">https://github.com/OCA/account-financial-tools</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_payment_order</p>
<p>Required if you are using the OCA account_payment_order module.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/bank-payment">https://github.com/OCA/bank-payment</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_oca_statement_import</p>
<p>Required if you are using the OCA Bank Statement import modules.</p>
<p><a class="reference external" href="https://github.com/OCA/bank-statement-import">https://github.com/OCA/bank-statement-import</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_statement_import_fr_cfonb</p>
<p>Required to handle french CFONB files.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/l10n_france">https://github.com/OCA/l10n_france</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_statement_import_camt</p>
<p>Required to handle camt.052 and camt.054 files.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/bank-statement-import">https://github.com/OCA/bank-statement-import</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="fintech-license">
<h3>Fintech license</h3>
<p>If you have a valid Fintech.ebics license, you should add the following
licensing parameters to the odoo server configuration file:</p>
<ul class="simple">
<li>fintech_register_name</li>
</ul>
<p>The name of the licensee.</p>
<ul class="simple">
<li>fintech_register_keycode</li>
</ul>
<p>The keycode of the licensed version.</p>
<div class="line-block">
<div class="line"><br /></div>
<div class="line">Example:</div>
<div class="line"><br /></div>
</div>
<pre class="literal-block">
; fintech
fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
</pre>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Cf. <a class="reference external" href="https://www.joonis.de/en/fintech/prices/">https://www.joonis.de/en/fintech/prices/</a></p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
</div>
<div class="section" id="configuration">
<h2>Configuration</h2>
<p>Go to <strong>Settings &gt; Users</strong></p>
<p>Add the users that are authorised to maintain the EBICS configuration to the 'EBICS Manager' Group.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Go to <strong>Accounting &gt; Configuration &gt; Miscellaneous &gt; EBICS &gt; EBICS File Formats</strong></p>
<p>Check if the EBICS File formats that you want to process in Odoo are defined.</p>
<p>Most commonly used formats for which support is available in Odoo should be there already.</p>
<p>Please open an issue on <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a> to report missing EBICS File Formats.</p>
<p>For File Formats of type 'Downloads' you can also specify a 'Download Process Method'.</p>
<p>This is the method that will be executed when hitting the 'Process' button on the downloaded file.</p>
<p>The following methods are currently available:</p>
<ul class="simple">
<li>cfonb120</li>
<li>camt.053</li>
<li>camt.052</li>
<li>camt.054</li>
</ul>
<p>All these methods require complimentary modules to be installed (cf. Installation section supra).</p>
<p>You'll get an error message when the required module is not installed on your Odoo instance.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Go to <strong>Accounting &gt; Configuration &gt; Miscellaneous &gt; EBICS &gt; EBICS Configuration</strong></p>
<p>Configure your EBICS configuration according to the contract with your bank.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
<div class="section" id="usage">
<h2>Usage</h2>
<p>Go to <strong>Accounting &gt; Bank and Cash &gt; EBICS Processing</strong></p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
<div class="section" id="diagnostics">
<h2>Diagnostics</h2>
<p>Add the following to your Odoo config file in order to diagnose
issues with the EBICS connection with your bank:</p>
<p>log_handler = fintech.ebics:DEBUG</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="ebics-return-codes">
<h3>EBICS Return Codes</h3>
<p>During the processing of your EBICS upload/download, your bank may return an Error Code, e.g.</p>
<p>EBICS Functional Error:
EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 90005)</p>
<p>A detailed explanation of the codes can be found on <a class="reference external" href="http://www.ebics.org">http://www.ebics.org</a>.
You can also find this information in the doc folder of this module (file EBICS_Annex1_ReturnCodes).</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
<div class="section" id="electronic-distributed-signature-eds">
<h3>Electronic Distributed Signature (EDS)</h3>
<p>This is supported via external signing apps, e.g. BankingVEU:</p>
<ul class="simple">
<li><a class="reference external" href="https://play.google.com/store/apps/details?id=subsembly.bankingveu">https://play.google.com/store/apps/details?id=subsembly.bankingveu</a></li>
<li><a class="reference external" href="https://apps.apple.com/de/app/bankingveu/id1578694190">https://apps.apple.com/de/app/bankingveu/id1578694190</a></li>
</ul>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h2>Known Issues / Roadmap</h2>
<ul class="simple">
<li>The end user is currently not able to change his passphrases (only the users with 'EBICS Manager' rights can do so).</li>
<li>Add support to import externally generated keys &amp; certificates (currently only 3SKey signature certificate).</li>
<li>Add support for SWIFT 3SKey signing javascript lib (SConnect, cf <a class="reference external" href="https://www2.swift.com/3skey/help/sconnect.html">https://www2.swift.com/3skey/help/sconnect.html</a>).</li>
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_config_view_tree" model="ir.ui.view">
<field name="name">ebics.config.tree</field>
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<tree decoration-muted="state == 'draft'">
<field name="name" />
<field name="ebics_host" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
<record id="ebics_config_view_form" model="ir.ui.view">
<field name="name">ebics.config.form</field>
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<form string="EBICS Configuration">
<header>
<button
name="set_to_draft"
invisible="state != 'confirm'"
string="Set to Draft"
type="object"
groups="account_ebics.group_ebics_manager"
help="Set to Draft in order to change the EBICS configuration parameters."
/>
<button
name="set_to_confirm"
invisible="state != 'draft'"
string="Confirm"
type="object"
class="oe_highlight"
groups="account_ebics.group_ebics_manager"
help="The EBICS configuration must be confirmed before it can used for bank transactions."
/>
<field name="state" widget="statusbar" />
</header>
<field name="active" invisible="1" />
<widget
name="web_ribbon"
text="Archived"
bg_color="bg-danger"
invisible="active"
/>
<group name="main">
<group name="main-left">
<field name="name" readonly="state != 'draft'" colspan="2" />
<field name="ebics_host" readonly="state != 'draft'" />
<field name="ebics_url" readonly="state != 'draft'" />
<field name="ebics_partner" readonly="state != 'draft'" />
<field name="ebics_keys" readonly="state != 'draft'" />
</group>
<group name="main-right">
<field
name="journal_ids"
readonly="state != 'draft'"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_version" readonly="state != 'draft'" />
<field name="ebics_key_version" readonly="state != 'draft'" />
<field name="ebics_key_bitlength" readonly="state != 'draft'" />
<field
name="order_number"
invisible="ebics_version != 'H003'"
readonly="state != 'draft'"
/>
<field name="company_ids" widget="many2many_tags" invisible="1" />
</group>
</group>
<notebook>
<page string="EBICS Users" groups="account_ebics.group_ebics_manager">
<field name="ebics_userid_ids" readonly="state != 'draft'" />
</page>
<page string="File Formats" groups="account_ebics.group_ebics_manager">
<field name="ebics_file_format_ids" readonly="state != 'draft'" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_config_action" model="ir.actions.act_window">
<field name="name">EBICS Configuration</field>
<field name="res_model">ebics.config</field>
<field name="view_mode">tree,form</field>
<field name="context">{'active_test': False}</field>
</record>
</odoo>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_file_format_view_tree" model="ir.ui.view">
<field name="name">ebics.file.format.tree</field>
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<tree>
<field name="ebics_version" />
<field name="type" />
<field name="order_type" />
<field name="signature_class" />
<field name="name" />
<field name="description" />
</tree>
</field>
</record>
<record id="ebics_file_format_view_form" model="ir.ui.view">
<field name="name">ebics.file.format.form</field>
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<form string="EBICS File Format">
<group name="main">
<group name="main-left">
<field name="ebics_version" />
<field name="type" />
<field name="suffix" />
<field
name="download_process_method"
invisible="type == 'up'"
force_save="1"
/>
<field name="signature_class" />
</group>
<group name="main-right">
<field name="order_type" />
<field
name="name"
required="ebics_version == '2'"
invisible="ebics_version == '3'"
/>
<field
name="btf_service"
required="ebics_version == '3'"
invisible="ebics_version == '2'"
/>
<field
name="btf_message"
required="ebics_version == '3'"
invisible="ebics_version == '2'"
/>
<field name="btf_scope" invisible="ebics_version == '2'" />
<field name="btf_option" invisible="ebics_version == '2'" />
<field name="btf_container" invisible="ebics_version == '2'" />
<field name="btf_version" invisible="ebics_version == '2'" />
<field name="btf_variant" invisible="ebics_version == '2'" />
<field name="btf_format" invisible="ebics_version == '2'" />
</group>
</group>
<group name="description">
<field name="description" />
</group>
</form>
</field>
</record>
<record id="ebics_file_format_action" model="ir.actions.act_window">
<field name="name">EBICS File Formats</field>
<field name="res_model">ebics.file.format</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@ -0,0 +1,253 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_file_view_search" model="ir.ui.view">
<field name="name">ebics.file.search</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<search string="Search EBICS Files">
<group col="10" colspan="4">
<field name="date_from" />
<field name="date_to" />
<field name="name" />
<field name="format_id" />
<field name="user_id" />
<field
name="company_ids"
widget="selection"
groups="base.group_multi_company"
/>
</group>
<newline />
<group expand="0" string="Group By">
<filter
string="File Format"
name="file_format"
context="{'group_by':'format_id'}"
/>
<filter string="State" name="state" context="{'group_by':'state'}" />
<filter string="User" name="user" context="{'group_by':'user_id'}" />
</group>
</search>
</field>
</record>
<!-- Download -->
<record id="ebics_file_view_tree_download" model="ir.ui.view">
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Download Date" />
<field name="name" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
<record id="ebics_file_view_form_download" model="ir.ui.view">
<field name="name">ebics.file.form</field>
<field name="model">ebics.file</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button
name="set_to_draft"
invisible="state != 'done'"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="process"
class="oe_highlight"
invisible="state != 'draft'"
string="Process"
type="object"
groups="account.group_account_invoice"
help="Process the EBICS File"
/>
<button
name="set_to_done"
invisible="state != 'draft'"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" />
</header>
<group colspan="4" col="4">
<field name="date" string="Download Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1" />
</page>
<page string="Bank Statements" invisible="not bank_statement_ids">
<field name="bank_statement_ids" nolabel="1" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_file_view_form_result" model="ir.ui.view">
<field name="name">ebics.file.process.result</field>
<field name="model">ebics.file</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="Process EBICS File">
<separator colspan="4" string="Results :" />
<field name="note_process" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
name="action_open_bank_statements"
string="View Bank Statement(s)"
type="object"
class="oe_highlight"
invisible="not context.get('statement_ids')"
/>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>
</record>
<record id="ebics_file_action_download" model="ir.actions.act_window">
<field name="name">EBICS Download Files</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','down')]</field>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_download_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<record id="ebics_file_action_download_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<!-- Upload -->
<record id="ebics_file_view_tree_upload" model="ir.ui.view">
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Upload Date" />
<field name="name" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
<record id="ebics_file_view_form_upload" model="ir.ui.view">
<field name="name">ebics.file.form</field>
<field name="model">ebics.file</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button
name="set_to_draft"
invisible="state != 'done'"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="set_to_done"
invisible="state != 'draft'"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" />
</header>
<group colspan="4" col="4">
<field name="date" string="Upload Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_file_action_upload" model="ir.actions.act_window">
<field name="name">EBICS Upload Files</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','up')]</field>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_upload_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
<record id="ebics_file_action_upload_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
</odoo>

View File

@ -0,0 +1,173 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_userid_view_tree" model="ir.ui.view">
<field name="name">ebics.userid.tree</field>
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<tree decoration-muted="state != 'active_keys'">
<field name="name" />
<field name="signature_class" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
<record id="ebics_userid_view_form" model="ir.ui.view">
<field name="name">ebics.userid.form</field>
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<form string="EBICS UserID">
<header>
<button
name="ebics_init_1"
invisible="state != 'draft'"
string="EBICS Initialisation"
type="object"
class="oe_highlight"
help="Initialise EBICS Bank Keys"
/>
<button
name="ebics_init_2"
invisible="state != 'init'"
string="Account activated"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the account has been activated by the bank."
/>
<button
name="ebics_init_3"
invisible="state != 'get_bank_keys'"
string="Get Bank Keys"
type="object"
class="oe_highlight"
help="EBICS Initialisation - After the account has been activated the public bank keys must be downloaded and checked for consistency."
/>
<button
name="ebics_init_4"
invisible="state != 'to_verify'"
string="Bank Keys Verified"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the public have been checked for consistency."
/>
<button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="oe_highlight"
invisible="not ebics_keys_found or state != 'active_keys'"
/>
<button
name="set_to_draft"
invisible="state != 'active_keys'"
string="Set to Draft"
type="object"
help="Set to Draft in order to reinitialize your bank connection."
/>
<button
name="set_to_get_bank_keys"
invisible="state != 'active_keys'"
string="Renew Bank Keys"
type="object"
help="Use this button to update the EBICS certificates of your bank."
/>
<button
name="set_to_active_keys"
invisible="state != 'draft'"
string="Force Active Keys"
type="object"
help="Use this button to bypass the EBICS initialization (e.g. in case you have manually transferred active EBICS keys from another system."
/>
<field name="state" widget="statusbar" />
</header>
<group name="invisible" invisible="1">
<field name="ebics_keys_found" />
<field name="ebics_keys_fn" />
<field name="ebics_version" />
<field name="ebics_passphrase_required" />
<field name="ebics_passphrase_invisible" />
<field name="ebics_passphrase_store_readonly" />
<field name="ebics_sig_passphrase_invisible" invisible="1" />
</group>
<group name="main" readonly="state != 'draft'">
<group name="main-left">
<field name="name" readonly="state != 'draft'" />
<field
name="ebics_passphrase"
password="True"
required="ebics_passphrase_required"
invisible="ebics_passphrase_invisible"
/>
<field
name="ebics_passphrase_store"
readonly="ebics_passphrase_store_readonly"
/>
<field
name="ebics_sig_passphrase"
password="True"
invisible="ebics_sig_passphrase_invisible"
/>
<field name="transaction_rights" />
<field name="active" />
</group>
<group name="main-right">
<field name="signature_class" readonly="state != 'draft'" />
<field
name="user_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<!-- TODO: restore these fields after implementation of SWIFT SConnect
<field
name="swift_3skey"
invisible="signature_class == 'T'"
/>
<field name="swift_3skey_certificate_fn" invisible="1" />
<field
name="swift_3skey_certificate"
filename="swift_3skey_certificate_fn"
invisible="not swift_3skey"
required="swift_3skey"
/>
-->
<field name="ebics_key_x509" readonly="state != 'draft'" />
</group>
</group>
<group name="dn" invisible="not ebics_key_x509" readonly="state != 'draft'">
<div colspan="2" col="1">
<strong
>Distinguished Name attributes used to create self-signed X.509 certificates:</strong>
</div>
<group name="dn_l">
<field name="ebics_key_x509_dn_cn" readonly="state != 'draft'" />
<field name="ebics_key_x509_dn_o" readonly="state != 'draft'" />
<field name="ebics_key_x509_dn_l" readonly="state != 'draft'" />
<field name="ebics_key_x509_dn_c" readonly="state != 'draft'" />
</group>
<group name="dn_r">
<field name="ebics_key_x509_dn_e" readonly="state != 'draft'" />
<field name="ebics_key_x509_dn_ou" readonly="state != 'draft'" />
<field name="ebics_key_x509_dn_st" readonly="state != 'draft'" />
</group>
</group>
<group name="files">
<group colspan="2" name="ebics_ini_letter" invisible="not ebics_ini_letter">
<field name="ebics_ini_letter_fn" invisible="1" />
<field name="ebics_ini_letter" filename="ebics_ini_letter_fn" />
</group>
<group
colspan="2"
name="ebics_public_bank_keys"
invisible="not ebics_public_bank_keys"
>
<field name="ebics_public_bank_keys_fn" invisible="1" />
<field name="ebics_public_bank_keys" filename="ebics_public_bank_keys_fn" />
</group>
</group>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,85 @@
<?xml version="1.0" ?>
<odoo>
<menuitem
id="ebics_processing_menu"
name="EBICS Processing"
parent="account.menu_finance"
sequence="4"
/>
<menuitem
id="ebics_xfer_menu_download"
name="EBICS Download"
parent="ebics_processing_menu"
action="ebics_xfer_action_download"
sequence="10"
/>
<menuitem
id="ebics_xfer_menu_upload"
name="EBICS Upload"
parent="ebics_processing_menu"
action="ebics_xfer_action_upload"
sequence="20"
/>
<menuitem
id="ebics_file_menu"
name="EBICS Files"
parent="ebics_processing_menu"
sequence="30"
/>
<menuitem
id="ebics_file_menu_download"
name="Download"
parent="ebics_file_menu"
action="ebics_file_action_download"
sequence="10"
/>
<menuitem
id="ebics_file_menu_upload"
name="Upload"
parent="ebics_file_menu"
action="ebics_file_action_upload"
sequence="20"
/>
<menuitem
id="ebics_menu"
name="EBICS"
parent='account.menu_finance_configuration'
groups="account_ebics.group_ebics_manager"
sequence="100"
/>
<menuitem
id="ebics_config_menu"
name="EBICS Configuration"
parent="ebics_menu"
action="ebics_config_action"
groups="account_ebics.group_ebics_manager"
sequence="10"
/>
<menuitem
id="ebics_file_format_menu"
name="EBICS File Formats"
parent="ebics_menu"
action="ebics_file_format_action"
groups="account_ebics.group_ebics_manager"
sequence="20"
/>
<menuitem
id="ebics_admin_order_menu"
name="EBICS Administrative Orders"
parent="ebics_menu"
action="ebics_admin_order_action"
groups="account_ebics.group_ebics_manager"
sequence="30"
/>
</odoo>

View File

@ -0,0 +1,3 @@
from . import ebics_change_passphrase
from . import ebics_xfer
from . import ebics_admin_order

View File

@ -0,0 +1,52 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import pprint
from odoo import _, api, fields, models
class EbicsAdminOrder(models.TransientModel):
_inherit = "ebics.xfer"
_name = "ebics.admin.order"
_description = "EBICS Administrative Order"
admin_order_type = fields.Selection(
selection=lambda self: self._selection_admin_order_type(),
string="Order",
)
@api.model
def _selection_admin_order_type(self):
return [
("HAA", "HAA - Business transaction formats BTF"),
("HPD", "HPD - Bank parameters"),
("HKD", "HKD - Subscriber information"),
("HTD", "HTD - Customer properties and settings"),
]
def ebics_admin_order(self):
self.ensure_one()
client = self._setup_client()
if not client:
self.note += (
_("EBICS client setup failed for connection '%s'")
% self.ebics_config_id.name
)
else:
data = getattr(client, self.admin_order_type)(parsed=True)
pp = pprint.PrettyPrinter()
self.note = pp.pformat(data)
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_admin_order_view_form_result" % module)
return {
"name": _("EBICS Administrative Order result"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.admin.order",
"view_id": result_view.id,
"target": "new",
"context": self.env.context,
"type": "ir.actions.act_window",
}

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_admin_order_view_form" model="ir.ui.view">
<field name="name">EBICS Administrative Order</field>
<field name="model">ebics.admin.order</field>
<field name="inherit_id" ref="ebics_xfer_view_form_download" />
<field name="priority">1</field>
<field name="arch" type="xml">
<field name="date_from" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="date_to" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="format_id" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="order_type" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="order_type" position="after">
<field name="admin_order_type" />
</field>
<button name="ebics_download" position="attributes">
<attribute name="name">ebics_admin_order</attribute>
<attribute name="string">Execute</attribute>
</button>
</field>
</record>
<record id="ebics_admin_order_view_form_result" model="ir.ui.view">
<field name="name">EBICS Administrative Order result</field>
<field name="model">ebics.admin.order</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="EBICS Administrative Order result">
<separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
string="Close"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
<record id="ebics_admin_order_action" model="ir.actions.act_window">
<field name="name">EBICS Administrative Order</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.admin.order</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="ebics_admin_order_view_form" />
</record>
</odoo>

View File

@ -0,0 +1,120 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import _, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import EbicsKeyRing
fintech.cryptolib = "cryptography"
except ImportError:
_logger.warning("Failed to import fintech")
class EbicsChangePassphrase(models.TransientModel):
_name = "ebics.change.passphrase"
_description = "Change EBICS keys passphrase"
ebics_userid_id = fields.Many2one(
comodel_name="ebics.userid", string="EBICS UserID", readonly=True
)
old_pass = fields.Char(string="Old Passphrase")
new_pass = fields.Char(string="New Passphrase")
new_pass_check = fields.Char(string="New Passphrase (verification)")
old_sig_pass = fields.Char(string="Old Signature Passphrase")
new_sig_pass = fields.Char(string="New Signature Passphrase")
new_sig_pass_check = fields.Char(string="New Signature Passphrase (verification)")
ebics_sig_passphrase_invisible = fields.Boolean(
compute="_compute_ebics_sig_passphrase_invisible"
)
note = fields.Text(string="Notes", readonly=True)
def _compute_ebics_sig_passphrase_invisible(self):
for rec in self:
if fintech.__version_info__ < (7, 3, 1):
rec.ebics_sig_passphrase_invisible = True
else:
rec.ebics_sig_passphrase_invisible = False
def change_passphrase(self):
self.ensure_one()
self.note = ""
if (
self.ebics_userid_id.ebics_passphrase_store
and self.old_pass
and self.old_pass != self.ebics_userid_id.ebics_passphrase
):
raise UserError(_("Incorrect old passphrase."))
if self.new_pass != self.new_pass_check:
raise UserError(_("New passphrase verification error."))
if self.new_pass and self.new_pass == self.ebics_userid_id.ebics_passphrase:
raise UserError(_("New passphrase equal to old passphrase."))
if (
self.new_sig_pass
and self.old_sig_pass
and self.new_sig_pass == self.old_sig_pass
):
raise UserError(
_("New signature passphrase equal to old signature passphrase.")
)
if self.new_sig_pass != self.new_sig_pass_check:
raise UserError(_("New signature passphrase verification error."))
passphrase = (
self.ebics_userid_id.ebics_passphrase_store
and self.ebics_userid_id.ebics_passphrase
or self.old_pass
)
try:
keyring_params = {
"keys": self.ebics_userid_id.ebics_keys_fn,
"passphrase": passphrase,
}
if self.new_sig_pass:
keyring_params["sig_passphrase"] = self.old_sig_pass
keyring = EbicsKeyRing(**keyring_params)
change_params = {}
if self.new_pass:
change_params["passphrase"] = self.new_pass
if self.new_sig_pass:
change_params["sig_passphrase"] = self.new_sig_pass
if change_params:
keyring.change_passphrase(**change_params)
except (ValueError, RuntimeError) as err:
raise UserError(str(err)) from err
if self.new_pass:
self.ebics_userid_id.ebics_passphrase = (
self.ebics_userid_id.ebics_passphrase_store and self.new_pass
)
self.note += "The EBICS Passphrase has been changed."
if self.new_sig_pass:
# removing ebics_sig_passphrase from db should not be required
# but we do it for double safety
if self.ebics_userid_id.ebics_sig_passphrase:
self.ebics_userid_id.ebics_sig_passphrase = False
self.note += "The EBICS Signature Passphrase has been changed."
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref(
"%s.ebics_change_passphrase_view_form_result" % module
)
return {
"name": _("EBICS Keys Change Passphrase"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.change.passphrase",
"view_id": result_view.id,
"target": "new",
"type": "ir.actions.act_window",
}
def button_close(self):
self.ensure_one()
return {"type": "ir.actions.act_window_close"}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_change_passphrase_view_form" model="ir.ui.view">
<field name="name">EBICS Keys Change Passphrase</field>
<field name="model">ebics.change.passphrase</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<group>
<group name="pass">
<field name="old_pass" password="True" />
<field name="new_pass" password="True" />
<field name="new_pass_check" password="True" />
</group>
<group name="sig_pass" invisible="ebics_sig_passphrase_invisible">
<field name="old_sig_pass" password="True" />
<field name="new_sig_pass" password="True" />
<field name="new_sig_pass_check" password="True" />
</group>
<group name="invisible" invisible="1">
<field name="ebics_sig_passphrase_invisible" />
</group>
</group>
<footer>
<button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="btn-primary"
data-hotkey="q"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
<record id="ebics_change_passphrase_view_form_result" model="ir.ui.view">
<field name="name">EBICS Keys Change Passphrase</field>
<field name="model">ebics.change.passphrase</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
name="button_close"
type="object"
string="Close"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,658 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
import base64
import logging
from sys import exc_info
from traceback import format_exception
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import (
BusinessTransactionFormat,
EbicsBank,
EbicsClient,
EbicsFunctionalError,
EbicsKeyRing,
EbicsTechnicalError,
EbicsUser,
EbicsVerificationError,
)
fintech.cryptolib = "cryptography"
except ImportError:
EbicsBank = object
_logger.warning("Failed to import fintech")
class EbicsBank(EbicsBank):
def _next_order_id(self, partnerid):
"""
EBICS protocol version H003 requires generation of the OrderID.
The OrderID must be a string between 'A000' and 'ZZZZ' and
unique for each partner id.
"""
return hasattr(self, "_order_number") and self._order_number or "A000"
class EbicsXfer(models.TransientModel):
_name = "ebics.xfer"
_description = "EBICS file transfer"
ebics_config_id = fields.Many2one(
comodel_name="ebics.config",
string="EBICS Configuration",
domain=[("state", "=", "confirm")],
default=lambda self: self._default_ebics_config_id(),
)
ebics_userid_id = fields.Many2one(
comodel_name="ebics.userid", string="EBICS UserID"
)
ebics_passphrase = fields.Char(string="EBICS Passphrase")
ebics_passphrase_stored = fields.Char(
string="EBICS Stored Passphrase", related="ebics_userid_id.ebics_passphrase"
)
ebics_passphrase_store = fields.Boolean(
related="ebics_userid_id.ebics_passphrase_store"
)
ebics_sig_passphrase = fields.Char(
string="EBICS Signature Passphrase",
)
ebics_sig_passphrase_invisible = fields.Boolean(
compute="_compute_ebics_sig_passphrase_invisible"
)
date_from = fields.Date()
date_to = fields.Date()
upload_data = fields.Binary(string="File to Upload")
upload_fname = fields.Char(string="Upload Filename", default="")
upload_fname_dummy = fields.Char(
related="upload_fname", string="Dummy Upload Filename", readonly=True
)
format_id = fields.Many2one(
comodel_name="ebics.file.format",
string="EBICS File Format",
help="Select EBICS File Format to upload/download."
"\nLeave blank to download all available files.",
)
allowed_format_ids = fields.Many2many(
related="ebics_config_id.ebics_file_format_ids",
string="Allowed EBICS File Formats",
)
order_type = fields.Char(
related="format_id.order_type",
string="Order Type",
)
test_mode = fields.Boolean(
help="Select this option to test if the syntax of "
"the upload file is correct."
"\nThis option is only available for "
"Order Type 'FUL'.",
)
note = fields.Text(string="EBICS file transfer Log", readonly=True)
@api.model
def _default_ebics_config_id(self):
cfg_mod = self.env["ebics.config"]
cfg = cfg_mod.search(
[
("company_ids", "in", self.env.user.company_ids.ids),
("state", "=", "confirm"),
]
)
if cfg and len(cfg) == 1:
return cfg
else:
return cfg_mod
def _compute_ebics_sig_passphrase_invisible(self):
for rec in self:
rec.ebics_sig_passphrase_invisible = True
if fintech.__version_info__ < (7, 3, 1):
rec.ebics_sig_passphrase_invisible = True
else:
rec.ebics_sig_passphrase_invisible = False
@api.onchange("ebics_config_id")
def _onchange_ebics_config_id(self):
avail_userids = self.ebics_config_id.ebics_userid_ids.filtered(
lambda r: self.env.user.id in r.user_ids.ids
)
if self.env.context.get("ebics_download"): # Download Form
avail_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "down"
)
avail_userids = avail_userids.filtered(
lambda r: r.transaction_rights in ["both", "down"]
)
else: # Upload Form
avail_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "up"
)
avail_userids = avail_userids.filtered(
lambda r: r.transaction_rights in ["both", "up"]
)
if avail_formats and len(avail_formats) == 1:
self.format_id = avail_formats
else:
self.format_id = False
if avail_userids:
if len(avail_userids) == 1:
self.ebics_userid_id = avail_userids
else:
with_passphrase_userids = avail_userids.filtered(
lambda r: r.ebics_passphrase_store
)
if len(with_passphrase_userids) == 1:
self.ebics_userid_id = with_passphrase_userids
else:
self.ebics_userid_id = False
@api.onchange("upload_data")
def _onchange_upload_data(self):
self.upload_fname_dummy = self.upload_fname
self.format_id = False
self._detect_upload_format()
if not self.format_id:
upload_formats = (
self.format_id
or self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "up"
)
)
if len(upload_formats) > 1:
upload_formats = upload_formats.filtered(
lambda r: self.upload_fname.endswith(r.suffix or "")
)
if len(upload_formats) == 1:
self.format_id = upload_formats
@api.onchange("format_id")
def _onchange_format_id(self):
self.order_type = self.format_id.order_type
def ebics_upload(self):
self.ensure_one()
ctx = self._context.copy()
ebics_file = self._ebics_upload()
if ebics_file:
ctx["ebics_file_id"] = ebics_file.id
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
return {
"name": _("EBICS file transfer result"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def ebics_download(self):
self.ensure_one()
ctx = self.env.context.copy()
self.note = ""
err_cnt = 0
client = self._setup_client()
if not client:
err_cnt += 1
self.note += (
_("EBICS client setup failed for connection '%s'")
% self.ebics_config_id.name
)
else:
download_formats = (
self.format_id
or self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "down"
)
)
ebics_files = self.env["ebics.file"]
date_from = self.date_from and self.date_from.isoformat() or None
date_to = self.date_to and self.date_to.isoformat() or None
for df in download_formats:
try:
success = False
if df.order_type == "BTD":
btf = BusinessTransactionFormat(
df.btf_service,
df.btf_message,
scope=df.btf_scope or None,
option=df.btf_option or None,
container=df.btf_container or None,
version=df.btf_version or None,
variant=df.btf_variant or None,
format=df.btf_format or None,
)
data = client.BTD(btf, start=date_from, end=date_to)
elif df.order_type == "FDL":
data = client.FDL(df.name, date_from, date_to)
else:
params = None
if date_from and date_to:
params = {
"DateRange": {
"Start": date_from,
"End": date_to,
}
}
data = client.download(df.order_type, params=params)
ebics_files += self._handle_download_data(data, df)
success = True
except EbicsFunctionalError:
err_cnt += 1
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Functional Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name or df.description,
order_type=df.order_type,
)
self.note += "\n"
self.note += f"{e[1].message} (code: {e[1].code})"
except EbicsTechnicalError:
err_cnt += 1
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Technical Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name or df.description,
order_type=df.order_type,
)
self.note += "\n"
self.note += f"{e[1].message} (code: {e[1].code})"
except EbicsVerificationError:
err_cnt += 1
self.note += "\n"
self.note += _(
"EBICS Verification Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name or df.description,
order_type=df.order_type,
)
self.note += "\n"
self.note += _("The EBICS response could not be verified.")
except UserError as e:
err_cnt += 1
self.note += "\n"
self.note += _(
"Error detected during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name or df.description,
order_type=df.order_type,
)
self.note += "\n"
self.note += " ".join(e.args)
except Exception:
err_cnt += 1
self.note += "\n"
self.note += _(
"Unknown Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name or df.description,
order_type=df.order_type,
)
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
else:
# mark received data so that it is not included in further
# downloads
trans_id = client.last_trans_id
client.confirm_download(trans_id=trans_id, success=success)
ctx["ebics_file_ids"] = ebics_files.ids
if ebics_files:
self.note += "\n"
for f in ebics_files:
self.note += (
_("EBICS File '%s' is available for further processing.")
% f.name
)
self.note += "\n"
ctx["err_cnt"] = err_cnt
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
return {
"name": _("EBICS file transfer result"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def view_ebics_file(self):
self.ensure_one()
module = __name__.split("addons.")[1].split(".")[0]
act = self.env["ir.actions.act_window"]._for_xml_id(
f"{module}.ebics_file_action_download"
)
act["domain"] = [("id", "in", self._context["ebics_file_ids"])]
return act
def _ebics_upload(self):
self.ensure_one()
ebics_file = self.env["ebics.file"]
self.note = ""
client = self._setup_client()
if client:
upload_data = base64.decodebytes(self.upload_data)
ef_format = self.format_id
OrderID = False
try:
order_type = self.order_type
if order_type == "BTU":
btf = BusinessTransactionFormat(
ef_format.btf_service,
ef_format.btf_message,
scope=ef_format.btf_scope or None,
option=ef_format.btf_option or None,
container=ef_format.btf_container or None,
version=ef_format.btf_version or None,
variant=ef_format.btf_variant or None,
format=ef_format.btf_format or None,
)
kwargs = {}
if self.test_mode:
kwargs["TEST"] = "TRUE"
OrderID = client.BTU(btf, upload_data, **kwargs)
elif order_type == "FUL":
kwargs = {}
bank = self.ebics_config_id.journal_ids[0].bank_id
cc = bank.country.code
if cc:
kwargs["country"] = cc
if self.test_mode:
kwargs["TEST"] = "TRUE"
OrderID = client.FUL(ef_format.name, upload_data, **kwargs)
else:
OrderID = client.upload(order_type, upload_data)
if OrderID:
self.note += "\n"
self.note += (
_("EBICS File has been uploaded (OrderID %s).") % OrderID
)
ef_note = _("EBICS OrderID: %s") % OrderID
if self.env.context.get("origin"):
ef_note += "\n" + _("Origin: %s") % self._context["origin"]
suffix = self.format_id.suffix
fn = self.upload_fname
if suffix and not fn.endswith(suffix):
fn = ".".join([fn, suffix])
ef_vals = {
"name": self.upload_fname,
"data": self.upload_data,
"date": fields.Datetime.now(),
"format_id": self.format_id.id,
"state": "done",
"user_id": self._uid,
"ebics_userid_id": self.ebics_userid_id.id,
"note": ef_note,
"company_ids": [
self.env.context.get("force_company", self.env.company.id)
],
}
self._update_ef_vals(ef_vals)
ebics_file = self.env["ebics.file"].create(ef_vals)
except EbicsFunctionalError:
e = exc_info()
self.note += "\n"
self.note += _("EBICS Functional Error:")
self.note += "\n"
self.note += f"{e[1].message} (code: {e[1].code})"
except EbicsTechnicalError:
e = exc_info()
self.note += "\n"
self.note += _("EBICS Technical Error:")
self.note += "\n"
self.note += f"{e[1].message} (code: {e[1].code})"
except EbicsVerificationError:
self.note += "\n"
self.note += _("EBICS Verification Error:")
self.note += "\n"
self.note += _("The EBICS response could not be verified.")
except Exception:
self.note += "\n"
self.note += _("Unknown Error")
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
if self.ebics_config_id.ebics_version == "H003":
OrderID = self.ebics_config_id._get_order_number()
self.ebics_config_id.sudo()._update_order_number(OrderID)
ebics_file and self._payment_order_postprocess(ebics_file)
return ebics_file
def _payment_order_postprocess(self, ebics_file):
active_model = self.env.context.get("model")
if active_model == "account.payment.order":
order = self.env["account.payment.order"].browse(
self.env.context["active_id"]
)
order.generated2uploaded()
def _setup_client(self):
self.ebics_config_id._check_ebics_keys()
passphrase = self._get_passphrase()
keyring_params = {
"keys": self.ebics_userid_id.ebics_keys_fn,
"passphrase": passphrase,
}
if self.ebics_sig_passphrase:
keyring_params["sig_passphrase"] = self.ebics_sig_passphrase
try:
keyring = EbicsKeyRing(**keyring_params)
except (RuntimeError, ValueError) as err:
error = _("Error while accessing the EBICS Keys:")
error += "\n"
error += err.args[0]
raise UserError(error) from err
bank = EbicsBank(
keyring=keyring,
hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url,
)
if self.ebics_config_id.ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number()
signature_class = (
self.format_id.signature_class or self.ebics_userid_id.signature_class
)
user_params = {
"keyring": keyring,
"partnerid": self.ebics_config_id.ebics_partner,
"userid": self.ebics_userid_id.name,
}
# manual_approval replaced by transport_only class param in fintech 7.4
fintech74 = hasattr(EbicsUser, "transport_only")
if fintech74:
user_params["transport_only"] = signature_class == "T" and True or False
try:
user = EbicsUser(**user_params)
except ValueError as err:
error = _("Error while accessing the EBICS UserID:")
error += "\n"
err_str = err.args[0]
error += err.args[0]
if err_str == "unknown key format":
error += "\n"
error += _("Doublecheck your EBICS Passphrase and UserID settings.")
raise UserError(error) from err
# manual_approval replaced by transport_only class param in fintech 7.4
if not fintech74 and signature_class == "T":
user.manual_approval = True
try:
client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception:
self.note += "\n"
self.note += _("Unknown Error")
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
client = False
return client
def _get_passphrase(self):
return self.ebics_passphrase or self.ebics_passphrase_stored
def _file_format_methods(self):
"""
Extend this dictionary in order to add support
for extra file formats.
"""
res = {
"camt.xxx.cfonb120.stm": self._handle_cfonb120,
"camt.xxx.cfonb120.stm.rfi": self._handle_cfonb120,
"camt.052.001.02.stm": self._handle_camt052,
"camt.053.001.02.stm": self._handle_camt053,
}
return res
def _update_ef_vals(self, ef_vals):
"""
Adapt this method to customize the EBICS File values.
"""
if self.format_id and self.format_id.type == "up":
fn = ef_vals["name"]
dups = self._check_duplicate_ebics_file(fn, self.format_id)
if dups:
n = 1
fn = "_".join([fn, str(n)])
while self._check_duplicate_ebics_file(fn, self.format_id):
n += 1
fn = "_".join([fn, str(n)])
ef_vals["name"] = fn
def _handle_download_data(self, data, file_format):
ebics_files = self.env["ebics.file"]
if isinstance(data, dict):
for doc in data:
ebics_files += self._create_ebics_file(
data[doc], file_format, docname=doc
)
else:
ebics_files += self._create_ebics_file(data, file_format)
return ebics_files
def _create_ebics_file(self, data, file_format, docname=None):
fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner]
if docname:
fn_parts.append(docname)
else:
fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date.isoformat())
fn = "_".join(fn_parts)
ff_methods = self._file_format_methods()
if file_format.name in ff_methods:
data = ff_methods[file_format.name](data)
suffix = file_format.suffix
if suffix and not fn.endswith(suffix):
fn = ".".join([fn, suffix])
dups = self._check_duplicate_ebics_file(fn, file_format)
if dups:
raise UserError(
_(
"EBICS File with name '%s' has already been downloaded."
"\nPlease check this file and rename in case there is "
"no risk on duplicate transactions."
)
% fn
)
data = base64.encodebytes(data)
ef_vals = {
"name": fn,
"data": data,
"date": fields.Datetime.now(),
"date_from": self.date_from,
"date_to": self.date_to,
"format_id": file_format.id,
"user_id": self._uid,
"ebics_userid_id": self.ebics_userid_id.id,
"company_ids": self.ebics_config_id.company_ids.ids,
}
self._update_ef_vals(ef_vals)
ebics_file = self.env["ebics.file"].create(ef_vals)
return ebics_file
def _check_duplicate_ebics_file(self, fn, file_format):
dups = self.env["ebics.file"].search(
[("name", "=", fn), ("format_id", "=", file_format.id)]
)
return dups
def _detect_upload_format(self):
"""
Use this method in order to automatically detect and set the
EBICS upload file format.
"""
def _update_order_number(self, OrderID):
o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1):
if c == "9":
o_list[-i] = "A"
break
if c == "Z":
continue
else:
o_list[-i] = chr(ord(c) + 1)
break
next_nr = "".join(o_list)
if next_nr == "ZZZZ":
next_nr = "A000"
self.ebics_config_id.order_number = next_nr
def _insert_line_terminator(self, data_in, line_len):
data_in = data_in.replace(b"\n", b"").replace(b"\r", b"")
data_out = b""
max_len = len(data_in)
i = 0
while i + line_len <= max_len:
data_out += data_in[i : i + line_len] + b"\n"
i += line_len
return data_out
def _handle_cfonb120(self, data_in):
return self._insert_line_terminator(data_in, 120)
def _handle_cfonb240(self, data_in):
return self._insert_line_terminator(data_in, 240)
def _handle_camt052(self, data_in):
"""
Use this method if you need to fix camt files received
from your bank before passing them to the
Odoo Community CAMT parser.
Remark: Odoo Enterprise doesn't support camt.052.
"""
return data_in
def _handle_camt053(self, data_in):
"""
Use this method if you need to fix camt files received
from your bank before passing them to the
Odoo Enterprise or Community CAMT parser.
"""
return data_in

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_xfer_view_form_download" model="ir.ui.view">
<field name="name">EBICS File Download</field>
<field name="model">ebics.xfer</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File Download">
<group>
<separator string="Select your bank :" colspan="2" />
<field
name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id), ('user_ids.id', '=', uid), ('transaction_rights', 'in', ['both', 'down'])]"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_passphrase"
password="True"
invisible="ebics_passphrase_store"
required="not ebics_passphrase_store"
/>
<field name="ebics_passphrase_store" invisible="1" />
<field name="date_from" />
<field name="date_to" />
<field
name="format_id"
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field name="allowed_format_ids" invisible="1" />
</group>
<footer>
<button
name="ebics_download"
string="Download Files"
type="object"
class="btn-primary"
data-hotkey="q"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_view_form_upload" model="ir.ui.view">
<field name="name">EBICS File Upload</field>
<field name="model">ebics.xfer</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File Upload">
<group>
<separator string="Select your bank :" colspan="2" />
<field
name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id), ('user_ids.id', '=', uid), ('transaction_rights', 'in', ['both', 'up'])]"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_passphrase"
password="True"
invisible="ebics_passphrase_store"
required="not ebics_passphrase_store"
/>
<field
name="ebics_sig_passphrase"
password="True"
invisible="ebics_sig_passphrase_invisible"
/>
<field name="ebics_passphrase_store" invisible="1" />
<field name="ebics_sig_passphrase_invisible" invisible="1" />
<separator string="Select your file :" colspan="2" />
<field name="upload_data" filename="upload_fname" required="1" />
<field name="upload_fname" invisible="1" />
<field name="upload_fname_dummy" string="Upload Filename" />
<field
name="format_id"
required="1"
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field name="test_mode" invisible="order_type not in ('FUL', 'BTU')" />
<field name="allowed_format_ids" invisible="1" />
</group>
<footer>
<button
name="ebics_upload"
string="Upload File"
type="object"
class="btn-primary"
data-hotkey="q"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_view_form_result" model="ir.ui.view">
<field name="name">EBICS File Transfer</field>
<field name="model">ebics.xfer</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="EBICS File Transfer">
<separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
name="view_ebics_file"
type="object"
string="View EBICS File(s)"
class="btn-primary"
invisible="not context.get('ebics_file_ids')"
data-hotkey="q"
/>
<button
string="Close"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_action_download" model="ir.actions.act_window">
<field name="name">EBICS File Transfer - Download</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.xfer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_download': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_download" />
</record>
<record id="ebics_xfer_action_upload" model="ir.actions.act_window">
<field name="name">EBICS File Transfer - Upload</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.xfer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_upload': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_upload" />
</record>
</odoo>