diff --git a/account_ebics/README.rst b/account_ebics/README.rst index 73aa9cb..cd02e8e 100644 --- a/account_ebics/README.rst +++ b/account_ebics/README.rst @@ -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) diff --git a/account_ebics/__manifest__.py b/account_ebics/__manifest__.py index d78be3c..c9a0ac5 100644 --- a/account_ebics/__manifest__.py +++ b/account_ebics/__manifest__.py @@ -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": [ diff --git a/account_ebics/data/ebics_file_format.xml b/account_ebics/data/ebics_file_format.xml index 42119b9..05bad93 100644 --- a/account_ebics/data/ebics_file_format.xml +++ b/account_ebics/data/ebics_file_format.xml @@ -1,180 +1,215 @@ - - + - + - - camt.052 - down - C52 - camt.052 - bank to customer account report in format camt.052 - c52.xml - + + 2 + camt.052 + down + C52 + camt.052 + bank to customer account report in format camt.052 + c52.xml + - - camt.052 - down - Z52 - camt.052 - bank to customer account report in format camt.052 - c52.xml - + + 2 + camt.052 + down + Z52 + camt.052 + bank to customer account report in format camt.052 + c52.xml + - - camt.053 - down - C53 - camt.053 - Bank to customer statement report in format camt.053 - c53.xml - + + 2 + camt.053 + down + C53 + camt.053 + Bank to customer statement report in format camt.053 + c53.xml + - - camt.053 - down - Z53 - camt.053 - Bank to customer statement report in format camt.053 - c53.xml - + + 2 + camt.053 + down + Z53 + camt.053 + Bank to customer statement report in format camt.053 + c53.xml + - - camt.054 - down - C54 - camt.054 - Bank to customer debit credit notification in format camt.054 - c52.xml - + + 2 + camt.054 + down + C54 + camt.054 + Bank to customer debit credit notification in format camt.054 + c52.xml + - - camt.054 - down - Z54 - camt.054 - Bank to customer debit credit notification in format camt.054 - c52.xml - + + 2 + camt.054 + down + Z54 + camt.054 + Bank to customer debit credit notification in format camt.054 + c52.xml + - - camt.xxx.cfonb120.stm - down - FDL - cfonb120 - Bank to customer statement report in format cfonb120 - cfonb120.dat - + + 2 + camt.xxx.cfonb120.stm + down + FDL + cfonb120 + Bank to customer statement report in format cfonb120 + cfonb120.dat + - - pain.002 - down - CDZ - Payment status report for direct debit in format pain.002 - psr.xml - + + 2 + pain.002 + down + CDZ + Payment status report for direct debit in format pain.002 + psr.xml + - - pain.002 - down - Z01 - pain.002 - Payment status report for direct debit in format pain.002 - psr.xml - + + 2 + pain.002 + down + Z01 + pain.002 + Payment status report for direct debit in format pain.002 + psr.xml + - + + 3 + down + BTD + cfonb120 + Bank to customer statement report in format cfonb120 + cfonb120.dat + EOP + cfonb120 + - - pain.xxx.cfonb160.dco - up - FUL - Remises de LCR - txt - + - - pain.001.001.03 - up - CCT - Payment Order in format pain.001.001.03 - xml - + + 2 + pain.xxx.cfonb160.dco + up + FUL + Remises de LCR + txt + - - pain.001.001.03 - up - XE2 - Payment Order in format pain.001.001.03 - xml - + + 2 + pain.001.001.03 + up + CCT + Payment Order in format pain.001.001.03 + xml + - - pain.008.001.02.sdd - up - CDD - Sepa Core Direct Debit Order in format pain.008.001.02 - xml - + + 2 + pain.001.001.03 + up + XE2 + Payment Order in format pain.001.001.03 + xml + - - pain.008.001.02.sdd - up - XE3 - Sepa Core Direct Debit Order in format pain.008.001.02 - xml - + + 2 + pain.008.001.02.sdd + up + CDD + Sepa Core Direct Debit Order in format pain.008.001.02 + xml + - - pain.008.001.02.sbb - up - CDB - Sepa Direct Debit (B2B) Order in format pain.008.001.02 - xml - + + 2 + pain.008.001.02.sdd + up + XE3 + Sepa Core Direct Debit Order in format pain.008.001.02 + xml + - - pain.008.001.02.sbb - up - XE4 - Sepa Direct Debit (B2B) Order in format pain.008.001.02 - xml - + + 2 + pain.008.001.02.sbb + up + CDB + Sepa Direct Debit (B2B) Order in format pain.008.001.02 + xml + - - pain.001.001.02.sct - up - FUL - Payment Order in format pain.001.001.02 - xml - + + 2 + pain.008.001.02.sbb + up + XE4 + Sepa Direct Debit (B2B) Order in format pain.008.001.02 + xml + + + + 2 + pain.001.001.02.sct + up + FUL + Payment Order in format pain.001.001.02 + xml + + + + 3 + up + BTU + SEPA credit transfer + txt + SCT + pain.001 + GLB + - diff --git a/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf b/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf new file mode 100644 index 0000000..472c58f Binary files /dev/null and b/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf differ diff --git a/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf b/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf new file mode 100644 index 0000000..1df4109 Binary files /dev/null and b/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf differ diff --git a/account_ebics/migrations/14.0.1.1/pre-migration.py b/account_ebics/migrations/14.0.1.1/pre-migration.py new file mode 100644 index 0000000..4082ce7 --- /dev/null +++ b/account_ebics/migrations/14.0.1.1/pre-migration.py @@ -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), + ) diff --git a/account_ebics/models/account_bank_statement.py b/account_ebics/models/account_bank_statement.py index e1e1c2b..6b6540f 100644 --- a/account_ebics/models/account_bank_statement.py +++ b/account_ebics/models/account_bank_statement.py @@ -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 diff --git a/account_ebics/models/ebics_config.py b/account_ebics/models/ebics_config.py index 4cc9ec3..4cde65c 100644 --- a/account_ebics/models/ebics_config.py +++ b/account_ebics/models/ebics_config.py @@ -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: diff --git a/account_ebics/models/ebics_file.py b/account_ebics/models/ebics_file.py index 7e5b390..526a52d 100644 --- a/account_ebics/models/ebics_file.py +++ b/account_ebics/models/ebics_file.py @@ -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"): diff --git a/account_ebics/models/ebics_file_format.py b/account_ebics/models/ebics_file_format.py index 8dee0a9..d2ab636 100644 --- a/account_ebics/models/ebics_file_format.py +++ b/account_ebics/models/ebics_file_format.py @@ -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 diff --git a/account_ebics/models/ebics_userid.py b/account_ebics/models/ebics_userid.py index f7fc99f..7df3026 100644 --- a/account_ebics/models/ebics_userid.py +++ b/account_ebics/models/ebics_userid.py @@ -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", } diff --git a/account_ebics/static/description/index.html b/account_ebics/static/description/index.html index f4667ee..20511dc 100644 --- a/account_ebics/static/description/index.html +++ b/account_ebics/static/description/index.html @@ -379,8 +379,9 @@ ul.auto-toc {
  • https://pypi.python.org/pypi/cryptography
  • Remark:

    -

    The EBICS 'Test Mode' for uploading orders requires Fintech 4.3.4 or higher.

    -

    SWIFT 3SKey support requires Fintech 6.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.


    @@ -391,6 +392,7 @@ ul.auto-toc {
    @@ -399,6 +401,7 @@ ul.auto-toc {
    @@ -407,6 +410,7 @@ ul.auto-toc {
    @@ -547,7 +551,6 @@ 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)
    diff --git a/account_ebics/views/ebics_config_views.xml b/account_ebics/views/ebics_config_views.xml index d407af5..f7220d9 100644 --- a/account_ebics/views/ebics_config_views.xml +++ b/account_ebics/views/ebics_config_views.xml @@ -5,7 +5,7 @@ ebics.config.tree ebics.config - + @@ -66,10 +66,10 @@ + - diff --git a/account_ebics/views/ebics_file_format_views.xml b/account_ebics/views/ebics_file_format_views.xml index 03dd598..e677d3c 100644 --- a/account_ebics/views/ebics_file_format_views.xml +++ b/account_ebics/views/ebics_file_format_views.xml @@ -5,7 +5,8 @@ ebics.file.format.tree ebics.file.format - + + @@ -22,6 +23,7 @@
    + - + + + + + + + + + diff --git a/account_ebics/views/ebics_file_views.xml b/account_ebics/views/ebics_file_views.xml index 5961602..f64d4bb 100644 --- a/account_ebics/views/ebics_file_views.xml +++ b/account_ebics/views/ebics_file_views.xml @@ -38,7 +38,7 @@ ebics.file.tree ebics.file - + @@ -169,7 +169,7 @@ ebics.file.tree ebics.file - + diff --git a/account_ebics/views/ebics_userid_views.xml b/account_ebics/views/ebics_userid_views.xml index 8626d68..cd22353 100644 --- a/account_ebics/views/ebics_userid_views.xml +++ b/account_ebics/views/ebics_userid_views.xml @@ -5,7 +5,7 @@ ebics.userid.tree ebics.userid - + diff --git a/account_ebics/wizards/ebics_change_passphrase.py b/account_ebics/wizards/ebics_change_passphrase.py index 1ac046c..3f3c3ee 100644 --- a/account_ebics/wizards/ebics_change_passphrase.py +++ b/account_ebics/wizards/ebics_change_passphrase.py @@ -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." diff --git a/account_ebics/wizards/ebics_xfer.py b/account_ebics/wizards/ebics_xfer.py index 5493056..703ac47 100644 --- a/account_ebics/wizards/ebics_xfer.py +++ b/account_ebics/wizards/ebics_xfer.py @@ -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 diff --git a/account_ebics/wizards/ebics_xfer.xml b/account_ebics/wizards/ebics_xfer.xml index 3dfd4d9..0d28980 100644 --- a/account_ebics/wizards/ebics_xfer.xml +++ b/account_ebics/wizards/ebics_xfer.xml @@ -74,7 +74,7 @@