Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files)
+and using import parsers based upon the OCA account_statement_import module.
+
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
Required if you are processing bank statements with local bank account numbers
+and using import parsers based upon the Odoo Enterprise account_bank_statement_import module.
+
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
A detailled explanation of the codes can be found on http://www.ebics.org.
+You can also find this information in the doc folder of this module (file EBICS_Annex1_ReturnCodes).
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000..b15b21e
--- /dev/null
+++ b/account_ebics/views/ebics_config_views.xml
@@ -0,0 +1,93 @@
+
+
+
+
+ ebics.config.tree
+ ebics.config
+
+
+
+
+
+
+
+
+
+
+
+ ebics.config.form
+ ebics.config
+
+
+
+
+
+
+ EBICS Configuration
+ ebics.config
+ tree,form
+ {'active_test': False}
+
+
+
diff --git a/account_ebics/views/ebics_file_format_views.xml b/account_ebics/views/ebics_file_format_views.xml
new file mode 100644
index 0000000..e677d3c
--- /dev/null
+++ b/account_ebics/views/ebics_file_format_views.xml
@@ -0,0 +1,89 @@
+
+
+
+
+ ebics.file.format.tree
+ ebics.file.format
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.format.form
+ ebics.file.format
+
+
+
+
+
+
+ EBICS File Formats
+ ebics.file.format
+ tree,form
+
+
+
diff --git a/account_ebics/views/ebics_file_views.xml b/account_ebics/views/ebics_file_views.xml
new file mode 100644
index 0000000..f64d4bb
--- /dev/null
+++ b/account_ebics/views/ebics_file_views.xml
@@ -0,0 +1,256 @@
+
+
+
+
+ ebics.file.search
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.tree
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.form
+ ebics.file
+ 1
+
+
+
+
+
+
+ ebics.file.process.result
+ ebics.file
+ 2
+
+
+
+
+
+
+ EBICS Download Files
+ ir.actions.act_window
+ ebics.file
+ tree,form
+
+ [('type','=','down')]
+
+
+
+
+
+ tree
+
+
+
+
+
+
+ form
+
+
+
+
+
+
+
+ ebics.file.tree
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.form
+ ebics.file
+ 1
+
+
+
+
+
+
+ EBICS Upload Files
+ ir.actions.act_window
+ ebics.file
+ tree,form
+
+ [('type','=','up')]
+
+
+
+
+
+ tree
+
+
+
+
+
+
+ form
+
+
+
+
+
diff --git a/account_ebics/views/ebics_userid_views.xml b/account_ebics/views/ebics_userid_views.xml
new file mode 100644
index 0000000..cd22353
--- /dev/null
+++ b/account_ebics/views/ebics_userid_views.xml
@@ -0,0 +1,158 @@
+
+
+
+
+ ebics.userid.tree
+ ebics.userid
+
+
+
+
+
+
+
+
+
+
+
+ ebics.userid.form
+ ebics.userid
+
+
+
+
+
+
diff --git a/account_ebics/views/menu.xml b/account_ebics/views/menu.xml
new file mode 100644
index 0000000..b3e3b23
--- /dev/null
+++ b/account_ebics/views/menu.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_ebics/wizards/__init__.py b/account_ebics/wizards/__init__.py
new file mode 100644
index 0000000..b66b034
--- /dev/null
+++ b/account_ebics/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import ebics_change_passphrase
+from . import ebics_xfer
diff --git a/account_ebics/wizards/ebics_change_passphrase.py b/account_ebics/wizards/ebics_change_passphrase.py
new file mode 100644
index 0000000..3f3c3ee
--- /dev/null
+++ b/account_ebics/wizards/ebics_change_passphrase.py
@@ -0,0 +1,68 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+import logging
+
+from odoo import _, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import fintech
+ from fintech.ebics import EbicsKeyRing
+
+ fintech.cryptolib = "cryptography"
+except ImportError:
+ _logger.warning("Failed to import fintech")
+
+
+class EbicsChangePassphrase(models.TransientModel):
+ _name = "ebics.change.passphrase"
+ _description = "Change EBICS keys passphrase"
+
+ ebics_userid_id = fields.Many2one(
+ comodel_name="ebics.userid", string="EBICS UserID", readonly=True
+ )
+ old_pass = fields.Char(string="Old Passphrase", required=True)
+ new_pass = fields.Char(string="New Passphrase", required=True)
+ new_pass_check = fields.Char(string="New Passphrase (verification)", required=True)
+ note = fields.Text(string="Notes", readonly=True)
+
+ def change_passphrase(self):
+ self.ensure_one()
+ if self.old_pass != self.ebics_userid_id.ebics_passphrase:
+ raise UserError(_("Incorrect old passphrase."))
+ if self.new_pass != self.new_pass_check:
+ raise UserError(_("New passphrase verification error."))
+ if self.new_pass == self.ebics_userid_id.ebics_passphrase:
+ raise UserError(_("New passphrase equal to old passphrase."))
+ try:
+ keyring = EbicsKeyRing(
+ keys=self.ebics_userid_id.ebics_keys_fn,
+ passphrase=self.ebics_userid_id.ebics_passphrase,
+ )
+ keyring.change_passphrase(self.new_pass)
+ 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."
+
+ module = __name__.split("addons.")[1].split(".")[0]
+ result_view = self.env.ref(
+ "%s.ebics_change_passphrase_view_form_result" % module
+ )
+ return {
+ "name": _("EBICS Keys Change Passphrase"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.change.passphrase",
+ "view_id": result_view.id,
+ "target": "new",
+ "type": "ir.actions.act_window",
+ }
+
+ def button_close(self):
+ self.ensure_one()
+ return {"type": "ir.actions.act_window_close"}
diff --git a/account_ebics/wizards/ebics_change_passphrase.xml b/account_ebics/wizards/ebics_change_passphrase.xml
new file mode 100644
index 0000000..bb24f0a
--- /dev/null
+++ b/account_ebics/wizards/ebics_change_passphrase.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ EBICS Keys Change Passphrase
+ ebics.change.passphrase
+ 1
+
+
+
+
+
+
+ EBICS Keys Change Passphrase
+ ebics.change.passphrase
+ 2
+
+
+
+
+
+
diff --git a/account_ebics/wizards/ebics_xfer.py b/account_ebics/wizards/ebics_xfer.py
new file mode 100644
index 0000000..703ac47
--- /dev/null
+++ b/account_ebics/wizards/ebics_xfer.py
@@ -0,0 +1,643 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+"""
+import logging
+logging.basicConfig(
+ level=logging.DEBUG,
+ format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
+"""
+
+import base64
+import logging
+import os
+from sys import exc_info
+from traceback import format_exception
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import fintech
+ from fintech.ebics import (
+ BusinessTransactionFormat,
+ EbicsBank,
+ EbicsClient,
+ EbicsFunctionalError,
+ EbicsKeyRing,
+ EbicsTechnicalError,
+ EbicsUser,
+ EbicsVerificationError,
+ )
+
+ fintech.cryptolib = "cryptography"
+except ImportError:
+ EbicsBank = object
+ _logger.warning("Failed to import fintech")
+
+
+class EbicsBank(EbicsBank):
+ def _next_order_id(self, partnerid):
+ """
+ EBICS protocol version H003 requires generation of the OrderID.
+ The OrderID must be a string between 'A000' and 'ZZZZ' and
+ unique for each partner id.
+ """
+ return hasattr(self, "_order_number") and self._order_number or "A000"
+
+
+class EbicsXfer(models.TransientModel):
+ _name = "ebics.xfer"
+ _description = "EBICS file transfer"
+
+ ebics_config_id = fields.Many2one(
+ comodel_name="ebics.config",
+ string="EBICS Configuration",
+ domain=[("state", "=", "confirm")],
+ default=lambda self: self._default_ebics_config_id(),
+ )
+ ebics_userid_id = fields.Many2one(
+ comodel_name="ebics.userid", string="EBICS UserID"
+ )
+ ebics_passphrase = fields.Char(string="EBICS Passphrase")
+ date_from = fields.Date()
+ date_to = fields.Date()
+ upload_data = fields.Binary(string="File to Upload")
+ upload_fname = fields.Char(string="Upload Filename", default="")
+ upload_fname_dummy = fields.Char(
+ related="upload_fname", string="Upload Filename", readonly=True
+ )
+ format_id = fields.Many2one(
+ comodel_name="ebics.file.format",
+ string="EBICS File Format",
+ help="Select EBICS File Format to upload/download."
+ "\nLeave blank to download all available files.",
+ )
+ allowed_format_ids = fields.Many2many(
+ related="ebics_config_id.ebics_file_format_ids",
+ string="Allowed EBICS File Formats",
+ )
+ order_type = fields.Char(
+ related="format_id.order_type",
+ string="Order Type",
+ )
+ test_mode = fields.Boolean(
+ help="Select this option to test if the syntax of "
+ "the upload file is correct."
+ "\nThis option is only available for "
+ "Order Type 'FUL'.",
+ )
+ note = fields.Text(string="EBICS file transfer Log", readonly=True)
+
+ @api.model
+ def _default_ebics_config_id(self):
+ cfg_mod = self.env["ebics.config"]
+ cfg = cfg_mod.search(
+ [
+ ("company_ids", "in", self.env.user.company_ids.ids),
+ ("state", "=", "confirm"),
+ ]
+ )
+ if cfg and len(cfg) == 1:
+ return cfg
+ else:
+ return cfg_mod
+
+ @api.onchange("ebics_config_id")
+ def _onchange_ebics_config_id(self):
+ ebics_userids = self.ebics_config_id.ebics_userid_ids
+ if self._context.get("ebics_download"):
+ download_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "down"
+ )
+ if len(download_formats) == 1:
+ self.format_id = download_formats
+ if len(ebics_userids) == 1:
+ self.ebics_userid_id = ebics_userids
+ else:
+ transport_users = ebics_userids.filtered(
+ lambda r: r.signature_class == "T"
+ )
+ if len(transport_users) == 1:
+ self.ebics_userid_id = transport_users
+ else:
+ upload_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "up"
+ )
+ if len(upload_formats) == 1:
+ self.format_id = upload_formats
+ if len(ebics_userids) == 1:
+ self.ebics_userid_id = ebics_userids
+
+ @api.onchange("upload_data")
+ def _onchange_upload_data(self):
+ self.upload_fname_dummy = self.upload_fname
+ self.format_id = False
+ self._detect_upload_format()
+ if not self.format_id:
+ upload_formats = (
+ self.format_id
+ or self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "up"
+ )
+ )
+ if len(upload_formats) > 1:
+ upload_formats = upload_formats.filtered(
+ lambda r: self.upload_fname.endswith(r.suffix)
+ )
+ if len(upload_formats) == 1:
+ self.format_id = upload_formats
+
+ @api.onchange("format_id")
+ def _onchange_format_id(self):
+ self.order_type = self.format_id.order_type
+
+ def ebics_upload(self):
+ self.ensure_one()
+ ctx = self._context.copy()
+ ebics_file = self._ebics_upload()
+ if ebics_file:
+ ctx["ebics_file_id"] = ebics_file.id
+ module = __name__.split("addons.")[1].split(".")[0]
+ result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
+ return {
+ "name": _("EBICS file transfer result"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.xfer",
+ "view_id": result_view.id,
+ "target": "new",
+ "context": ctx,
+ "type": "ir.actions.act_window",
+ }
+
+ def ebics_download(self):
+ self.ensure_one()
+ self.ebics_config_id._check_ebics_files()
+ ctx = self.env.context.copy()
+ self.note = ""
+ err_cnt = 0
+ client = self._setup_client()
+ if not client:
+ err_cnt += 1
+ self.note += (
+ _("EBICS client setup failed for connection '%s'")
+ % self.ebics_config_id.name
+ )
+ else:
+ download_formats = (
+ self.format_id
+ or self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "down"
+ )
+ )
+ ebics_files = self.env["ebics.file"]
+ date_from = self.date_from and self.date_from.isoformat() or None
+ date_to = self.date_to and self.date_to.isoformat() or None
+ for df in download_formats:
+ try:
+ success = False
+ if df.order_type == "BTD":
+ btf = BusinessTransactionFormat(
+ df.btf_service,
+ df.btf_message,
+ scope=df.btf_scope or None,
+ option=df.btf_option or None,
+ container=df.btf_container or None,
+ version=df.btf_version or None,
+ variant=df.btf_variant or None,
+ format=df.btf_format or None,
+ )
+ data = client.BTD(btf, start=date_from, end=date_to)
+ elif df.order_type == "FDL":
+ data = client.FDL(df.name, date_from, date_to)
+ else:
+ params = None
+ if date_from and date_to:
+ params = {
+ "DateRange": {
+ "Start": date_from,
+ "End": date_to,
+ }
+ }
+ data = client.download(df.order_type, params=params)
+ ebics_files += self._handle_download_data(data, df)
+ success = True
+ except EbicsFunctionalError:
+ err_cnt += 1
+ e = exc_info()
+ self.note += "\n"
+ self.note += _(
+ "EBICS Functional Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += "{} (code: {})".format(e[1].message, e[1].code)
+ except EbicsTechnicalError:
+ err_cnt += 1
+ e = exc_info()
+ self.note += "\n"
+ self.note += _(
+ "EBICS Technical Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += "{} (code: {})".format(e[1].message, e[1].code)
+ except EbicsVerificationError:
+ err_cnt += 1
+ self.note += "\n"
+ self.note += _(
+ "EBICS Verification Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name,
+ 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 %(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 %(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:
+ # mark received data so that it is not included in further
+ # downloads
+ trans_id = client.last_trans_id
+ client.confirm_download(trans_id=trans_id, success=success)
+
+ ctx["ebics_file_ids"] = ebics_files.ids
+
+ if ebics_files:
+ self.note += "\n"
+ for f in ebics_files:
+ self.note += (
+ _("EBICS File '%s' is available for further processing.")
+ % f.name
+ )
+ self.note += "\n"
+
+ ctx["err_cnt"] = err_cnt
+ module = __name__.split("addons.")[1].split(".")[0]
+ result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
+ return {
+ "name": _("EBICS file transfer result"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.xfer",
+ "view_id": result_view.id,
+ "target": "new",
+ "context": ctx,
+ "type": "ir.actions.act_window",
+ }
+
+ def button_close(self):
+ self.ensure_one()
+ return {"type": "ir.actions.act_window_close"}
+
+ def view_ebics_file(self):
+ self.ensure_one()
+ module = __name__.split("addons.")[1].split(".")[0]
+ act = self.env["ir.actions.act_window"]._for_xml_id(
+ "{}.ebics_file_action_download".format(module)
+ )
+ act["domain"] = [("id", "in", self._context["ebics_file_ids"])]
+ return act
+
+ def _ebics_upload(self):
+ self.ensure_one()
+ ebics_file = self.env["ebics.file"]
+ self.note = ""
+ client = self._setup_client()
+ if client:
+ upload_data = base64.decodebytes(self.upload_data)
+ ef_format = self.format_id
+ OrderID = False
+ try:
+ order_type = self.order_type
+ if order_type == "BTU":
+ btf = BusinessTransactionFormat(
+ ef_format.btf_service,
+ ef_format.btf_message,
+ scope=ef_format.btf_scope or None,
+ option=ef_format.btf_option or None,
+ container=ef_format.btf_container or None,
+ version=ef_format.btf_version or None,
+ variant=ef_format.btf_variant or None,
+ format=ef_format.btf_format or None,
+ )
+ kwargs = {}
+ if self.test_mode:
+ kwargs["TEST"] = "TRUE"
+ OrderID = client.BTU(btf, upload_data, **kwargs)
+ elif order_type == "FUL":
+ kwargs = {}
+ bank = self.ebics_config_id.journal_ids[0].bank_id
+ cc = bank.country.code
+ if cc:
+ kwargs["country"] = cc
+ if self.test_mode:
+ kwargs["TEST"] = "TRUE"
+ OrderID = client.FUL(ef_format.name, upload_data, **kwargs)
+ else:
+ OrderID = client.upload(order_type, upload_data)
+ if OrderID:
+ self.note += "\n"
+ self.note += (
+ _("EBICS File has been uploaded (OrderID %s).") % OrderID
+ )
+ ef_note = _("EBICS OrderID: %s") % OrderID
+ if self.env.context.get("origin"):
+ ef_note += "\n" + _("Origin: %s") % self._context["origin"]
+ suffix = self.format_id.suffix
+ fn = self.upload_fname
+ if not fn.endswith(suffix):
+ fn = ".".join([fn, suffix])
+ ef_vals = {
+ "name": self.upload_fname,
+ "data": self.upload_data,
+ "date": fields.Datetime.now(),
+ "format_id": self.format_id.id,
+ "state": "done",
+ "user_id": self._uid,
+ "ebics_userid_id": self.ebics_userid_id.id,
+ "note": ef_note,
+ "company_ids": [
+ self.env.context.get("force_company", self.env.company.id)
+ ],
+ }
+ self._update_ef_vals(ef_vals)
+ ebics_file = self.env["ebics.file"].create(ef_vals)
+
+ except EbicsFunctionalError:
+ e = exc_info()
+ self.note += "\n"
+ self.note += _("EBICS Functional Error:")
+ self.note += "\n"
+ self.note += "{} (code: {})".format(e[1].message, e[1].code)
+ except EbicsTechnicalError:
+ e = exc_info()
+ self.note += "\n"
+ self.note += _("EBICS Technical Error:")
+ self.note += "\n"
+ self.note += "{} (code: {})".format(e[1].message, e[1].code)
+ except EbicsVerificationError:
+ self.note += "\n"
+ self.note += _("EBICS Verification Error:")
+ self.note += "\n"
+ self.note += _("The EBICS response could not be verified.")
+ except Exception:
+ self.note += "\n"
+ self.note += _("Unknown Error")
+ tb = "".join(format_exception(*exc_info()))
+ self.note += "\n%s" % tb
+
+ if self.ebics_config_id.ebics_version == "H003":
+ OrderID = self.ebics_config_id._get_order_number()
+ self.ebics_config_id.sudo()._update_order_number(OrderID)
+
+ return ebics_file
+
+ def _setup_client(self):
+ self.ebics_config_id._check_ebics_keys()
+ passphrase = self._get_passphrase()
+ keyring = EbicsKeyRing(
+ keys=self.ebics_userid_id.ebics_keys_fn, passphrase=passphrase
+ )
+
+ bank = EbicsBank(
+ keyring=keyring,
+ hostid=self.ebics_config_id.ebics_host,
+ url=self.ebics_config_id.ebics_url,
+ )
+ if self.ebics_config_id.ebics_version == "H003":
+ bank._order_number = self.ebics_config_id._get_order_number()
+
+ user = EbicsUser(
+ keyring=keyring,
+ partnerid=self.ebics_config_id.ebics_partner,
+ userid=self.ebics_userid_id.name,
+ )
+ signature_class = (
+ self.format_id.signature_class or self.ebics_userid_id.signature_class
+ )
+ if signature_class == "T":
+ user.manual_approval = True
+
+ try:
+ client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
+ except Exception:
+ self.note += "\n"
+ self.note += _("Unknown Error")
+ tb = "".join(format_exception(*exc_info()))
+ self.note += "\n%s" % tb
+ client = False
+
+ return client
+
+ def _get_passphrase(self):
+ passphrase = self.ebics_userid_id.ebics_passphrase
+
+ if passphrase:
+ return passphrase
+
+ module = __name__.split("addons.")[1].split(".")[0]
+ passphrase_view = self.env.ref("%s.ebics_xfer_view_form_passphrase" % module)
+ return {
+ "name": _("EBICS file transfer"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.xfer",
+ "view_id": passphrase_view.id,
+ "target": "new",
+ "context": self._context,
+ "type": "ir.actions.act_window",
+ }
+
+ def _file_format_methods(self):
+ """
+ Extend this dictionary in order to add support
+ for extra file formats.
+ """
+ res = {
+ "camt.xxx.cfonb120.stm": self._handle_cfonb120,
+ "camt.xxx.cfonb120.stm.rfi": self._handle_cfonb120,
+ "camt.052.001.02.stm": self._handle_camt052,
+ "camt.053.001.02.stm": self._handle_camt053,
+ }
+ return res
+
+ def _update_ef_vals(self, ef_vals):
+ """
+ Adapt this method to customize the EBICS File values.
+ """
+ if self.format_id and self.format_id.type == "up":
+ fn = ef_vals["name"]
+ dups = self._check_duplicate_ebics_file(fn, self.format_id)
+ if dups:
+ n = 1
+ fn = "_".join([fn, str(n)])
+ while self._check_duplicate_ebics_file(fn, self.format_id):
+ n += 1
+ fn = "_".join([fn, str(n)])
+ ef_vals["name"] = fn
+
+ def _handle_download_data(self, data, file_format):
+ ebics_files = self.env["ebics.file"]
+ if isinstance(data, dict):
+ for doc in data:
+ ebics_files += self._create_ebics_file(
+ data[doc], file_format, docname=doc
+ )
+ else:
+ ebics_files += self._create_ebics_file(data, file_format)
+ return ebics_files
+
+ def _create_ebics_file(self, data, file_format, docname=None):
+ """
+ Write the data as received over the EBICS connection
+ to a temporary file so that is is available for
+ analysis (e.g. in case formats are received that cannot
+ be handled in the current version of this module).
+
+ TODO: add code to clean-up /tmp on a regular basis.
+
+ After saving the data received we call the method to perform
+ file format specific processing.
+ """
+ ebics_files_root = self.ebics_config_id.ebics_files
+ tmp_dir = os.path.normpath(ebics_files_root + "/tmp")
+ if not os.path.isdir(tmp_dir):
+ os.makedirs(tmp_dir, mode=0o700)
+ fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner]
+ if docname:
+ fn_parts.append(docname)
+ else:
+ fn_date = self.date_to or fields.Date.today()
+ fn_parts.append(fn_date.isoformat())
+ base_fn = "_".join(fn_parts)
+ n = 1
+ full_tmp_fn = os.path.normpath(tmp_dir + "/" + base_fn)
+ while os.path.exists(full_tmp_fn):
+ n += 1
+ tmp_fn = base_fn + "_" + str(n).rjust(3, "0")
+ full_tmp_fn = os.path.normpath(tmp_dir + "/" + tmp_fn)
+
+ with open(full_tmp_fn, "wb") as f:
+ f.write(data)
+
+ ff_methods = self._file_format_methods()
+ if file_format.name in ff_methods:
+ data = ff_methods[file_format.name](data)
+
+ fn = ".".join([base_fn, file_format.suffix])
+ dups = self._check_duplicate_ebics_file(fn, file_format)
+ if dups:
+ raise UserError(
+ _(
+ "EBICS File with name '%s' has already been downloaded."
+ "\nPlease check this file and rename in case there is "
+ "no risk on duplicate transactions."
+ )
+ % fn
+ )
+ data = base64.encodebytes(data)
+ ef_vals = {
+ "name": fn,
+ "data": data,
+ "date": fields.Datetime.now(),
+ "date_from": self.date_from,
+ "date_to": self.date_to,
+ "format_id": file_format.id,
+ "user_id": self._uid,
+ "ebics_userid_id": self.ebics_userid_id.id,
+ "company_ids": self.ebics_config_id.company_ids.ids,
+ }
+ self._update_ef_vals(ef_vals)
+ ebics_file = self.env["ebics.file"].create(ef_vals)
+ return ebics_file
+
+ def _check_duplicate_ebics_file(self, fn, file_format):
+ dups = self.env["ebics.file"].search(
+ [("name", "=", fn), ("format_id", "=", file_format.id)]
+ )
+ return dups
+
+ def _detect_upload_format(self):
+ """
+ Use this method in order to automatically detect and set the
+ EBICS upload file format.
+ """
+
+ def _update_order_number(self, OrderID):
+ o_list = list(OrderID)
+ for i, c in enumerate(reversed(o_list), start=1):
+ if c == "9":
+ o_list[-i] = "A"
+ break
+ if c == "Z":
+ continue
+ else:
+ o_list[-i] = chr(ord(c) + 1)
+ break
+ next_nr = "".join(o_list)
+ if next_nr == "ZZZZ":
+ next_nr = "A000"
+ self.ebics_config_id.order_number = next_nr
+
+ def _insert_line_terminator(self, data_in, line_len):
+ data_in = data_in.replace(b"\n", b"").replace(b"\r", b"")
+ data_out = b""
+ max_len = len(data_in)
+ i = 0
+ while i + line_len <= max_len:
+ data_out += data_in[i : i + line_len] + b"\n"
+ i += line_len
+ return data_out
+
+ def _handle_cfonb120(self, data_in):
+ return self._insert_line_terminator(data_in, 120)
+
+ def _handle_cfonb240(self, data_in):
+ return self._insert_line_terminator(data_in, 240)
+
+ def _handle_camt052(self, data_in):
+ """
+ Use this method if you need to fix camt files received
+ from your bank before passing them to the
+ Odoo Community CAMT parser.
+ Remark: Odoo Enterprise doesn't support camt.052.
+ """
+ return data_in
+
+ def _handle_camt053(self, data_in):
+ """
+ Use this method if you need to fix camt files received
+ from your bank before passing them to the
+ Odoo Enterprise or Community CAMT parser.
+ """
+ return data_in
diff --git a/account_ebics/wizards/ebics_xfer.xml b/account_ebics/wizards/ebics_xfer.xml
new file mode 100644
index 0000000..0d28980
--- /dev/null
+++ b/account_ebics/wizards/ebics_xfer.xml
@@ -0,0 +1,137 @@
+
+
+
+
+ EBICS File Download
+ ebics.xfer
+ 1
+
+
+
+
+
+
+ EBICS File Upload
+ ebics.xfer
+ 1
+
+
+
+
+
+
+ EBICS File Transfer
+ ebics.xfer
+ 2
+
+
+
+
+
+
+ EBICS File Transfer
+ ir.actions.act_window
+ ebics.xfer
+ form
+ new
+ {'ebics_download': 1}
+
+
+
+
+ EBICS File Transfer
+ ir.actions.act_window
+ ebics.xfer
+ form
+ new
+ {'ebics_upload': 1}
+
+
+
+
diff --git a/account_ebics_batch_payment/README.rst b/account_ebics_batch_payment/README.rst
new file mode 100644
index 0000000..1a28c97
--- /dev/null
+++ b/account_ebics_batch_payment/README.rst
@@ -0,0 +1,24 @@
+.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
+ :target: https://www.gnu.org/licenses/lpgl
+ :alt: License: AGPL-3
+
+==============================
+Upload Batch Payment via EBICS
+==============================
+
+This module allows to upload a Batch Payment to the bank via the EBICS protocol.
+
+Installation
+============
+
+This module depends upon the following modules:
+
+- account_ebics (cf. https://github.com/Noviat/account_ebics)
+- account_ebics_oe (cf. https://github.com/Noviat/account_ebics)
+- account_batch_payment (Odoo Enterprise)
+
+Usage
+=====
+
+Create your Batch Payment and generate the bank file.
+Upload the generated file via the 'EBICS Upload' button on the batch payment.
diff --git a/account_ebics_batch_payment/__init__.py b/account_ebics_batch_payment/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/account_ebics_batch_payment/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/account_ebics_batch_payment/__manifest__.py b/account_ebics_batch_payment/__manifest__.py
new file mode 100644
index 0000000..f6b0415
--- /dev/null
+++ b/account_ebics_batch_payment/__manifest__.py
@@ -0,0 +1,14 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "Upload Batch Payment via EBICS",
+ "version": "15.0.1.0.0",
+ "license": "LGPL-3",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Accounting & Finance",
+ "depends": ["account_ebics", "account_batch_payment"],
+ "data": ["views/account_batch_payment_views.xml"],
+ "installable": True,
+}
diff --git a/account_ebics_batch_payment/models/__init__.py b/account_ebics_batch_payment/models/__init__.py
new file mode 100644
index 0000000..015ee74
--- /dev/null
+++ b/account_ebics_batch_payment/models/__init__.py
@@ -0,0 +1 @@
+from . import account_batch_payment
diff --git a/account_ebics_batch_payment/models/account_batch_payment.py b/account_ebics_batch_payment/models/account_batch_payment.py
new file mode 100644
index 0000000..62e51db
--- /dev/null
+++ b/account_ebics_batch_payment/models/account_batch_payment.py
@@ -0,0 +1,53 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AccountBatchPayment(models.Model):
+ _inherit = "account.batch.payment"
+
+ def ebics_upload(self):
+ self.ensure_one()
+ ctx = self.env.context.copy()
+
+ origin = _("Batch Payment") + ": " + self.name
+ ebics_config = self.env["ebics.config"].search(
+ [
+ ("journal_ids", "=", self.journal_id.id),
+ ("state", "=", "confirm"),
+ ]
+ )
+ if not ebics_config:
+ raise UserError(
+ _("No active EBICS configuration available " "for the selected bank.")
+ )
+ if len(ebics_config) == 1:
+ ctx["default_ebics_config_id"] = ebics_config.id
+ ctx.update(
+ {
+ "default_upload_data": self.export_file,
+ "default_upload_fname": self.export_filename,
+ "origin": origin,
+ "force_comany": self.journal_id.company_id.id,
+ }
+ )
+
+ ebics_xfer = self.env["ebics.xfer"].with_context(**ctx).create({})
+ ebics_xfer._onchange_ebics_config_id()
+ ebics_xfer._onchange_upload_data()
+ ebics_xfer._onchange_format_id()
+ view = self.env.ref("account_ebics.ebics_xfer_view_form_upload")
+ act = {
+ "name": _("EBICS Upload"),
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.xfer",
+ "view_id": view.id,
+ "res_id": ebics_xfer.id,
+ "type": "ir.actions.act_window",
+ "target": "new",
+ "context": ctx,
+ }
+ return act
diff --git a/account_ebics_batch_payment/static/description/icon.png b/account_ebics_batch_payment/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics_batch_payment/static/description/icon.png differ
diff --git a/account_ebics_batch_payment/views/account_batch_payment_views.xml b/account_ebics_batch_payment/views/account_batch_payment_views.xml
new file mode 100644
index 0000000..ef1f564
--- /dev/null
+++ b/account_ebics_batch_payment/views/account_batch_payment_views.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ account.batch.payment.form
+ account.batch.payment
+
+
+
+
+
+
+
diff --git a/account_ebics_oe/README.rst b/account_ebics_oe/README.rst
new file mode 100644
index 0000000..cf11b5f
--- /dev/null
+++ b/account_ebics_oe/README.rst
@@ -0,0 +1,16 @@
+.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
+ :target: https://www.gnu.org/licenses/lgpl
+ :alt: License: LGPL-3
+
+==============================================
+Deploy account_ebics module on Odoo Enterprise
+==============================================
+
+This module makes it possible to deploy the 'account_ebics'
+module on Odoo Enterprise.
+
+This module will be installed automatically when following modules are activated
+on your odoo database :
+
+- account_ebics
+- account_accountant
diff --git a/account_ebics_oe/__init__.py b/account_ebics_oe/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/account_ebics_oe/__manifest__.py b/account_ebics_oe/__manifest__.py
new file mode 100644
index 0000000..5e7f000
--- /dev/null
+++ b/account_ebics_oe/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2020-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "account_ebics on Odoo Enterprise",
+ "summary": "Deploy account_ebics module on Odoo Enterprise",
+ "version": "15.0.1.0.0",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Hidden",
+ "license": "LGPL-3",
+ "depends": [
+ "account_ebics",
+ "account_accountant",
+ ],
+ "data": ["views/account_ebics_menu.xml"],
+ "installable": True,
+ "auto_install": True,
+}
diff --git a/account_ebics_oe/static/description/icon.png b/account_ebics_oe/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics_oe/static/description/icon.png differ
diff --git a/account_ebics_oe/views/account_ebics_menu.xml b/account_ebics_oe/views/account_ebics_menu.xml
new file mode 100644
index 0000000..4f9f379
--- /dev/null
+++ b/account_ebics_oe/views/account_ebics_menu.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/account_ebics_payment_order/README.rst b/account_ebics_payment_order/README.rst
new file mode 100644
index 0000000..c6ecb6d
--- /dev/null
+++ b/account_ebics_payment_order/README.rst
@@ -0,0 +1,28 @@
+.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
+ :target: https://www.gnu.org/licenses/lpgl
+ :alt: License: AGPL-3
+
+==============================
+Upload Payment Order via EBICS
+==============================
+
+This module allows to upload a Payment Order to the bank via the EBICS protocol.
+
+Installation
+============
+
+This module depends upon the following modules (cf. apps.odoo.com):
+
+- account_ebics
+- account_payment_order
+
+Usage
+=====
+
+Create your Payment Order and generate the bank file.
+Upload the generated file via the 'EBICS Upload' button on the payment order.
+
+Known issues / Roadmap
+======================
+
+ * Add support for multiple EBICS connections.
diff --git a/account_ebics_payment_order/__init__.py b/account_ebics_payment_order/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/account_ebics_payment_order/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/account_ebics_payment_order/__manifest__.py b/account_ebics_payment_order/__manifest__.py
new file mode 100644
index 0000000..d79491a
--- /dev/null
+++ b/account_ebics_payment_order/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "Upload Payment Order via EBICS",
+ "version": "15.0.1.0.0",
+ "license": "LGPL-3",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Accounting & Finance",
+ "depends": ["account_ebics", "account_payment_order"],
+ "data": [
+ "views/account_payment_order_views.xml",
+ ],
+ "installable": True,
+}
diff --git a/account_ebics_payment_order/models/__init__.py b/account_ebics_payment_order/models/__init__.py
new file mode 100644
index 0000000..429f032
--- /dev/null
+++ b/account_ebics_payment_order/models/__init__.py
@@ -0,0 +1 @@
+from . import account_payment_order
diff --git a/account_ebics_payment_order/models/account_payment_order.py b/account_ebics_payment_order/models/account_payment_order.py
new file mode 100644
index 0000000..42045c2
--- /dev/null
+++ b/account_ebics_payment_order/models/account_payment_order.py
@@ -0,0 +1,74 @@
+# Copyright 2009-2022 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+from odoo import _, models
+from odoo.exceptions import UserError
+
+
+class AccountPaymentOrder(models.Model):
+ _inherit = "account.payment.order"
+
+ def ebics_upload(self):
+ self.ensure_one()
+ attach = self.env["ir.attachment"].search(
+ [("res_model", "=", self._name), ("res_id", "=", self.id)]
+ )
+ if not attach:
+ raise UserError(
+ _(
+ "This payment order doesn't contains attachements."
+ "\nPlease generate first the Payment Order file first."
+ )
+ )
+ elif len(attach) > 1:
+ raise UserError(
+ _(
+ "This payment order contains multiple attachments."
+ "\nPlease remove the obsolete attachments or upload "
+ "the payment order file via the "
+ "EBICS Processing > EBICS Upload menu"
+ )
+ )
+ else:
+ origin = _("Payment Order") + ": " + self.name
+ ebics_config = self.env["ebics.config"].search(
+ [
+ ("journal_ids", "=", self.journal_id.id),
+ ("state", "=", "confirm"),
+ ]
+ )
+ if not ebics_config:
+ raise UserError(
+ _(
+ "No active EBICS configuration available "
+ "for the selected bank."
+ )
+ )
+ ctx = self.env.context.copy()
+ if len(ebics_config) == 1:
+ ctx["default_ebics_config_id"] = ebics_config.id
+ ctx.update(
+ {
+ "default_upload_data": attach.datas,
+ "default_upload_fname": attach.name,
+ "origin": origin,
+ "force_comany": self.company_id.id,
+ }
+ )
+ ebics_xfer = self.env["ebics.xfer"].with_context(**ctx).create({})
+ ebics_xfer._onchange_ebics_config_id()
+ ebics_xfer._onchange_upload_data()
+ ebics_xfer._onchange_format_id()
+ view = self.env.ref("account_ebics.ebics_xfer_view_form_upload")
+ act = {
+ "name": _("EBICS Upload"),
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.xfer",
+ "view_id": view.id,
+ "res_id": ebics_xfer.id,
+ "type": "ir.actions.act_window",
+ "target": "new",
+ "context": ctx,
+ }
+ return act
diff --git a/account_ebics_payment_order/static/description/icon.png b/account_ebics_payment_order/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics_payment_order/static/description/icon.png differ
diff --git a/account_ebics_payment_order/views/account_payment_order_views.xml b/account_ebics_payment_order/views/account_payment_order_views.xml
new file mode 100644
index 0000000..bdadee4
--- /dev/null
+++ b/account_ebics_payment_order/views/account_payment_order_views.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ account.payment.order.form
+ account.payment.order
+
+
+
+
+
+
+
+
+
diff --git a/setup/.setuptools-odoo-make-default-ignore b/setup/.setuptools-odoo-make-default-ignore
new file mode 100644
index 0000000..207e615
--- /dev/null
+++ b/setup/.setuptools-odoo-make-default-ignore
@@ -0,0 +1,2 @@
+# addons listed in this file are ignored by
+# setuptools-odoo-make-default (one addon per line)
diff --git a/setup/README b/setup/README
new file mode 100644
index 0000000..a63d633
--- /dev/null
+++ b/setup/README
@@ -0,0 +1,2 @@
+To learn more about this directory, please visit
+https://pypi.python.org/pypi/setuptools-odoo
diff --git a/setup/account_ebics/odoo/addons/account_ebics b/setup/account_ebics/odoo/addons/account_ebics
new file mode 120000
index 0000000..16c1742
--- /dev/null
+++ b/setup/account_ebics/odoo/addons/account_ebics
@@ -0,0 +1 @@
+../../../../account_ebics
\ No newline at end of file
diff --git a/setup/account_ebics/setup.py b/setup/account_ebics/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/account_ebics/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/account_ebics_batch_payment/odoo/addons/account_ebics_batch_payment b/setup/account_ebics_batch_payment/odoo/addons/account_ebics_batch_payment
new file mode 120000
index 0000000..1e5ed4c
--- /dev/null
+++ b/setup/account_ebics_batch_payment/odoo/addons/account_ebics_batch_payment
@@ -0,0 +1 @@
+../../../../account_ebics_batch_payment
\ No newline at end of file
diff --git a/setup/account_ebics_batch_payment/setup.py b/setup/account_ebics_batch_payment/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/account_ebics_batch_payment/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/account_ebics_oe/odoo/addons/account_ebics_oe b/setup/account_ebics_oe/odoo/addons/account_ebics_oe
new file mode 120000
index 0000000..bcdee8e
--- /dev/null
+++ b/setup/account_ebics_oe/odoo/addons/account_ebics_oe
@@ -0,0 +1 @@
+../../../../account_ebics_oe
\ No newline at end of file
diff --git a/setup/account_ebics_oe/setup.py b/setup/account_ebics_oe/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/account_ebics_oe/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order b/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order
new file mode 120000
index 0000000..5ff2d5c
--- /dev/null
+++ b/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order
@@ -0,0 +1 @@
+../../../../account_ebics_payment_order
\ No newline at end of file
diff --git a/setup/account_ebics_payment_order/setup.py b/setup/account_ebics_payment_order/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/account_ebics_payment_order/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)