Recommended if you have multiple financial journals.
+This module adds a number of accounting menu entries such as bank statement list view
+which allows to see all statements downloaded via the ir.cron automated EBICS download.
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 to import externally generated keys & certificates (currently only 3SKey signature certificate)
+
For Odoo 16.0 the interaction with the OCA payment order and bank statement import modules (e.g. french CFONB) is not yet available.
+
+
+
+
+
+
diff --git a/account_ebics/views/ebics_config_views.xml b/account_ebics/views/ebics_config_views.xml
new file mode 100644
index 0000000..f7220d9
--- /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..11bb02e
--- /dev/null
+++ b/account_ebics/views/ebics_userid_views.xml
@@ -0,0 +1,160 @@
+
+
+
+
+ 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..77faf6b
--- /dev/null
+++ b/account_ebics/wizards/ebics_change_passphrase.py
@@ -0,0 +1,68 @@
+# Copyright 2009-2023 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..12eb03c
--- /dev/null
+++ b/account_ebics/wizards/ebics_xfer.py
@@ -0,0 +1,646 @@
+# Copyright 2009-2023 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 suffix and not fn.endswith(suffix):
+ fn = ".".join([fn, suffix])
+ ef_vals = {
+ "name": self.upload_fname,
+ "data": self.upload_data,
+ "date": fields.Datetime.now(),
+ "format_id": self.format_id.id,
+ "state": "done",
+ "user_id": self._uid,
+ "ebics_userid_id": self.ebics_userid_id.id,
+ "note": ef_note,
+ "company_ids": [
+ self.env.context.get("force_company", self.env.company.id)
+ ],
+ }
+ self._update_ef_vals(ef_vals)
+ ebics_file = self.env["ebics.file"].create(ef_vals)
+
+ except EbicsFunctionalError:
+ e = exc_info()
+ self.note += "\n"
+ self.note += _("EBICS Functional Error:")
+ self.note += "\n"
+ self.note += "{} (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 = base_fn
+ suffix = file_format.suffix
+ if suffix and not fn.endswith(suffix):
+ fn = ".".join([fn, suffix])
+ dups = self._check_duplicate_ebics_file(fn, file_format)
+ if dups:
+ raise UserError(
+ _(
+ "EBICS File with name '%s' has already been downloaded."
+ "\nPlease check this file and rename in case there is "
+ "no risk on duplicate transactions."
+ )
+ % fn
+ )
+ data = base64.encodebytes(data)
+ ef_vals = {
+ "name": fn,
+ "data": data,
+ "date": fields.Datetime.now(),
+ "date_from": self.date_from,
+ "date_to": self.date_to,
+ "format_id": file_format.id,
+ "user_id": self._uid,
+ "ebics_userid_id": self.ebics_userid_id.id,
+ "company_ids": self.ebics_config_id.company_ids.ids,
+ }
+ self._update_ef_vals(ef_vals)
+ ebics_file = self.env["ebics.file"].create(ef_vals)
+ return ebics_file
+
+ def _check_duplicate_ebics_file(self, fn, file_format):
+ dups = self.env["ebics.file"].search(
+ [("name", "=", fn), ("format_id", "=", file_format.id)]
+ )
+ return dups
+
+ def _detect_upload_format(self):
+ """
+ Use this method in order to automatically detect and set the
+ EBICS upload file format.
+ """
+
+ def _update_order_number(self, OrderID):
+ o_list = list(OrderID)
+ for i, c in enumerate(reversed(o_list), start=1):
+ if c == "9":
+ o_list[-i] = "A"
+ break
+ if c == "Z":
+ continue
+ else:
+ o_list[-i] = chr(ord(c) + 1)
+ break
+ next_nr = "".join(o_list)
+ if next_nr == "ZZZZ":
+ next_nr = "A000"
+ self.ebics_config_id.order_number = next_nr
+
+ def _insert_line_terminator(self, data_in, line_len):
+ data_in = data_in.replace(b"\n", b"").replace(b"\r", b"")
+ data_out = b""
+ max_len = len(data_in)
+ i = 0
+ while i + line_len <= max_len:
+ data_out += data_in[i : i + line_len] + b"\n"
+ i += line_len
+ return data_out
+
+ def _handle_cfonb120(self, data_in):
+ return self._insert_line_terminator(data_in, 120)
+
+ def _handle_cfonb240(self, data_in):
+ return self._insert_line_terminator(data_in, 240)
+
+ def _handle_camt052(self, data_in):
+ """
+ Use this method if you need to fix camt files received
+ from your bank before passing them to the
+ Odoo Community CAMT parser.
+ Remark: Odoo Enterprise doesn't support camt.052.
+ """
+ return data_in
+
+ def _handle_camt053(self, data_in):
+ """
+ Use this method if you need to fix camt files received
+ from your bank before passing them to the
+ Odoo Enterprise or Community CAMT parser.
+ """
+ return data_in
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/README.rst b/account_ebics_batch/README.rst
new file mode 100644
index 0000000..80f2d09
--- /dev/null
+++ b/account_ebics_batch/README.rst
@@ -0,0 +1,50 @@
+.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+ :target: https://www.gnu.org/licenses/agpl
+ :alt: License: AGPL-3
+
+============================================
+Module to enable batch import of EBICS files
+============================================
+
+This module adds a cron job for the automated import of EBICS files.
+
+|
+
+A Log is created during the import in order to document import errors.
+If errors have been detected, the Batch Import Log state is set to 'error'.
+
+When all EBICS Files have been imported correctly, the Batch Import Log state is set to 'done'.
+
+|
+
+The user can reprocess the imported EBICS files in status 'draft' via the Log object 'REPROCESS' button until all errors have been cleared.
+
+As an alternative, the user can force the Batch Import Log state to 'done'
+(e.g. when the errors have been circumvented via manual encoding or the reprocessing of a single EBICS file).
+
+|
+
+Configuration
+=============
+
+Adapt the 'EBICS Batch Import' ir.cron job created during the module installation.
+
+The cron job calls the following python method:
+
+|
+
+.. code-block:: python
+
+ _batch_import()
+
+
+The EBICS download will be performed on all confirmed EBICS connections.
+
+You can limit the automated operation to a subset of your EBICS connections via the ebics_config_ids parameter, e.g.
+
+|
+
+.. code-block:: python
+
+ _batch_import(ebics_config_ids=[1,3])
+
diff --git a/account_ebics_batch/__init__.py b/account_ebics_batch/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/account_ebics_batch/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/account_ebics_batch/__manifest__.py b/account_ebics_batch/__manifest__.py
new file mode 100644
index 0000000..4f71eab
--- /dev/null
+++ b/account_ebics_batch/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2009-2023 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "EBICS Files batch import",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Accounting & Finance",
+ "summary": "EBICS Files automated import and processing",
+ "depends": ["account_ebics"],
+ "data": [
+ "security/ir.model.access.csv",
+ "data/ir_cron_data.xml",
+ "views/ebics_batch_log_views.xml",
+ "views/menu.xml",
+ ],
+ "installable": True,
+}
diff --git a/account_ebics_batch/data/ir_cron_data.xml b/account_ebics_batch/data/ir_cron_data.xml
new file mode 100644
index 0000000..2726b75
--- /dev/null
+++ b/account_ebics_batch/data/ir_cron_data.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ EBICS Batch Import
+
+ code
+ model._batch_import()
+
+ 1
+ days
+ -1
+
+
+
+
+
diff --git a/account_ebics_batch/models/__init__.py b/account_ebics_batch/models/__init__.py
new file mode 100644
index 0000000..740bafa
--- /dev/null
+++ b/account_ebics_batch/models/__init__.py
@@ -0,0 +1 @@
+from . import ebics_batch_log
diff --git a/account_ebics_batch/models/ebics_batch_log.py b/account_ebics_batch/models/ebics_batch_log.py
new file mode 100644
index 0000000..74d8b25
--- /dev/null
+++ b/account_ebics_batch/models/ebics_batch_log.py
@@ -0,0 +1,190 @@
+# Copyright 2009-2023 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from sys import exc_info
+from traceback import format_exception
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+
+class EbicsBatchLog(models.Model):
+ _name = "ebics.batch.log"
+ _description = "Object to store EBICS Batch Import Logs"
+ _order = "create_date desc"
+
+ date_from = fields.Date()
+ date_to = fields.Date()
+ ebics_config_ids = fields.Many2many(
+ comodel_name="ebics.config", string="EBICS Configurations"
+ )
+ log_ids = fields.One2many(
+ comodel_name="ebics.batch.log.item",
+ inverse_name="log_id",
+ string="Batch Import Log Items",
+ readonly=True,
+ )
+ file_ids = fields.Many2many(
+ comodel_name="ebics.file",
+ string="Batch Import EBICS Files",
+ readonly=True,
+ )
+ file_count = fields.Integer(
+ string="EBICS Files Count", compute="_compute_ebics_files_fields", readonly=True
+ )
+ has_draft_files = fields.Boolean(compute="_compute_ebics_files_fields")
+ state = fields.Selection(
+ selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")],
+ required=True,
+ readonly=True,
+ default="draft",
+ )
+
+ @api.depends("file_ids")
+ def _compute_ebics_files_fields(self):
+ for rec in self:
+ rec.has_draft_files = "draft" in rec.file_ids.mapped("state")
+ rec.file_count = len(rec.file_ids)
+
+ def unlink(self):
+ for log in self:
+ if log.state != "draft":
+ raise UserError(_("Only log objects in state 'draft' can be deleted !"))
+ return super().unlink()
+
+ def button_draft(self):
+ self.state = "draft"
+
+ def button_done(self):
+ self.state = "done"
+
+ def reprocess(self):
+ import_dict = {"errors": []}
+ self._ebics_process(import_dict)
+ self._finalise_processing(import_dict)
+
+ def view_ebics_files(self):
+ action = self.env["ir.actions.actions"]._for_xml_id(
+ "account_ebics.ebics_file_action_download"
+ )
+ action["domain"] = [("id", "in", self.file_ids.ids)]
+ return action
+
+ def _batch_import(self, ebics_config_ids=None, date_from=None, date_to=None):
+ """
+ Call this method from a cron job to automate the EBICS import.
+ """
+ log_model = self.env["ebics.batch.log"]
+ import_dict = {"errors": []}
+ configs = self.env["ebics.config"].browse(ebics_config_ids) or self.env[
+ "ebics.config"
+ ].search(
+ [
+ ("company_ids", "in", self.env.user.company_ids.ids),
+ ("state", "=", "confirm"),
+ ]
+ )
+ log = log_model.create(
+ {
+ "ebics_config_ids": [(6, 0, configs.ids)],
+ "date_from": date_from,
+ "date_to": date_to,
+ }
+ )
+ ebics_file_ids = []
+ for config in configs:
+ err_msg = (
+ _("Error while processing EBICS connection '%s' :\n") % config.name
+ )
+ if config.state == "draft":
+ import_dict["errors"].append(
+ err_msg
+ + _(
+ "Please set state to 'Confirm' and "
+ "Reprocess this EBICS Import Log."
+ )
+ )
+ continue
+ try:
+ with self.env.cr.savepoint():
+ ebics_file_ids += self._ebics_import(
+ config, date_from, date_to, import_dict
+ )
+ except UserError as e:
+ import_dict["errors"].append(err_msg + " ".join(e.args))
+ except Exception:
+ tb = "".join(format_exception(*exc_info()))
+ import_dict["errors"].append(err_msg + tb)
+ log.file_ids = [(6, 0, ebics_file_ids)]
+ try:
+ with self.env.cr.savepoint():
+ log._ebics_process(import_dict)
+ except UserError as e:
+ import_dict["errors"].append(err_msg + " ".join(e.args))
+ except Exception:
+ tb = "".join(format_exception(*exc_info()))
+ import_dict["errors"].append(err_msg + tb)
+ log._finalise_processing(import_dict)
+
+ def _finalise_processing(self, import_dict):
+ log_item_model = self.env["ebics.batch.log.item"]
+ state = self.has_draft_files and "draft" or "done"
+ note = ""
+ error_count = 0
+ if import_dict["errors"]:
+ state = "error"
+ note = "\n\n".join(import_dict["errors"])
+ error_count = len(import_dict["errors"])
+ log_item_model.create(
+ {
+ "log_id": self.id,
+ "state": state,
+ "note": note,
+ "error_count": error_count,
+ }
+ )
+ self.state = state
+
+ def _ebics_import(self, config, date_from, date_to, import_dict):
+ xfer_wiz = (
+ self.env["ebics.xfer"]
+ .with_context(ebics_download=True)
+ .create(
+ {
+ "ebics_config_id": config.id,
+ "date_from": date_from,
+ "date_to": date_to,
+ }
+ )
+ )
+ xfer_wiz._onchange_ebics_config_id()
+ res = xfer_wiz.ebics_download()
+ file_ids = res["context"].get("ebics_file_ids")
+ if res["context"]["err_cnt"]:
+ import_dict["errors"].append(xfer_wiz.note)
+ return file_ids
+
+ def _ebics_process(self, import_dict):
+ to_process = self.file_ids.filtered(lambda r: r.state == "draft")
+ for ebics_file in to_process:
+ ebics_file.process()
+
+
+class EbicsBatchLogItem(models.Model):
+ _name = "ebics.batch.log.item"
+ _description = "Object to store EBICS Batch Import Log Items"
+ _order = "create_date desc"
+
+ log_id = fields.Many2one(
+ comodel_name="ebics.batch.log",
+ string="Batch Object",
+ ondelete="cascade",
+ readonly=True,
+ )
+ state = fields.Selection(
+ selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")],
+ required=True,
+ readonly=True,
+ )
+ note = fields.Text(string="Batch Import Log", readonly=True)
+ error_count = fields.Integer(string="Number of Errors", required=True, default=0)
diff --git a/account_ebics_batch/security/ir.model.access.csv b/account_ebics_batch/security/ir.model.access.csv
new file mode 100644
index 0000000..af76670
--- /dev/null
+++ b/account_ebics_batch/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_ebics_batch_log,ebics.batch.log,model_ebics_batch_log,account.group_account_invoice,1,1,1,1
+access_ebics_batch_log_item,ebics.batch.log.item,model_ebics_batch_log_item,account.group_account_invoice,1,1,1,1
diff --git a/account_ebics_batch/static/description/icon.png b/account_ebics_batch/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics_batch/static/description/icon.png differ
diff --git a/account_ebics_batch/views/ebics_batch_log_views.xml b/account_ebics_batch/views/ebics_batch_log_views.xml
new file mode 100644
index 0000000..8e3ae59
--- /dev/null
+++ b/account_ebics_batch/views/ebics_batch_log_views.xml
@@ -0,0 +1,118 @@
+
+
+
+
+ ebics.batch.log.search
+ ebics.batch.log
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.batch.log.tree
+ ebics.batch.log
+
+
+
+
+
+
+
+
+
+
+ ebics.batch.log.form
+ ebics.batch.log
+
+
+
+
+
+
+ EBICS Batch Import Logs
+ ir.actions.act_window
+ ebics.batch.log
+ tree,form
+
+
+
+
+
diff --git a/account_ebics_batch/views/menu.xml b/account_ebics_batch/views/menu.xml
new file mode 100644
index 0000000..21343b8
--- /dev/null
+++ b/account_ebics_batch/views/menu.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
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..f1dca19
--- /dev/null
+++ b/account_ebics_batch_payment/__manifest__.py
@@ -0,0 +1,14 @@
+# Copyright 2009-2023 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "Upload Batch Payment via EBICS",
+ "version": "16.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..c313252
--- /dev/null
+++ b/account_ebics_batch_payment/models/account_batch_payment.py
@@ -0,0 +1,53 @@
+# Copyright 2009-2023 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_oca_statement_import/README.rst b/account_ebics_oca_statement_import/README.rst
new file mode 100644
index 0000000..4fe75a6
--- /dev/null
+++ b/account_ebics_oca_statement_import/README.rst
@@ -0,0 +1,17 @@
+.. 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 with OCA Bank Statement Import
+==========================================================
+
+This module makes it possible to use OCA account_statement_import
+in combination with 'account_ebics'.
+
+This module will be installed automatically when following modules are activated
+on your odoo database :
+
+- account_ebics
+- account_statement_import
+
diff --git a/account_ebics_oca_statement_import/__init__.py b/account_ebics_oca_statement_import/__init__.py
new file mode 100644
index 0000000..5cb1c49
--- /dev/null
+++ b/account_ebics_oca_statement_import/__init__.py
@@ -0,0 +1 @@
+from . import wizards
diff --git a/account_ebics_oca_statement_import/__manifest__.py b/account_ebics_oca_statement_import/__manifest__.py
new file mode 100644
index 0000000..3c4cdc2
--- /dev/null
+++ b/account_ebics_oca_statement_import/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2020-2023 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "account_ebics with OCA Bank Statement Imoort",
+ "summary": "Use OCA Bank Statement Import with account_ebics",
+ "version": "16.0.1.0.0",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Hidden",
+ "license": "LGPL-3",
+ "depends": [
+ "account_ebics",
+ "account_statement_import",
+ ],
+ # installable False unit OCA statement import becomes
+ # available for 16.0
+ "installable": False,
+ "auto_install": True,
+}
diff --git a/account_ebics_oca_statement_import/static/description/icon.png b/account_ebics_oca_statement_import/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics_oca_statement_import/static/description/icon.png differ
diff --git a/account_ebics_oca_statement_import/wizards/__init__.py b/account_ebics_oca_statement_import/wizards/__init__.py
new file mode 100644
index 0000000..ae69bca
--- /dev/null
+++ b/account_ebics_oca_statement_import/wizards/__init__.py
@@ -0,0 +1 @@
+from . import account_statement_import
diff --git a/account_ebics_oca_statement_import/wizards/account_statement_import.py b/account_ebics_oca_statement_import/wizards/account_statement_import.py
new file mode 100644
index 0000000..86b798d
--- /dev/null
+++ b/account_ebics_oca_statement_import/wizards/account_statement_import.py
@@ -0,0 +1,60 @@
+# Copyright 2009-2020 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+import logging
+
+from odoo import _, models
+
+_logger = logging.getLogger(__name__)
+
+
+class AccountStatementImport(models.TransientModel):
+ _inherit = "account.statement.import"
+
+ def _check_parsed_data(self, stmts_vals):
+ """Basic and structural verifications"""
+ if self.env.context.get("active_model") == "ebics.file":
+ message = False
+ if len(stmts_vals) == 0:
+ message = _("This file doesn't contain any statement.")
+ if not message:
+ no_st_line = True
+ for vals in stmts_vals:
+ if vals["transactions"] and len(vals["transactions"]) > 0:
+ no_st_line = False
+ break
+ if no_st_line:
+ message = _("This file doesn't contain any transaction.")
+ if message:
+ log_msg = (
+ _("Error detected while processing and EBICS File")
+ + ":\n"
+ + message
+ )
+ _logger.warn(log_msg)
+ return
+ return super()._check_parsed_data(stmts_vals)
+
+ def _create_bank_statements(self, stmts_vals, result):
+ """
+ Return error message to ebics.file when handling empty camt.
+
+ Remarks/TODO:
+ We could add more info to the message (e.g. date, balance, ...)
+ and write this to the ebics.file, note field.
+ We could also create empty bank statement (in state done) to clearly
+ show days without transactions via the bank statement list view.
+ """
+ if self.env.context.get("active_model") == "ebics.file":
+ transactions = False
+ for st_vals in stmts_vals:
+ if st_vals.get("transactions"):
+ transactions = True
+ break
+ if not transactions:
+ message = _("This file doesn't contain any transaction.")
+ st_line_ids = []
+ notifications = {"type": "warning", "message": message, "details": ""}
+ return st_line_ids, [notifications]
+
+ return super()._create_bank_statements(stmts_vals, result)
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..b442879
--- /dev/null
+++ b/account_ebics_oe/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2020-2023 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": "16.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..b5be3f9
--- /dev/null
+++ b/account_ebics_payment_order/__manifest__.py
@@ -0,0 +1,18 @@
+# Copyright 2009-2023 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
+
+{
+ "name": "Upload Payment Order via EBICS",
+ "version": "16.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 False unit OCA payment order becomes
+ # available for 16.0
+ "installable": False,
+}
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..ecd9c78
--- /dev/null
+++ b/account_ebics_payment_order/models/account_payment_order.py
@@ -0,0 +1,74 @@
+# Copyright 2009-2023 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/odoo/addons/account_ebics_batch b/setup/account_ebics_batch/odoo/addons/account_ebics_batch
new file mode 120000
index 0000000..d1e7e96
--- /dev/null
+++ b/setup/account_ebics_batch/odoo/addons/account_ebics_batch
@@ -0,0 +1 @@
+../../../../account_ebics_batch
\ No newline at end of file
diff --git a/setup/account_ebics_batch/setup.py b/setup/account_ebics_batch/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/account_ebics_batch/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,
+)