Merge pull request #55 from Noviat/14-ebics-3.0-support

[14.0] add support for EBICS 3.0
This commit is contained in:
Luc De Meyer 2022-12-24 12:00:23 +01:00 committed by GitHub
commit dd5ba5bb36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 639 additions and 320 deletions

View File

@ -22,9 +22,12 @@ The module depends upon
Remark: 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 - account_ebics_oe
Required if you are running Odoo Enterprise 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. This module adds a cron job for the automated import of EBICS files.
Cf. https://github.com/Noviat/account_ebics
| |
- account_ebics_batch_payment - account_ebics_batch_payment
Recommended if you are using the Odoo Enterprise account_batch_payment module Recommended if you are using the Odoo Enterprise account_batch_payment module
Cf. https://github.com/Noviat/account_ebics
| |
- account_ebics_payment_order - 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 Known Issues / Roadmap
====================== ======================
- add support for EBICS 3.0
- add support to import externally generated keys & certificates (currently only 3SKey signature certificate) - add support to import externally generated keys & certificates (currently only 3SKey signature certificate)

View File

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

View File

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

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

View File

@ -22,13 +22,13 @@ class EbicsConfig(models.Model):
_order = "name" _order = "name"
name = fields.Char( name = fields.Char(
string="Name",
readonly=True, readonly=True,
states={"draft": [("readonly", False)]}, states={"draft": [("readonly", False)]},
required=True, required=True,
) )
journal_ids = fields.Many2many( journal_ids = fields.Many2many(
comodel_name="account.journal", comodel_name="account.journal",
relation="account_journal_ebics_config_rel",
readonly=True, readonly=True,
states={"draft": [("readonly", False)]}, states={"draft": [("readonly", False)]},
string="Bank Accounts", string="Bank Accounts",
@ -52,7 +52,11 @@ class EbicsConfig(models.Model):
help="Contact your bank to get the EBICS URL.", help="Contact your bank to get the EBICS URL.",
) )
ebics_version = fields.Selection( 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", string="EBICS protocol version",
readonly=True, readonly=True,
states={"draft": [("readonly", False)]}, states={"draft": [("readonly", False)]},
@ -130,7 +134,6 @@ class EbicsConfig(models.Model):
) )
state = fields.Selection( state = fields.Selection(
[("draft", "Draft"), ("confirm", "Confirmed")], [("draft", "Draft"), ("confirm", "Confirmed")],
string="State",
default="draft", default="draft",
required=True, required=True,
readonly=True, readonly=True,
@ -143,11 +146,12 @@ class EbicsConfig(models.Model):
"\nThis number should match the following pattern : " "\nThis number should match the following pattern : "
"[A-Z]{1}[A-Z0-9]{3}", "[A-Z]{1}[A-Z0-9]{3}",
) )
active = fields.Boolean(string="Active", default=True) active = fields.Boolean(default=True)
company_ids = fields.Many2many( company_ids = fields.Many2many(
comodel_name="res.company", comodel_name="res.company",
relation="ebics_config_res_company_rel",
string="Companies", string="Companies",
required=True, readonly=True,
help="Companies sharing this EBICS contract.", help="Companies sharing this EBICS contract.",
) )
@ -159,6 +163,12 @@ class EbicsConfig(models.Model):
def _default_ebics_keys(self): def _default_ebics_keys(self):
return "/".join(["/etc/odoo/ebics_keys", self._cr.dbname]) 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") @api.constrains("order_number")
def _check_order_number(self): def _check_order_number(self):
for cfg in self: for cfg in self:
@ -179,9 +189,26 @@ class EbicsConfig(models.Model):
) )
) )
@api.onchange("journal_ids") def write(self, vals):
def _onchange_journal_ids(self): """
self.company_ids = self.journal_ids.mapped("company_id") 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): def unlink(self):
for ebics_config in self: for ebics_config in self:

View File

@ -3,6 +3,8 @@
import base64 import base64
import logging import logging
from sys import exc_info
from traceback import format_exception
from odoo import _, fields, models from odoo import _, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
@ -46,7 +48,6 @@ class EbicsFile(models.Model):
) )
state = fields.Selection( state = fields.Selection(
[("draft", "Draft"), ("done", "Done")], [("draft", "Draft"), ("done", "Done")],
string="State",
default="draft", default="draft",
required=True, required=True,
readonly=True, readonly=True,
@ -93,8 +94,7 @@ class EbicsFile(models.Model):
def process(self): def process(self):
self.ensure_one() self.ensure_one()
ctx = dict(self.env.context, allowed_company_ids=self.env.user.company_ids.ids) self = self.with_context(allowed_company_ids=self.env.user.company_ids.ids)
self = self.with_context(ctx)
self.note_process = "" self.note_process = ""
ff_methods = self._file_format_methods() ff_methods = self._file_format_methods()
ff = self.format_id.download_process_method ff = self.format_id.download_process_method
@ -159,33 +159,23 @@ class EbicsFile(models.Model):
if raise_if_not_found: if raise_if_not_found:
raise UserError( raise UserError(
_( _(
"The module to process the '%s' format is not installed " "The module to process the '%(ebics_format)s' "
"on your system. " "format is not installed on your system. "
"\nPlease install module '%s'" "\nPlease install module '%(module)s'",
ebics_format=self.format_id.name,
module=module,
) )
% (self.format_id.name, module)
) )
return False return False
return True return True
def _process_result_action(self, res): def _process_result_action(self, res_action):
notifications = [] notifications = []
st_line_ids = [] st_line_ids = []
statement_ids = [] statement_ids = []
sts_data = [] sts_data = []
if res.get("type") and res["type"] == "ir.actions.client": if res_action.get("type") and res_action["type"] == "ir.actions.client":
notifications = res["context"].get("notifications", []) st_line_ids = res_action["context"].get("statement_line_ids", [])
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 st_line_ids: if st_line_ids:
self.flush() self.flush()
self.env.cr.execute( self.env.cr.execute(
@ -206,10 +196,10 @@ class EbicsFile(models.Model):
) )
sts_data = self.env.cr.dictfetchall() sts_data = self.env.cr.dictfetchall()
else: else:
if res.get("res_id"): if res_action.get("res_id"):
st_ids = res["res_id"] st_ids = res_action["res_id"]
else: else:
st_ids = res["domain"][2] st_ids = res_action["domain"][0][2]
statements = self.env["account.bank.statement"].browse(st_ids) statements = self.env["account.bank.statement"].browse(st_ids)
for statement in statements: for statement in statements:
sts_data.append( sts_data.append(
@ -221,7 +211,29 @@ class EbicsFile(models.Model):
} }
) )
st_cnt = len(sts_data) 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: if st_cnt:
self.note_process += "\n\n"
self.note_process += _("%s bank statements have been imported: ") % st_cnt self.note_process += _("%s bank statements have been imported: ") % st_cnt
self.note_process += "\n" self.note_process += "\n"
for st_data in sts_data: for st_data in sts_data:
@ -232,7 +244,9 @@ class EbicsFile(models.Model):
) )
statement_ids = [x["statement_id"] for x in sts_data] statement_ids = [x["statement_id"] for x in sts_data]
if statement_ids: 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) ctx = dict(self.env.context, statement_ids=statement_ids)
module = __name__.split("addons.")[1].split(".")[0] module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module) result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
@ -279,39 +293,73 @@ class EbicsFile(models.Model):
) )
st_lines = b"" st_lines = b""
transactions = False transactions = False
result = { result_action = self.env["ir.actions.act_window"]._for_xml_id(
"type": "ir.actions.client", "account.action_bank_statement_tree"
"tag": "bank_statement_reconciliation_view", )
"context": { result_action["context"] = safe_eval(result_action["context"])
"statement_line_ids": [], statement_ids = []
"company_ids": self.env.user.company_ids.ids, notifications = []
"notifications": [],
},
}
wiz_ctx = dict(self.env.context, active_model="ebics.file")
for i, wiz_vals in enumerate(wiz_vals_list, start=1): for i, wiz_vals in enumerate(wiz_vals_list, start=1):
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals) result = {
res = wiz.import_file_button() "statement_ids": [],
ctx = res.get("context") "notifications": [],
if res.get("res_model") == "account.bank.statement.import.journal.creation": }
message = _("Error detected while importing statement number %s.\n") % i statement_filename = wiz_vals["statement_filename"]
message += _("No financial journal found.") wiz = (
details = _("Bank account number: %s") % ctx.get( self.env[wiz_model]
"default_bank_acc_number" .with_context(active_model="ebics.file")
) .create(wiz_vals)
result["context"]["notifications"].extend( )
[ try:
{ with self.env.cr.savepoint():
"type": "warning", file_data = base64.b64decode(wiz_vals["statement_file"])
"message": message, msg_hdr = _(
"details": details, "{} : Import failed for statement number %(index)s, "
} "filename %(fn)s:\n",
] index=i,
) fn=statement_filename,
continue )
result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"]) wiz.import_single_file(file_data, result)
result["context"]["notifications"].extend(ctx["notifications"]) if not result["statement_ids"]:
return self._process_result_action(result) 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 @staticmethod
def _unlink_cfonb120(self): def _unlink_cfonb120(self):
@ -360,11 +408,12 @@ class EbicsFile(models.Model):
if not found: if not found:
raise UserError( raise UserError(
_( _(
"The module to process the '%s' format is not installed " "The module to process the '%(ebics_format)s' format is "
"on your system. " "not installed on your system. "
"\nPlease install one of the following modules: \n%s." "\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": if _src == "oca":
self._process_camt053_oca() self._process_camt053_oca()
@ -386,25 +435,14 @@ class EbicsFile(models.Model):
"notifications": [], "notifications": [],
}, },
} }
wiz_ctx = dict(self.env.context, active_model="ebics.file") wiz = (
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals) self.env[wiz_model].with_context(active_model="ebics.file").create(wiz_vals)
)
res = wiz.import_file_button() res = wiz.import_file_button()
ctx = res.get("context") result["context"]["statement_line_ids"].extend(
if res.get("res_model") == "account.bank.statement.import.journal.creation": res["context"]["statement_line_ids"]
message = _("Error detected while importing statement %s.\n") % self.name )
message += _("No financial journal found.") result["context"]["notifications"].extend(res["context"]["notifications"])
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"])
return self._process_result_action(result) return self._process_result_action(result)
def _process_camt053_oe(self): def _process_camt053_oe(self):
@ -418,8 +456,9 @@ class EbicsFile(models.Model):
) )
] ]
} }
ctx = dict(self.env.context, active_model="ebics.file") wiz = (
wiz = self.env[wiz_model].with_context(ctx).create(wiz_vals) self.env[wiz_model].with_context(active_model="ebics.file").create(wiz_vals)
)
res = wiz.import_file() res = wiz.import_file()
if res.get("res_model") == "account.bank.statement.import.journal.creation": if res.get("res_model") == "account.bank.statement.import.journal.creation":
if res.get("context"): 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). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
from odoo import api, fields, models from odoo import api, fields, models
@ -9,9 +9,17 @@ class EbicsFileFormat(models.Model):
_description = "EBICS File Formats" _description = "EBICS File Formats"
_order = "type,name,order_type" _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( name = fields.Char(
string="Request Type", string="Request Type",
required=True,
help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n" help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
"Specify camt.052, camt.053, camt.054 for camt " "Specify camt.052, camt.053, camt.054 for camt "
"Order Types such as C53, Z53, C54, Z54.\n" "Order Types such as C53, Z53, C54, Z54.\n"
@ -22,9 +30,9 @@ class EbicsFileFormat(models.Model):
selection=[("down", "Download"), ("up", "Upload")], required=True selection=[("down", "Download"), ("up", "Upload")], required=True
) )
order_type = fields.Char( order_type = fields.Char(
string="Order Type",
required=True, 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 " "For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload " "format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.", "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 # move signature_class parameter so that it can be set per EBICS config
signature_class = fields.Selection( signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")], selection=[("E", "Single signature"), ("T", "Transport signature")],
string="Signature Class",
help="Please doublecheck the security of your Odoo " help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised " "ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments." "users to make supplier payments."
@ -50,7 +57,48 @@ class EbicsFileFormat(models.Model):
description = fields.Char() description = fields.Char()
suffix = fields.Char( suffix = fields.Char(
required=True, 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 @api.model
@ -62,3 +110,10 @@ class EbicsFileFormat(models.Model):
def _onchange_type(self): def _onchange_type(self):
if self.type == "up": if self.type == "up":
self.download_process_method = False 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). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import base64 import base64
@ -14,8 +14,8 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
# logging.basicConfig( # logging.basicConfig(
# level=logging.DEBUG, # level=logging.DEBUG,
# format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s') # format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
try: try:
import fintech import fintech
@ -64,6 +64,7 @@ class EbicsUserID(models.Model):
ebics_config_id = fields.Many2one( ebics_config_id = fields.Many2one(
comodel_name="ebics.config", string="EBICS Configuration", ondelete="cascade" comodel_name="ebics.config", string="EBICS Configuration", ondelete="cascade"
) )
ebics_version = fields.Selection(related="ebics_config_id.ebics_version")
user_ids = fields.Many2many( user_ids = fields.Many2many(
comodel_name="res.users", comodel_name="res.users",
string="Users", string="Users",
@ -75,7 +76,6 @@ class EbicsUserID(models.Model):
# Classes A and B are not yet supported. # Classes A and B are not yet supported.
signature_class = fields.Selection( signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")], selection=[("E", "Single signature"), ("T", "Transport signature")],
string="Signature Class",
required=True, required=True,
default="T", default="T",
readonly=True, readonly=True,
@ -157,12 +157,11 @@ class EbicsUserID(models.Model):
("to_verify", "Verification"), ("to_verify", "Verification"),
("active_keys", "Active Keys"), ("active_keys", "Active Keys"),
], ],
string="State",
default="draft", default="draft",
required=True, required=True,
readonly=True, readonly=True,
) )
active = fields.Boolean(string="Active", default=True) active = fields.Boolean(default=True)
company_ids = fields.Many2many( company_ids = fields.Many2many(
comodel_name="res.company", comodel_name="res.company",
string="Companies", string="Companies",
@ -175,7 +174,9 @@ class EbicsUserID(models.Model):
for rec in self: for rec in self:
keys_dir = rec.ebics_config_id.ebics_keys keys_dir = rec.ebics_config_id.ebics_keys
rec.ebics_keys_fn = ( 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") @api.depends("ebics_keys_fn")
@ -185,12 +186,23 @@ class EbicsUserID(models.Model):
rec.ebics_keys_fn rec.ebics_keys_fn
) )
@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") @api.constrains("ebics_passphrase")
def _check_ebics_passphrase(self): def _check_ebics_passphrase(self):
for rec in self: for rec in self:
if not rec.ebics_passphrase or len(rec.ebics_passphrase) < 8: if not rec.ebics_passphrase or len(rec.ebics_passphrase) < 8:
raise UserError(_("The passphrase must be at least 8 characters long")) raise UserError(_("The 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") @api.onchange("signature_class")
def _onchange_signature_class(self): def _onchange_signature_class(self):
if self.signature_class == "T": if self.signature_class == "T":
@ -243,11 +255,11 @@ class EbicsUserID(models.Model):
partnerid=self.ebics_config_id.ebics_partner, partnerid=self.ebics_config_id.ebics_partner,
userid=self.name, userid=self.name,
) )
except Exception: except Exception as err:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error) from err
self.ebics_config_id._check_ebics_keys() self.ebics_config_id._check_ebics_keys()
if not os.path.isfile(self.ebics_keys_fn): if not os.path.isfile(self.ebics_keys_fn):
@ -256,7 +268,7 @@ class EbicsUserID(models.Model):
# enable import of all type of certicates: A00x, X002, E002 # enable import of all type of certicates: A00x, X002, E002
if self.swift_3skey: if self.swift_3skey:
kwargs = { kwargs = {
self.ebics_config_id.ebics_key_version: base64.decodestring( self.ebics_config_id.ebics_key_version: base64.decodebytes(
self.swift_3skey_certificate self.swift_3skey_certificate
), ),
} }
@ -265,11 +277,11 @@ class EbicsUserID(models.Model):
keyversion=self.ebics_config_id.ebics_key_version, keyversion=self.ebics_config_id.ebics_key_version,
bitlength=self.ebics_config_id.ebics_key_bitlength, bitlength=self.ebics_config_id.ebics_key_bitlength,
) )
except Exception: except Exception as err:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value) 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: if self.swift_3skey and not self.ebics_key_x509:
raise UserError( raise UserError(
@ -292,7 +304,14 @@ class EbicsUserID(models.Model):
kwargs = {k: v for k, v in dn_attrs.items() if v} kwargs = {k: v for k, v in dn_attrs.items() if v}
user.create_certificates(**kwargs) user.create_certificates(**kwargs)
client = EbicsClient(bank, user, version=ebics_version) 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. # Send the public electronic signature key to the bank.
ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
@ -302,7 +321,7 @@ class EbicsUserID(models.Model):
) )
try: try:
supported_versions = client.HEV() 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 = _("EBICS version mismatch.") + "\n"
err_msg += _("Versions supported by your bank:") err_msg += _("Versions supported by your bank:")
for k in supported_versions: for k in supported_versions:
@ -314,7 +333,7 @@ class EbicsUserID(models.Model):
_logger.info("%s, EBICS INI command, OrderID=%s", self._name, OrderID) _logger.info("%s, EBICS INI command, OrderID=%s", self._name, OrderID)
if ebics_version == "H003": if ebics_version == "H003":
self.ebics_config_id._update_order_number(OrderID) self.ebics_config_id._update_order_number(OrderID)
except URLError: except URLError as err:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
tb = "".join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
_logger.error( _logger.error(
@ -323,21 +342,24 @@ class EbicsUserID(models.Model):
tb, tb,
) )
raise UserError( raise UserError(
_("urlopen error:\n url '%s' - %s") _(
% (self.ebics_config_id.ebics_url, str(value)) "urlopen error:\n url '%(url)s' - %(val)s",
) url=self.ebics_config_id.ebics_url,
except EbicsFunctionalError: val=str(value),
)
) from err
except EbicsFunctionalError as err:
e = exc_info() e = exc_info()
error = _("EBICS Functional Error:") error = _("EBICS Functional Error:")
error += "\n" error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code) error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) raise UserError(error) from err
except EbicsTechnicalError: except EbicsTechnicalError as err:
e = exc_info() e = exc_info()
error = _("EBICS Technical Error:") error = _("EBICS Technical Error:")
error += "\n" error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code) 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. # Send the public authentication and encryption keys to the bank.
if ebics_version == "H003": if ebics_version == "H003":
@ -411,25 +433,25 @@ class EbicsUserID(models.Model):
userid=self.name, userid=self.name,
) )
client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version) client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception: except Exception as err:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error) from err
try: try:
public_bank_keys = client.HPB() public_bank_keys = client.HPB()
except EbicsFunctionalError: except EbicsFunctionalError as err:
e = exc_info() e = exc_info()
error = _("EBICS Functional Error:") error = _("EBICS Functional Error:")
error += "\n" error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code) error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) raise UserError(error) from err
except Exception: except Exception as err:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error) from err
public_bank_keys = public_bank_keys.encode() public_bank_keys = public_bank_keys.encode()
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp") tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
@ -442,7 +464,7 @@ class EbicsUserID(models.Model):
) )
self.write( 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, "ebics_public_bank_keys_fn": fn,
"state": "to_verify", "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> <li><a class="reference external" href="https://pypi.python.org/pypi/cryptography">https://pypi.python.org/pypi/cryptography</a></li>
</ul> </ul>
<p>Remark:</p> <p>Remark:</p>
<p>The EBICS 'Test Mode' for uploading orders requires Fintech 4.3.4 or higher.</p> <p>The EBICS 'Test Mode' for uploading orders requires fintech 4.3.4 or higher for EBICS 2.x
<p>SWIFT 3SKey support requires Fintech 6.4 or higher.</p> 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-block">
<div class="line"><br /></div> <div class="line"><br /></div>
</div> </div>
@ -391,6 +392,7 @@ ul.auto-toc {
<ul> <ul>
<li><p class="first">account_ebics_oe</p> <li><p class="first">account_ebics_oe</p>
<p>Required if you are running Odoo Enterprise</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> </li>
</ul> </ul>
<div class="line-block"> <div class="line-block">
@ -399,6 +401,7 @@ ul.auto-toc {
<ul> <ul>
<li><p class="first">account_ebics_batch</p> <li><p class="first">account_ebics_batch</p>
<p>This module adds a cron job for the automated import of EBICS files.</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> </li>
</ul> </ul>
<div class="line-block"> <div class="line-block">
@ -407,6 +410,7 @@ ul.auto-toc {
<ul> <ul>
<li><p class="first">account_ebics_batch_payment</p> <li><p class="first">account_ebics_batch_payment</p>
<p>Recommended if you are using the Odoo Enterprise account_batch_payment module</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> </li>
</ul> </ul>
<div class="line-block"> <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"> <div class="section" id="known-issues-roadmap">
<h2>Known Issues / Roadmap</h2> <h2>Known Issues / Roadmap</h2>
<ul class="simple"> <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> <li>add support to import externally generated keys &amp; certificates (currently only 3SKey signature certificate)</li>
</ul> </ul>
</div> </div>

View File

@ -5,7 +5,7 @@
<field name="name">ebics.config.tree</field> <field name="name">ebics.config.tree</field>
<field name="model">ebics.config</field> <field name="model">ebics.config</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="EBICS Configuration" decoration-muted="state == 'draft'"> <tree decoration-muted="state == 'draft'">
<field name="name" /> <field name="name" />
<field name="ebics_host" /> <field name="ebics_host" />
<field name="state" /> <field name="state" />
@ -66,10 +66,10 @@
<field name="ebics_key_bitlength" /> <field name="ebics_key_bitlength" />
<field <field
name="order_number" name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}" attrs="{'invisible': [('ebics_version', '!=', 'H003')]}"
/> />
<field name="company_ids" widget="many2many_tags" invisible="1" />
</group> </group>
<field name="company_ids" invisible="1" />
</group> </group>
<notebook> <notebook>
<page string="EBICS Users" groups="account_ebics.group_ebics_manager"> <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="name">ebics.file.format.tree</field>
<field name="model">ebics.file.format</field> <field name="model">ebics.file.format</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="EBICS File Formats"> <tree>
<field name="ebics_version" />
<field name="type" /> <field name="type" />
<field name="order_type" /> <field name="order_type" />
<field name="signature_class" /> <field name="signature_class" />
@ -22,6 +23,7 @@
<form string="EBICS File Format"> <form string="EBICS File Format">
<group name="main"> <group name="main">
<group name="main-left"> <group name="main-left">
<field name="ebics_version" />
<field name="type" /> <field name="type" />
<field name="suffix" /> <field name="suffix" />
<field <field
@ -33,7 +35,42 @@
</group> </group>
<group name="main-right"> <group name="main-right">
<field name="order_type" /> <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> </group>
<group name="description"> <group name="description">

View File

@ -38,7 +38,7 @@
<field name="name">ebics.file.tree</field> <field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field> <field name="model">ebics.file</field>
<field name="arch" type="xml"> <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="date" string="Download Date" />
<field name="name" /> <field name="name" />
<field name="date_from" /> <field name="date_from" />
@ -169,7 +169,7 @@
<field name="name">ebics.file.tree</field> <field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field> <field name="model">ebics.file</field>
<field name="arch" type="xml"> <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="date" string="Upload Date" />
<field name="name" /> <field name="name" />
<field name="user_id" /> <field name="user_id" />

View File

@ -5,7 +5,7 @@
<field name="name">ebics.userid.tree</field> <field name="name">ebics.userid.tree</field>
<field name="model">ebics.userid</field> <field name="model">ebics.userid</field>
<field name="arch" type="xml"> <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="name" />
<field name="signature_class" /> <field name="signature_class" />
<field name="state" /> <field name="state" />
@ -85,6 +85,7 @@
<group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}"> <group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}">
<field name="ebics_keys_found" invisible="1" /> <field name="ebics_keys_found" invisible="1" />
<field name="ebics_keys_fn" invisible="1" /> <field name="ebics_keys_fn" invisible="1" />
<field name="ebics_version" invisible="1" />
<group name="main-left"> <group name="main-left">
<field name="name" /> <field name="name" />
<field <field

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). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging import logging
@ -43,8 +43,8 @@ class EbicsChangePassphrase(models.TransientModel):
passphrase=self.ebics_userid_id.ebics_passphrase, passphrase=self.ebics_userid_id.ebics_passphrase,
) )
keyring.change_passphrase(self.new_pass) keyring.change_passphrase(self.new_pass)
except ValueError as e: except ValueError as err:
raise UserError(str(e)) raise UserError(str(err)) from err
self.ebics_userid.ebics_passphrase = self.new_pass self.ebics_userid.ebics_passphrase = self.new_pass
self.note = "The EBICS Passphrase has been changed." self.note = "The EBICS Passphrase has been changed."

View File

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

View File

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