[14.0] add support for EBICS 3.0

This commit is contained in:
Luc De Meyer 2022-12-18 20:55:23 +01:00
parent 4f9151617c
commit a98ea8ec23
19 changed files with 612 additions and 319 deletions

View File

@ -22,9 +22,12 @@ The module depends upon
Remark:
The EBICS 'Test Mode' for uploading orders requires Fintech 4.3.4 or higher.
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.
SWIFT 3SKey support requires Fintech 6.4 or higher.
|
@ -35,6 +38,8 @@ 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
|
@ -42,12 +47,16 @@ We also recommend to consider the installation of the following modules:
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_ebics_payment_order
@ -194,5 +203,4 @@ You can also find this information in the doc folder of this module (file EBICS_
Known Issues / Roadmap
======================
- add support for EBICS 3.0
- add support to import externally generated keys & certificates (currently only 3SKey signature certificate)

View File

@ -3,10 +3,10 @@
{
"name": "EBICS banking protocol",
"version": "14.0.1.0.6",
"version": "14.0.1.1.0",
"license": "LGPL-3",
"author": "Noviat",
"website": "www.noviat.com",
"website": "https://www.noviat.com",
"category": "Accounting & Finance",
"depends": ["account"],
"data": [

View File

@ -1,180 +1,215 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="1">
<odoo noupdate="1">
<!-- Download formats -->
<!-- Download formats -->
<record id="ebics_ff_C52" model="ebics.file.format">
<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_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="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_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="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_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="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_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="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_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="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_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="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_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="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_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="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_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>
<!-- Upload formats -->
<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>
<record id="ebics_ff_LCR" model="ebics.file.format">
<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>
<!-- Upload formats -->
<record id="ebics_ff_CCT" model="ebics.file.format">
<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_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_XE2" model="ebics.file.format">
<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_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_CDD" model="ebics.file.format">
<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_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_XE3" model="ebics.file.format">
<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_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_CDB" model="ebics.file.format">
<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_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_XE4" model="ebics.file.format">
<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_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_FUL_pain_001_001_02_sct" model="ebics.file.format">
<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_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>
</data>
</odoo>

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 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 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

@ -1,4 +1,4 @@
# Copyright 2009-2020 Noviat.
# Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
from odoo import fields, models

View File

@ -22,13 +22,13 @@ class EbicsConfig(models.Model):
_order = "name"
name = fields.Char(
string="Name",
readonly=True,
states={"draft": [("readonly", False)]},
required=True,
)
journal_ids = fields.Many2many(
comodel_name="account.journal",
relation="account_journal_ebics_config_rel",
readonly=True,
states={"draft": [("readonly", False)]},
string="Bank Accounts",
@ -52,7 +52,11 @@ class EbicsConfig(models.Model):
help="Contact your bank to get the EBICS URL.",
)
ebics_version = fields.Selection(
selection=[("H003", "H003 (2.4)"), ("H004", "H004 (2.5)")],
selection=[
("H003", "H003 (2.4)"),
("H004", "H004 (2.5)"),
("H005", "H005 (3.0)"),
],
string="EBICS protocol version",
readonly=True,
states={"draft": [("readonly", False)]},
@ -130,7 +134,6 @@ class EbicsConfig(models.Model):
)
state = fields.Selection(
[("draft", "Draft"), ("confirm", "Confirmed")],
string="State",
default="draft",
required=True,
readonly=True,
@ -143,11 +146,12 @@ class EbicsConfig(models.Model):
"\nThis number should match the following pattern : "
"[A-Z]{1}[A-Z0-9]{3}",
)
active = fields.Boolean(string="Active", default=True)
active = fields.Boolean(default=True)
company_ids = fields.Many2many(
comodel_name="res.company",
relation="ebics_config_res_company_rel",
string="Companies",
required=True,
readonly=True,
help="Companies sharing this EBICS contract.",
)
@ -179,9 +183,26 @@ class EbicsConfig(models.Model):
)
)
@api.onchange("journal_ids")
def _onchange_journal_ids(self):
self.company_ids = self.journal_ids.mapped("company_id")
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:

View File

@ -3,6 +3,8 @@
import base64
import logging
from sys import exc_info
from traceback import format_exception
from odoo import _, fields, models
from odoo.exceptions import UserError
@ -46,7 +48,6 @@ class EbicsFile(models.Model):
)
state = fields.Selection(
[("draft", "Draft"), ("done", "Done")],
string="State",
default="draft",
required=True,
readonly=True,
@ -93,8 +94,7 @@ class EbicsFile(models.Model):
def process(self):
self.ensure_one()
ctx = dict(self.env.context, allowed_company_ids=self.env.user.company_ids.ids)
self = self.with_context(ctx)
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
@ -159,33 +159,23 @@ class EbicsFile(models.Model):
if raise_if_not_found:
raise UserError(
_(
"The module to process the '%s' format is not installed "
"on your system. "
"\nPlease install module '%s'"
"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,
)
% (self.format_id.name, module)
)
return False
return True
def _process_result_action(self, res):
def _process_result_action(self, res_action):
notifications = []
st_line_ids = []
statement_ids = []
sts_data = []
if res.get("type") and res["type"] == "ir.actions.client":
notifications = res["context"].get("notifications", [])
st_line_ids = res["context"].get("statement_line_ids", [])
if notifications:
for notif in notifications:
parts = []
for k in ["type", "message", "details"]:
if notif.get(k):
msg = "{}: {}".format(k, notif[k])
parts.append(msg)
self.note_process += "\n".join(parts)
self.note_process += "\n"
self.note_process += "\n"
if res_action.get("type") and res_action["type"] == "ir.actions.client":
st_line_ids = res_action["context"].get("statement_line_ids", [])
if st_line_ids:
self.flush()
self.env.cr.execute(
@ -206,10 +196,10 @@ class EbicsFile(models.Model):
)
sts_data = self.env.cr.dictfetchall()
else:
if res.get("res_id"):
st_ids = res["res_id"]
if res_action.get("res_id"):
st_ids = res_action["res_id"]
else:
st_ids = res["domain"][2]
st_ids = res_action["domain"][0][2]
statements = self.env["account.bank.statement"].browse(st_ids)
for statement in statements:
sts_data.append(
@ -221,7 +211,29 @@ class EbicsFile(models.Model):
}
)
st_cnt = len(sts_data)
warning_cnt = error_cnt = 0
notifications = res_action["context"].get("notifications", [])
if notifications:
for notif in notifications:
if notif["type"] == "error":
error_cnt += 1
elif notif["type"] == "warning":
warning_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")]
self.note_process += "\n".join(parts)
self.note_process += "\n\n"
self.note_process += "\n"
if error_cnt:
self.note_process += (
_("Number of errors detected during import: %s: ") % error_cnt
)
self.note_process += "\n"
if warning_cnt:
self.note_process += (
_("Number of watnings detected during import: %s: ") % warning_cnt
)
if st_cnt:
self.note_process += "\n\n"
self.note_process += _("%s bank statements have been imported: ") % st_cnt
self.note_process += "\n"
for st_data in sts_data:
@ -232,7 +244,9 @@ class EbicsFile(models.Model):
)
statement_ids = [x["statement_id"] for x in sts_data]
if statement_ids:
self.sudo().bank_statement_ids = [(6, 0, statement_ids)]
self.sudo().bank_statement_ids = [(4, x) for x in statement_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=statement_ids)
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
@ -279,39 +293,73 @@ class EbicsFile(models.Model):
)
st_lines = b""
transactions = False
result = {
"type": "ir.actions.client",
"tag": "bank_statement_reconciliation_view",
"context": {
"statement_line_ids": [],
"company_ids": self.env.user.company_ids.ids,
"notifications": [],
},
}
wiz_ctx = dict(self.env.context, active_model="ebics.file")
result_action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
result_action["context"] = safe_eval(result_action["context"])
statement_ids = []
notifications = []
for i, wiz_vals in enumerate(wiz_vals_list, start=1):
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button()
ctx = res.get("context")
if res.get("res_model") == "account.bank.statement.import.journal.creation":
message = _("Error detected while importing statement number %s.\n") % i
message += _("No financial journal found.")
details = _("Bank account number: %s") % ctx.get(
"default_bank_acc_number"
)
result["context"]["notifications"].extend(
[
{
"type": "warning",
"message": message,
"details": details,
}
]
)
continue
result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
result["context"]["notifications"].extend(ctx["notifications"])
return self._process_result_action(result)
result = {
"statement_ids": [],
"notifications": [],
}
statement_filename = wiz_vals["statement_filename"]
wiz = (
self.env[wiz_model]
.with_context(active_model="ebics.file")
.create(wiz_vals)
)
try:
with self.env.cr.savepoint():
file_data = base64.b64decode(wiz_vals["statement_file"])
msg_hdr = _(
"{} : Import failed for statement number %(index)s, "
"filename %(fn)s:\n",
index=i,
fn=statement_filename,
)
wiz.import_single_file(file_data, result)
if not result["statement_ids"]:
message = msg_hdr.format(_("Warning"))
message += _(
"You have already imported this file, or this file "
"only contains already imported transactions."
)
notifications += [
{
"type": "warning",
"message": message,
}
]
else:
statement_ids.extend(result["statement_ids"])
notifications.extend(result["notifications"])
except UserError as e:
message = msg_hdr.format(_("Error"))
message += "".join(e.args)
notifications += [
{
"type": "error",
"message": message,
}
]
except Exception:
tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error"))
message += tb
notifications += [
{
"type": "error",
"message": message,
}
]
result_action["context"]["notifications"] = notifications
result_action["domain"] = [("id", "in", statement_ids)]
return self._process_result_action(result_action)
@staticmethod
def _unlink_cfonb120(self):
@ -360,11 +408,12 @@ class EbicsFile(models.Model):
if not found:
raise UserError(
_(
"The module to process the '%s' format is not installed "
"on your system. "
"\nPlease install one of the following modules: \n%s."
"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]),
)
% (self.format_id.name, ", ".join([x[1] for x in modules]))
)
if _src == "oca":
self._process_camt053_oca()
@ -386,25 +435,14 @@ class EbicsFile(models.Model):
"notifications": [],
},
}
wiz_ctx = dict(self.env.context, active_model="ebics.file")
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
wiz = (
self.env[wiz_model].with_context(active_model="ebics.file").create(wiz_vals)
)
res = wiz.import_file_button()
ctx = res.get("context")
if res.get("res_model") == "account.bank.statement.import.journal.creation":
message = _("Error detected while importing statement %s.\n") % self.name
message += _("No financial journal found.")
details = _("Bank account number: %s") % ctx.get("default_bank_acc_number")
result["context"]["notifications"].extend(
[
{
"type": "warning",
"message": message,
"details": details,
}
]
)
result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
result["context"]["notifications"].extend(ctx["notifications"])
result["context"]["statement_line_ids"].extend(
res["context"]["statement_line_ids"]
)
result["context"]["notifications"].extend(res["context"]["notifications"])
return self._process_result_action(result)
def _process_camt053_oe(self):
@ -418,8 +456,9 @@ class EbicsFile(models.Model):
)
]
}
ctx = dict(self.env.context, active_model="ebics.file")
wiz = self.env[wiz_model].with_context(ctx).create(wiz_vals)
wiz = (
self.env[wiz_model].with_context(active_model="ebics.file").create(wiz_vals)
)
res = wiz.import_file()
if res.get("res_model") == "account.bank.statement.import.journal.creation":
if res.get("context"):

View File

@ -1,4 +1,4 @@
# Copyright 2009-2020 Noviat.
# Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
from odoo import api, fields, models
@ -9,9 +9,17 @@ class EbicsFileFormat(models.Model):
_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",
required=True,
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"
@ -22,9 +30,9 @@ class EbicsFileFormat(models.Model):
selection=[("down", "Download"), ("up", "Upload")], required=True
)
order_type = fields.Char(
string="Order Type",
required=True,
help="E.g. C53 (check your EBICS contract).\n"
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.",
@ -40,7 +48,6 @@ class EbicsFileFormat(models.Model):
# move signature_class parameter so that it can be set per EBICS config
signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")],
string="Signature Class",
help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments."
@ -50,7 +57,48 @@ class EbicsFileFormat(models.Model):
description = fields.Char()
suffix = fields.Char(
required=True,
help="Specify the filename suffix for this File Format." "\nE.g. c53.xml",
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
@ -62,3 +110,10 @@ class EbicsFileFormat(models.Model):
def _onchange_type(self):
if self.type == "up":
self.download_process_method = False
def name_get(self):
res = []
for rec in self:
name = rec.ebics_version == "2" and rec.name or rec.btf_message
res.append((rec.id, name))
return res

View File

@ -1,4 +1,4 @@
# Copyright 2009-2020 Noviat.
# Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import base64
@ -14,8 +14,8 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# logging.basicConfig(
# level=logging.DEBUG,
# format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
# level=logging.DEBUG,
# format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
try:
import fintech
@ -75,7 +75,6 @@ class EbicsUserID(models.Model):
# Classes A and B are not yet supported.
signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")],
string="Signature Class",
required=True,
default="T",
readonly=True,
@ -157,12 +156,11 @@ class EbicsUserID(models.Model):
("to_verify", "Verification"),
("active_keys", "Active Keys"),
],
string="State",
default="draft",
required=True,
readonly=True,
)
active = fields.Boolean(string="Active", default=True)
active = fields.Boolean(default=True)
company_ids = fields.Many2many(
comodel_name="res.company",
string="Companies",
@ -175,7 +173,9 @@ class EbicsUserID(models.Model):
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 + "_keys")
rec.name
and keys_dir
and (keys_dir + "/" + rec.name.replace(" ", "_") + "_keys")
)
@api.depends("ebics_keys_fn")
@ -243,11 +243,11 @@ class EbicsUserID(models.Model):
partnerid=self.ebics_config_id.ebics_partner,
userid=self.name,
)
except Exception:
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error)
raise UserError(error) from err
self.ebics_config_id._check_ebics_keys()
if not os.path.isfile(self.ebics_keys_fn):
@ -256,7 +256,7 @@ class EbicsUserID(models.Model):
# enable import of all type of certicates: A00x, X002, E002
if self.swift_3skey:
kwargs = {
self.ebics_config_id.ebics_key_version: base64.decodestring(
self.ebics_config_id.ebics_key_version: base64.decodebytes(
self.swift_3skey_certificate
),
}
@ -265,11 +265,11 @@ class EbicsUserID(models.Model):
keyversion=self.ebics_config_id.ebics_key_version,
bitlength=self.ebics_config_id.ebics_key_bitlength,
)
except Exception:
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error)
raise UserError(error) from err
if self.swift_3skey and not self.ebics_key_x509:
raise UserError(
@ -302,7 +302,7 @@ class EbicsUserID(models.Model):
)
try:
supported_versions = client.HEV()
if ebics_version not in supported_versions:
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:
@ -314,7 +314,7 @@ class EbicsUserID(models.Model):
_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:
except URLError as err:
exctype, value = exc_info()[:2]
tb = "".join(format_exception(*exc_info()))
_logger.error(
@ -323,21 +323,24 @@ class EbicsUserID(models.Model):
tb,
)
raise UserError(
_("urlopen error:\n url '%s' - %s")
% (self.ebics_config_id.ebics_url, str(value))
)
except EbicsFunctionalError:
_(
"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 += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error)
except EbicsTechnicalError:
raise UserError(error) from err
except EbicsTechnicalError as err:
e = exc_info()
error = _("EBICS Technical Error:")
error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error)
raise UserError(error) from err
# Send the public authentication and encryption keys to the bank.
if ebics_version == "H003":
@ -411,25 +414,25 @@ class EbicsUserID(models.Model):
userid=self.name,
)
client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception:
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error)
raise UserError(error) from err
try:
public_bank_keys = client.HPB()
except EbicsFunctionalError:
except EbicsFunctionalError as err:
e = exc_info()
error = _("EBICS Functional Error:")
error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error)
except Exception:
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)
raise UserError(error) from err
public_bank_keys = public_bank_keys.encode()
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
@ -442,7 +445,7 @@ class EbicsUserID(models.Model):
)
self.write(
{
"ebics_public_bank_keys": base64.encodestring(public_bank_keys),
"ebics_public_bank_keys": base64.encodebytes(public_bank_keys),
"ebics_public_bank_keys_fn": fn,
"state": "to_verify",
}

View File

@ -379,8 +379,9 @@ ul.auto-toc {
<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.</p>
<p>SWIFT 3SKey support requires Fintech 6.4 or higher.</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>
@ -391,6 +392,7 @@ ul.auto-toc {
<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">
@ -399,6 +401,7 @@ ul.auto-toc {
<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">
@ -407,6 +410,7 @@ ul.auto-toc {
<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">
@ -547,7 +551,6 @@ You can also find this information in the doc folder of this module (file EBICS_
<div class="section" id="known-issues-roadmap">
<h2>Known Issues / Roadmap</h2>
<ul class="simple">
<li>add support for EBICS 3.0</li>
<li>add support to import externally generated keys &amp; certificates (currently only 3SKey signature certificate)</li>
</ul>
</div>

View File

@ -5,7 +5,7 @@
<field name="name">ebics.config.tree</field>
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<tree string="EBICS Configuration" decoration-muted="state == 'draft'">
<tree decoration-muted="state == 'draft'">
<field name="name" />
<field name="ebics_host" />
<field name="state" />
@ -66,10 +66,10 @@
<field name="ebics_key_bitlength" />
<field
name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"
attrs="{'invisible': [('ebics_version', '!=', 'H003')]}"
/>
<field name="company_ids" widget="many2many_tags" invisible="1" />
</group>
<field name="company_ids" invisible="1" />
</group>
<notebook>
<page string="EBICS Users" groups="account_ebics.group_ebics_manager">

View File

@ -5,7 +5,8 @@
<field name="name">ebics.file.format.tree</field>
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<tree string="EBICS File Formats">
<tree>
<field name="ebics_version" />
<field name="type" />
<field name="order_type" />
<field name="signature_class" />
@ -22,6 +23,7 @@
<form string="EBICS File Format">
<group name="main">
<group name="main-left">
<field name="ebics_version" />
<field name="type" />
<field name="suffix" />
<field
@ -33,7 +35,42 @@
</group>
<group name="main-right">
<field name="order_type" />
<field name="name" />
<field
name="name"
attrs="{'required': [('ebics_version', '=', '2')], 'invisible': [('ebics_version', '=', '3')]}"
/>
<field
name="btf_service"
attrs="{'required': [('ebics_version', '=', '3')], 'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_message"
attrs="{'required': [('ebics_version', '=', '3')], 'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_scope"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_option"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_container"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_version"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_variant"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_format"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
</group>
</group>
<group name="description">

View File

@ -38,7 +38,7 @@
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree string="EBICS Files" decoration-muted="state=='draft'" create="false">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Download Date" />
<field name="name" />
<field name="date_from" />
@ -169,7 +169,7 @@
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree string="EBICS Files" decoration-muted="state=='draft'" create="false">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Upload Date" />
<field name="name" />
<field name="user_id" />

View File

@ -5,7 +5,7 @@
<field name="name">ebics.userid.tree</field>
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<tree string="EBICS UserID" decoration-muted="state != 'active_keys'">
<tree decoration-muted="state != 'active_keys'">
<field name="name" />
<field name="signature_class" />
<field name="state" />

View File

@ -1,4 +1,4 @@
# Copyright 2009-2020 Noviat.
# Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging
@ -43,8 +43,8 @@ class EbicsChangePassphrase(models.TransientModel):
passphrase=self.ebics_userid_id.ebics_passphrase,
)
keyring.change_passphrase(self.new_pass)
except ValueError as e:
raise UserError(str(e))
except ValueError as err:
raise UserError(str(err)) from err
self.ebics_userid.ebics_passphrase = self.new_pass
self.note = "The EBICS Passphrase has been changed."

View File

@ -22,6 +22,7 @@ _logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import (
BusinessTransactionFormat,
EbicsBank,
EbicsClient,
EbicsFunctionalError,
@ -81,12 +82,8 @@ class EbicsXfer(models.TransientModel):
order_type = fields.Char(
related="format_id.order_type",
string="Order Type",
help="For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.",
)
test_mode = fields.Boolean(
string="Test Mode",
help="Select this option to test if the syntax of "
"the upload file is correct."
"\nThis option is only available for "
@ -203,7 +200,19 @@ class EbicsXfer(models.TransientModel):
for df in download_formats:
try:
success = False
if df.order_type == "FDL":
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
@ -222,8 +231,11 @@ class EbicsXfer(models.TransientModel):
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Functional Error during download of File Format %s (%s):"
) % (df.name, df.order_type)
"EBICS Functional Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsTechnicalError:
@ -231,8 +243,11 @@ class EbicsXfer(models.TransientModel):
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Technical Error during download of File Format %s (%s):"
) % (df.name, df.order_type)
"EBICS Technical Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsVerificationError:
@ -240,23 +255,31 @@ class EbicsXfer(models.TransientModel):
self.note += "\n"
self.note += _(
"EBICS Verification Error during download of "
"File Format %s (%s):"
) % (df.name, df.order_type)
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += _("The EBICS response could not be verified.")
except UserError as e:
self.note += "\n"
self.note += _(
"Warning during download of File Format %s (%s):"
) % (df.name, df.order_type)
"Warning during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += e.name
except Exception:
err_cnt += 1
self.note += "\n"
self.note += _(
"Unknown Error during download of File Format %s (%s):"
) % (df.name, df.order_type)
"Unknown Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
else:
@ -310,12 +333,27 @@ class EbicsXfer(models.TransientModel):
self.note = ""
client = self._setup_client()
if client:
upload_data = base64.decodestring(self.upload_data)
upload_data = base64.decodebytes(self.upload_data)
ef_format = self.format_id
OrderID = False
try:
order_type = self.order_type
if order_type == "FUL":
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

View File

@ -74,7 +74,7 @@
<field name="order_type" />
<field
name="test_mode"
attrs="{'invisible': [('order_type', '!=', 'FUL')]}"
attrs="{'invisible': [('order_type', 'not in', ('FUL', 'BTU'))]}"
/>
<field name="allowed_format_ids" invisible="1" />
</group>