diff --git a/account_ebics/README.rst b/account_ebics/README.rst
new file mode 100644
index 0000000..babce1a
--- /dev/null
+++ b/account_ebics/README.rst
@@ -0,0 +1,219 @@
+.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
+ :target: https://www.gnu.org/licenses/lgpl
+ :alt: License: LGPL-3
+
+======================
+EBICS banking protocol
+======================
+
+Implementation of the EBICS banking protocol.
+
+This module facilitates the exchange of files with banks via the EBICS protocol.
+
+|
+
+Installation
+============
+
+The module depends upon
+
+- https://pypi.python.org/pypi/fintech
+- https://pypi.python.org/pypi/cryptography
+
+Remark:
+
+The EBICS 'Test Mode' for uploading orders requires fintech 4.3.4 or higher for EBICS 2.x
+and fintech 7.2.7 or higher for EBICS 3.0.
+
+SWIFT 3SKey support requires fintech 6.4 or higher.
+
+|
+
+We also recommend to consider the installation of the following modules:
+
+|
+
+- account_ebics_oe
+
+ Required if you are running Odoo Enterprise
+
+ Cf. https://github.com/Noviat/account_ebics
+
+|
+
+- account_ebics_batch
+
+ This module adds a cron job for the automated import of EBICS files.
+
+ Cf. https://github.com/Noviat/account_ebics
+
+|
+
+- account_ebics_batch_payment
+
+ Recommended if you are using the Odoo Enterprise account_batch_payment module
+
+ Cf. https://github.com/Noviat/account_ebics
+
+|
+
+- account_usability
+
+ 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.
+
+ Cf. https://github.com/OCA/account-financial-tools
+
+|
+
+- account_ebics_payment_order
+
+ Required if you are using the OCA account_payment_order module.
+
+ Cf. https://github.com/OCA/bank-payment
+
+|
+
+- account_ebics_oca_statement_import
+
+ Required if you are using the OCA Bank Statement import modules.
+
+ https://github.com/OCA/bank-statement-import
+
+|
+
+- account_statement_import_fr_cfonb
+
+ Required to handle french CFONB files.
+
+ Cf. https://github.com/OCA/l10n_france
+
+|
+
+- account_statement_import_camt
+
+ Required to handle camt.052 and camt.054 files.
+
+ Cf. https://github.com/OCA/bank-statement-import
+
+|
+
+
+Fintech license
+---------------
+
+If you have a valid Fintech.ebics license, you should add the following
+licensing parameters to the odoo server configuration file:
+
+
+- fintech_register_name
+
+The name of the licensee.
+
+- fintech_register_keycode
+
+The keycode of the licensed version.
+
+|
+| Example:
+|
+
+::
+
+ ; fintech
+ fintech_register_name = MyCompany
+ fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
+
+|
+
+Cf. https://www.joonis.de/en/fintech/prices/
+
+|
+
+Configuration
+=============
+
+Go to **Settings > Users**
+
+Add the users that are authorised to maintain the EBICS configuration to the 'EBICS Manager' Group.
+
+|
+
+Go to **Accounting > Configuration > Miscellaneous > EBICS > EBICS File Formats**
+
+Check if the EBICS File formats that you want to process in Odoo are defined.
+
+Most commonly used formats for which support is available in Odoo should be there already.
+
+Please open an issue on https://github.com/Noviat/account_ebics to report missing EBICS File Formats.
+
+For File Formats of type 'Downloads' you can also specify a 'Download Process Method'.
+
+This is the method that will be executed when hitting the 'Process' button on the downloaded file.
+
+The following methods are currently available:
+
+- cfonb120
+- camt.053
+- camt.052
+- camt.054
+
+All these methods require complimentary modules to be installed (cf. Installation section supra).
+
+You'll get an error message when the required module is not installed on your Odoo instance.
+
+|
+
+Go to **Accounting > Configuration > Miscellaneous > EBICS > EBICS Configuration**
+
+Configure your EBICS configuration according to the contract with your bank.
+
+|
+
+Usage
+=====
+
+Go to **Accounting > Bank and Cash > EBICS Processing**
+
+|
+
+Diagnostics
+===========
+
+Add the following to your Odoo config file in order to diagnose
+issues with the EBICS connection with your bank:
+
+log_handler = fintech.ebics:DEBUG
+
+|
+
+EBICS Return Codes
+------------------
+
+During the processing of your EBICS upload/download, your bank may return an Error Code, e.g.
+
+EBICS Functional Error:
+EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 90005)
+
+A detailed 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).
+
+|
+
+Electronic Distributed Signature (EDS)
+--------------------------------------
+
+This is supported via external signing apps, e.g. BankingVEU:
+
+- https://play.google.com/store/apps/details?id=subsembly.bankingveu
+- https://apps.apple.com/de/app/bankingveu/id1578694190
+
+
+Known Issues / Roadmap
+======================
+
+- The end user is currently not able to change his passphrases (only the users with 'EBICS Manager' rights can do so).
+- Add support to import externally generated keys & certificates (currently only 3SKey signature certificate).
+- Add support for SWIFT 3SKey signing javascript lib (SConnect, cf https://www2.swift.com/3skey/help/sconnect.html).
+
diff --git a/account_ebics/__init__.py b/account_ebics/__init__.py
new file mode 100644
index 0000000..168985a
--- /dev/null
+++ b/account_ebics/__init__.py
@@ -0,0 +1,9 @@
+import logging
+
+_logger = logging.getLogger(__name__)
+
+try:
+ from . import models
+ from . import wizards
+except Exception:
+ _logger.warning("Import Error, check if fintech lib has been installed")
diff --git a/account_ebics/__manifest__.py b/account_ebics/__manifest__.py
new file mode 100644
index 0000000..ca96480
--- /dev/null
+++ b/account_ebics/__manifest__.py
@@ -0,0 +1,34 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "EBICS banking protocol",
+ "version": "18.0.1.0.0",
+ "license": "LGPL-3",
+ "author": "Noviat",
+ "website": "https://www.noviat.com",
+ "category": "Accounting & Finance",
+ "depends": ["account"],
+ "data": [
+ "security/ebics_security.xml",
+ "security/ir.model.access.csv",
+ "data/ebics_file_format.xml",
+ "views/ebics_config_views.xml",
+ "views/ebics_file_views.xml",
+ "views/ebics_userid_views.xml",
+ "views/ebics_file_format_views.xml",
+ "wizards/ebics_change_passphrase.xml",
+ "wizards/ebics_xfer.xml",
+ "wizards/ebics_admin_order.xml",
+ "views/menu.xml",
+ ],
+ "installable": True,
+ "application": True,
+ "external_dependencies": {
+ "python": [
+ "fintech",
+ "cryptography",
+ ]
+ },
+ "images": ["static/description/cover.png"],
+}
diff --git a/account_ebics/data/ebics_file_format.xml b/account_ebics/data/ebics_file_format.xml
new file mode 100644
index 0000000..05bad93
--- /dev/null
+++ b/account_ebics/data/ebics_file_format.xml
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+ 2
+ camt.052
+ down
+ C52
+ camt.052
+ bank to customer account report in format camt.052
+ c52.xml
+
+
+
+ 2
+ camt.052
+ down
+ Z52
+ camt.052
+ bank to customer account report in format camt.052
+ c52.xml
+
+
+
+ 2
+ camt.053
+ down
+ C53
+ camt.053
+ Bank to customer statement report in format camt.053
+ c53.xml
+
+
+
+ 2
+ camt.053
+ down
+ Z53
+ camt.053
+ Bank to customer statement report in format camt.053
+ c53.xml
+
+
+
+ 2
+ camt.054
+ down
+ C54
+ camt.054
+ Bank to customer debit credit notification in format camt.054
+ c52.xml
+
+
+
+ 2
+ camt.054
+ down
+ Z54
+ camt.054
+ Bank to customer debit credit notification in format camt.054
+ c52.xml
+
+
+
+ 2
+ camt.xxx.cfonb120.stm
+ down
+ FDL
+ cfonb120
+ Bank to customer statement report in format cfonb120
+ cfonb120.dat
+
+
+
+ 2
+ pain.002
+ down
+ CDZ
+ Payment status report for direct debit in format pain.002
+ psr.xml
+
+
+
+ 2
+ pain.002
+ down
+ Z01
+ pain.002
+ Payment status report for direct debit in format pain.002
+ psr.xml
+
+
+
+ 3
+ down
+ BTD
+ cfonb120
+ Bank to customer statement report in format cfonb120
+ cfonb120.dat
+ EOP
+ cfonb120
+
+
+
+
+
+ 2
+ pain.xxx.cfonb160.dco
+ up
+ FUL
+ Remises de LCR
+ txt
+
+
+
+ 2
+ pain.001.001.03
+ up
+ CCT
+ Payment Order in format pain.001.001.03
+ xml
+
+
+
+ 2
+ pain.001.001.03
+ up
+ XE2
+ Payment Order in format pain.001.001.03
+ xml
+
+
+
+ 2
+ pain.008.001.02.sdd
+ up
+ CDD
+ Sepa Core Direct Debit Order in format pain.008.001.02
+ xml
+
+
+
+ 2
+ pain.008.001.02.sdd
+ up
+ XE3
+ Sepa Core Direct Debit Order in format pain.008.001.02
+ xml
+
+
+
+ 2
+ pain.008.001.02.sbb
+ up
+ CDB
+ Sepa Direct Debit (B2B) Order in format pain.008.001.02
+ xml
+
+
+
+ 2
+ pain.008.001.02.sbb
+ up
+ XE4
+ Sepa Direct Debit (B2B) Order in format pain.008.001.02
+ xml
+
+
+
+ 2
+ pain.001.001.02.sct
+ up
+ FUL
+ Payment Order in format pain.001.001.02
+ xml
+
+
+
+ 3
+ up
+ BTU
+ SEPA credit transfer
+ txt
+ SCT
+ pain.001
+ GLB
+
+
+
diff --git a/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf b/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf
new file mode 100644
index 0000000..472c58f
Binary files /dev/null and b/account_ebics/doc/2017-03-29-EBICS_V_3.0-FinalVersion.pdf differ
diff --git a/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf b/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf
new file mode 100644
index 0000000..1df4109
Binary files /dev/null and b/account_ebics/doc/2017-03-29-EBICS_V_3.0_Annex1_ReturnCodes-FinalVersion.pdf differ
diff --git a/account_ebics/doc/EBICS_Annex1_ReturnCodes_final-16-05-2011.pdf b/account_ebics/doc/EBICS_Annex1_ReturnCodes_final-16-05-2011.pdf
new file mode 100644
index 0000000..2e54c4e
Binary files /dev/null and b/account_ebics/doc/EBICS_Annex1_ReturnCodes_final-16-05-2011.pdf differ
diff --git a/account_ebics/doc/EBICS_Annex2_OrderTypes-File_Formats-15-04-2016.pdf b/account_ebics/doc/EBICS_Annex2_OrderTypes-File_Formats-15-04-2016.pdf
new file mode 100644
index 0000000..ffcc3ee
Binary files /dev/null and b/account_ebics/doc/EBICS_Annex2_OrderTypes-File_Formats-15-04-2016.pdf differ
diff --git a/account_ebics/doc/EBICS_Common_IG_based_EBICS_2.5.pdf b/account_ebics/doc/EBICS_Common_IG_based_EBICS_2.5.pdf
new file mode 100644
index 0000000..53f3017
Binary files /dev/null and b/account_ebics/doc/EBICS_Common_IG_based_EBICS_2.5.pdf differ
diff --git a/account_ebics/doc/EBICS_Specification_2.5_final-16-05-2011.pdf b/account_ebics/doc/EBICS_Specification_2.5_final-16-05-2011.pdf
new file mode 100644
index 0000000..cada02a
Binary files /dev/null and b/account_ebics/doc/EBICS_Specification_2.5_final-16-05-2011.pdf differ
diff --git a/account_ebics/i18n/account_ebics.pot b/account_ebics/i18n/account_ebics.pot
new file mode 100644
index 0000000..c6b9db0
--- /dev/null
+++ b/account_ebics/i18n/account_ebics.pot
@@ -0,0 +1,1789 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * account_ebics
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 17.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid " has"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "%(st_cnt)s bank statement%(sp)s been imported: "
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file__date_from
+msgid "'Date From' as entered in the download wizard."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file__date_to
+msgid "'Date To' as entered in the download wizard."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__swift_3skey_certificate
+msgid "3SKey Certficate"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__swift_3skey_certificate_fn
+msgid "3SKey Certificate Filename"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "3SKey certificate missing."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid ""
+"Distinguished Name attributes used to create self-signed X.509 "
+"certificates: "
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__ebics_key_version__a005
+msgid "A005 (RSASSA-PKCS1-v1_5)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__ebics_key_version__a006
+msgid "A006 (RSASSA-PSS)"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Account activated"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__active
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__active
+msgid "Active"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__state__active_keys
+msgid "Active Keys"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_upload
+msgid "Additional Information"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__allowed_format_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__allowed_format_ids
+msgid "Allowed EBICS File Formats"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__transaction_rights
+msgid "Allowed Transactions"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_container
+msgid "BTF Container"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_format
+msgid "BTF Format"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_message
+msgid "BTF Message Name"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_message
+msgid ""
+"BTF Message Name\n"
+"The message name consisting of up to 10 alphanumeric characters [a-z0-9.] (eg. pain.001, pain.008, camt.053)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_option
+msgid "BTF Option"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_scope
+msgid "BTF Scope"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_service
+msgid "BTF Service"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_service
+msgid ""
+"BTF Service Name)\n"
+"The service code name consisting of 3 alphanumeric characters [A-Z0-9] (e.g. SCT, SDD, STM, EOP)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_variant
+msgid "BTF Variant"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__btf_version
+msgid "BTF Version"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__journal_ids
+msgid "Bank Accounts"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Bank Keys Verified"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model,name:account_ebics.model_account_bank_statement
+msgid "Bank Statement"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+msgid "Bank Statements"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "Cancel"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model,name:account_ebics.model_ebics_change_passphrase
+msgid "Change EBICS keys passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Change Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_admin_order_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_result
+msgid "Close"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_cn
+msgid "Common Name [CN]"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__company_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__company_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__company_ids
+msgid "Companies"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__company_ids
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__company_ids
+msgid "Companies sharing this EBICS contract."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file__company_ids
+msgid "Companies sharing this EBICS file."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid "Confirm"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__state__confirm
+msgid "Confirmed"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_host
+msgid ""
+"Contact your bank to get the EBICS HostID.\n"
+"In France the BIC is usually allocated to the HostID whereas in Germany it tends to be an institute specific string of 8 characters."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_url
+msgid "Contact your bank to get the EBICS URL."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_c
+msgid "Country Name [C]"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__create_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__create_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Currency %(cc)s not found."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__date
+msgid "Date"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__date_from
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__date_from
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__date_from
+msgid "Date From"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__date_to
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__date_to
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__date_to
+msgid "Date To"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__signature_class
+msgid ""
+"Default signature class.This default can be overriden for specific EBICS "
+"transactions (cf. File Formats)."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__description
+msgid "Description"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__display_name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file__state__done
+msgid "Done"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "Doublecheck your EBICS Passphrase and UserID settings."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file_format__type__down
+#: model:ir.ui.menu,name:account_ebics.ebics_file_menu_download
+msgid "Download"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_tree_download
+msgid "Download Date"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_download
+msgid "Download Files"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__transaction_rights__down
+msgid "Download Only"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__download_process_method
+msgid "Download Process Method"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__transaction_rights__both
+msgid "Download and Upload"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__state__draft
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file__state__draft
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__state__draft
+msgid "Draft"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__upload_fname_dummy
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__upload_fname_dummy
+msgid "Dummy Upload Filename"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__name
+msgid ""
+"E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
+"Specify camt.052, camt.053, camt.054 for camt Order Types such as C53, Z53, C54, Z54.\n"
+"This name has to match the 'Request Type' in your EBICS contract for Order Type 'FDL' or 'FUL'.\n"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_menu
+msgid "EBICS"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_admin_order__order_type
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__order_type
+#: model:ir.model.fields,help:account_ebics.field_ebics_xfer__order_type
+msgid ""
+"EBICS 3.0: BTD (download) or BTU (upload).\n"
+"EBICS 2.0: E.g. C53 (check your EBICS contract). For most banks in France you should use the format neutral Order Types 'FUL' for upload and 'FDL' for download."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_admin_order_action
+#: model:ir.model,name:account_ebics.model_ebics_admin_order
+msgid "EBICS Administrative Order"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_admin_order.py:0
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_admin_order_view_form_result
+#, python-format
+msgid "EBICS Administrative Order result"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_admin_order_menu
+msgid "EBICS Administrative Orders"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_config_action
+#: model:ir.model,name:account_ebics.model_ebics_config
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_config_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_config_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_config_id
+#: model:ir.ui.menu,name:account_ebics.ebics_config_menu
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid "EBICS Configuration"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_account_bank_statement__ebics_file_id
+msgid "EBICS Data File"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_xfer_menu_download
+msgid "EBICS Download"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_file_action_download
+msgid "EBICS Download Files"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_upload
+msgid "EBICS File"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS File '%s' is available for further processing."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_download
+msgid "EBICS File Download"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__format_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_file_format_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__format_id
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_format_view_form
+msgid "EBICS File Format"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_file_format_action
+#: model:ir.model,name:account_ebics.model_ebics_file_format
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__format_id
+#: model:ir.ui.menu,name:account_ebics.ebics_file_format_menu
+msgid "EBICS File Formats"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_result
+msgid "EBICS File Transfer"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_xfer_action_download
+msgid "EBICS File Transfer - Download"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_xfer_action_upload
+msgid "EBICS File Transfer - Upload"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "EBICS File Upload"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS File has been uploaded (OrderID %s)."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"EBICS File with name '%s' has already been downloaded.\n"
+"Please check this file and rename in case there is no risk on duplicate transactions."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_file_menu
+msgid "EBICS Files"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"EBICS Functional Error during download of File Format %(name)s "
+"(%(order_type)s):"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS Functional Error:"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_host
+msgid "EBICS HostID"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_ini_letter
+msgid "EBICS INI Letter"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "EBICS Initialisation"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid ""
+"EBICS Initialisation - After the account has been activated the public bank "
+"keys must be downloaded and checked for consistency."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid ""
+"EBICS Initialisation - Push this button when the account has been activated "
+"by the bank."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid ""
+"EBICS Initialisation - Push this button when the public have been checked "
+"for consistency."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "EBICS Initialisation Error:"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "EBICS Initialization Error:"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form_result
+#, python-format
+msgid "EBICS Keys Change Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_keys
+msgid "EBICS Keys Root"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_config.py:0
+#, python-format
+msgid ""
+"EBICS Keys Root Directory %s is not available.\n"
+"Please contact your system administrator."
+msgstr ""
+
+#. module: account_ebics
+#: model:res.groups,name:account_ebics.group_ebics_manager
+msgid "EBICS Manager"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS OrderID: %s"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_partner
+msgid "EBICS PartnerID"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_passphrase
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_passphrase
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_passphrase
+msgid "EBICS Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_processing_menu
+msgid "EBICS Processing"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_public_bank_keys
+msgid "EBICS Public Bank Keys"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_public_bank_keys_fn
+msgid "EBICS Public Bank Keys Filename"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__ebics_public_bank_keys
+msgid "EBICS Public Bank Keys to be checked for consistency."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_sig_passphrase
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_sig_passphrase
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_sig_passphrase
+msgid "EBICS Signature Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_passphrase_stored
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_passphrase_stored
+msgid "EBICS Stored Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"EBICS Technical Error during download of File Format %(name)s "
+"(%(order_type)s):"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS Technical Error:"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_url
+msgid "EBICS URL"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.ui.menu,name:account_ebics.ebics_xfer_menu_upload
+msgid "EBICS Upload"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.actions.act_window,name:account_ebics.ebics_file_action_upload
+msgid "EBICS Upload Files"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model,name:account_ebics.model_ebics_userid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_userid_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__ebics_userid_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_userid_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__ebics_userid_id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__name
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_userid_id
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "EBICS UserID"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid "EBICS Users"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"EBICS Verification Error during download of File Format %(name)s "
+"(%(order_type)s):"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS Verification Error:"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_admin_order.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS client setup failed for connection '%s'"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model,name:account_ebics.model_ebics_xfer
+msgid "EBICS file transfer"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__note
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__note
+msgid "EBICS file transfer Log"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "EBICS file transfer result"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_key_bitlength
+msgid "EBICS key bitlength"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_config.py:0
+#, python-format
+msgid "EBICS key bitlength must be >= 2048."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_key_version
+msgid "EBICS key version"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "EBICS keys change passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__ebics_version
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__ebics_version
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_version
+msgid "EBICS protocol version"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "EBICS version mismatch."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_keys_fn
+msgid "Ebics Keys Fn"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_keys_found
+msgid "Ebics Keys Found"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_passphrase_invisible
+msgid "Ebics Passphrase Invisible"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_passphrase_required
+msgid "Ebics Passphrase Required"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_passphrase_store_readonly
+msgid "Ebics Passphrase Store Readonly"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_sig_passphrase_invisible
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__ebics_sig_passphrase_invisible
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_sig_passphrase_invisible
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_sig_passphrase_invisible
+msgid "Ebics Sig Passphrase Invisible"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_e
+msgid "Email Address"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__swift_3skey
+msgid "Enable 3SKey support"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__download_process_method
+msgid ""
+"Enable processing within Odoo of the downloaded file via the 'Process' "
+"button.E.g. specify camt.053 to import a camt.053 file and create a bank "
+"statement."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"Error detected during download of File Format %(name)s (%(order_type)s):"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "Error while accessing the EBICS Keys:"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "Error while accessing the EBICS UserID:"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Errors"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_admin_order_view_form
+msgid "Execute"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__data
+msgid "File"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_search
+msgid "File Format"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid "File Formats"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file__date
+msgid "File Upload/Download date"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__upload_data
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__upload_data
+msgid "File to Upload"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__name
+msgid "Filename"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Force Active Keys"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__bank_statement_ids
+msgid "Generated Bank Statements"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Get Bank Keys"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__state__get_bank_keys
+msgid "Get Keys from Bank"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_search
+msgid "Group By"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__ebics_version__h003
+msgid "H003 (2.4)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__ebics_version__h004
+msgid "H004 (2.5)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_config__ebics_version__h005
+msgid "H005 (3.0)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_userid_ids
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__name
+msgid ""
+"Human users or a technical system that is/are assigned to a customer. \n"
+"On the EBICS bank server it is identified by the combination of UserID and PartnerID. The technical subscriber serves only for the data exchange between customer and financial institution. The human user also can authorise orders."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__id
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__id
+msgid "ID"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_ini_letter_fn
+msgid "INI-letter Filename"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__ebics_ini_letter
+msgid "INI-letter PDF document to be sent to your bank."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Import EBICS File"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_account_bank_statement__import_format
+msgid "Import Format"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid ""
+"Incorrect CFONB120 file:\n"
+"the file is not divisible in 120 char lines"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#, python-format
+msgid "Incorrect old passphrase."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__state__init
+msgid "Initialisation"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Initialise EBICS Bank Keys"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Invalid XML file."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__write_uid
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__write_date
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_l
+msgid "Locality Name [L]"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_format
+msgid ""
+"Message format consisting of 1-4 alphanumeric characters [A-Z0-9] (eg. XML, "
+"JSON, PDF)."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_variant
+msgid "Message variant consisting of 3 numeric characters [0-9] (eg. 001)."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_version
+msgid "Message version consisting of 2 numeric characters [0-9] (eg. 03)."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__name
+msgid "Name"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__new_pass
+msgid "New Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__new_pass_check
+msgid "New Passphrase (verification)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__new_sig_pass
+msgid "New Signature Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__new_sig_pass_check
+msgid "New Signature Passphrase (verification)"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#, python-format
+msgid "New passphrase equal to old passphrase."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#, python-format
+msgid "New passphrase verification error."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#, python-format
+msgid "New signature passphrase equal to old signature passphrase."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_change_passphrase.py:0
+#, python-format
+msgid "New signature passphrase verification error."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "No bank account number found."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "No bank defined for the financial journal of the EBICS Config"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "No financial journal found for Account Number %(nbr)s, Currency %(cc)s"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__note
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__note
+msgid "Notes"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Number of errors: %(nr)s"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Number of warnings: %(nr)s"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model,name:account_ebics.model_ebics_file
+msgid "Object to store EBICS Data Files"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__old_pass
+msgid "Old Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_change_passphrase__old_sig_pass
+msgid "Old Signature Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__admin_order_type
+msgid "Order"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__order_number
+msgid "Order Number"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_config.py:0
+#, python-format
+msgid ""
+"Order Number should comply with the following pattern:\n"
+"[A-Z]{1}[A-Z0-9]{3}"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__order_type
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__order_type
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__order_type
+msgid "Order Type"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_o
+msgid "Organization Name [O]"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_ou
+msgid "Organizational Unit Name [OU]"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_partner
+msgid ""
+"Organizational unit (company or individual) that concludes a contract with the bank. \n"
+"In this contract it will be agreed which order types (file formats) are used, which accounts are concerned, which of the customer's users (subscribers) communicate with the EBICS bank server and the authorisations that these users will possess. \n"
+"It is identified by the PartnerID."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "Origin: %s"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Passphrase mismatch."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__signature_class
+msgid ""
+"Please doublecheck the security of your Odoo ERP system when using class 'E' to prevent unauthorised users to make supplier payments.\n"
+"Leave this field empty to use the default defined for your EBICS UserID."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+msgid "Process"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_result
+msgid "Process EBICS File"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__note_process
+msgid "Process Notes"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Process file %(fn)s results:"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+msgid "Process the EBICS File"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Renew Bank Keys"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__name
+msgid "Request Type"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_admin_order_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_change_passphrase_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_result
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_result
+msgid "Results :"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_keys
+msgid "Root Directory for storing the EBICS Keys."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_scope
+msgid ""
+"Scope of service.\n"
+"Either an ISO-3166 ALPHA 2 country code or an issuer code of 3 alphanumeric characters [A-Z0-9]."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_search
+msgid "Search EBICS Files"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_admin_order__format_id
+#: model:ir.model.fields,help:account_ebics.field_ebics_xfer__format_id
+msgid ""
+"Select EBICS File Format to upload/download.\n"
+"Leave blank to download all available files."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_admin_order__test_mode
+#: model:ir.model.fields,help:account_ebics.field_ebics_xfer__test_mode
+msgid ""
+"Select this option to test if the syntax of the upload file is correct.\n"
+"This option is only available for Order Type 'FUL'."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "Select your bank :"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "Select your file :"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Set a passphrase."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Set state to 'Get Keys from Bank'."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Set state to 'Initialisation'."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Set state to 'Verification'."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Set state to 'draft' before Bank Key (re)initialisation."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid ""
+"Set the EBICS Configuation record to 'Draft' before starting the Key Renewal"
+" process."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__ebics_key_x509
+msgid "Set this flag in order to work with self-signed X.509 certificates"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_upload
+msgid "Set to Done"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_download
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_upload
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Set to Draft"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid "Set to Draft in order to change the EBICS configuration parameters."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Set to Draft in order to reinitialize your bank connection."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__signature_class
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__signature_class
+msgid "Signature Class"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file_format__signature_class__e
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__signature_class__e
+msgid "Single signature"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__suffix
+msgid ""
+"Specify the filename suffix for this File Format.\n"
+"E.g. c53.xml"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__order_number
+msgid ""
+"Specify the number for the next order.\n"
+"This number should match the following pattern : [A-Z]{1}[A-Z0-9]{3}"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_config__state
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__state
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__state
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_search
+msgid "State"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509_dn_st
+msgid "State Or Province Name [ST]"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Statement %(st)s dated %(date)s (Company: %(cpy)s)"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Statement %(st_name)s dated %(date)s has already been imported."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Statement for Account Number %(nr)s has not been processed."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__ebics_passphrase_store
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_passphrase_store
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__ebics_passphrase_store
+msgid "Store EBICS Passphrase"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__suffix
+msgid "Suffix"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__test_mode
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__test_mode
+msgid "Test Mode"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_config_view_form
+msgid ""
+"The EBICS configuration must be confirmed before it can used for bank "
+"transactions."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "The EBICS response could not be verified."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "The Passphrase must be at least 8 characters long"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "The Signature Passphrase must be at least 8 characters long"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_key_bitlength
+msgid ""
+"The bit length of the generated keys. \n"
+"The value must be between 1536 and 4096."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid ""
+"The current version of the 'account_ebics' module has no support to "
+"automatically process EBICS files with format %s."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid ""
+"The current version of this module requires to X509 support when enabling "
+"3SKey"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_config__ebics_key_version
+msgid "The key version of the electronic signature."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid ""
+"The module to process the '%(ebics_format)s' format is not installed on your system. \n"
+"Please install module '%(module)s'"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid ""
+"The module to process the '%(ebics_format)s' format is not installed on your system. \n"
+"Please install one of the following modules: \n"
+"%(modules)s."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_option
+msgid ""
+"The service option code consisting of 3-10 alphanumeric characters [A-Z0-9] "
+"(eg. COR, B2B)"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.constraint,message:account_ebics.constraint_ebics_file_name_uniq
+msgid "This File has already been down- or uploaded !"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "This file doesn't contain any transaction."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__swift_3skey
+msgid ""
+"Transactions for this user will be signed by means of the SWIFT 3SKey token."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file_format__signature_class__t
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__signature_class__t
+msgid "Transport signature"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__type
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file_format__type
+msgid "Type"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_file_format__btf_container
+msgid "Type of container consisting of 3 characters [A-Z] (eg. XML, ZIP)."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid "Unknown Error"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/wizards/ebics_xfer.py:0
+#, python-format
+msgid ""
+"Unknown Error during download of File Format %(name)s (%(order_type)s):"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_file_format__type__up
+#: model:ir.ui.menu,name:account_ebics.ebics_file_menu_upload
+msgid "Upload"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_upload
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_tree_upload
+msgid "Upload Date"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "Upload File"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__upload_fname
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__upload_fname
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_upload
+msgid "Upload Filename"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_admin_order__upload_format_ids
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_xfer__upload_format_ids
+msgid "Upload Format"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__transaction_rights__up
+msgid "Upload Only"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid ""
+"Use this button to bypass the EBICS initialization (e.g. in case you have "
+"manually transferred active EBICS keys from another system."
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_userid_view_form
+msgid "Use this button to update the EBICS certificates of your bank."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__transaction_rights
+msgid ""
+"Use this parameter to limit the transactions for this User to downloads or "
+"uploads."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_file__user_id
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_search
+msgid "User"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__user_ids
+msgid "Users"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__user_ids
+msgid "Users who are allowed to use this EBICS UserID for bank transactions."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields.selection,name:account_ebics.selection__ebics_userid__state__to_verify
+msgid "Verification"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "Versions supported by your bank:"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_file_view_form_result
+msgid "View Bank Statement(s)"
+msgstr ""
+
+#. module: account_ebics
+#: model_terms:ir.ui.view,arch_db:account_ebics.ebics_xfer_view_form_result
+msgid "View EBICS File(s)"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "Warnings"
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_admin_order__ebics_passphrase_store
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__ebics_passphrase_store
+#: model:ir.model.fields,help:account_ebics.field_ebics_xfer__ebics_passphrase_store
+msgid ""
+"When you uncheck this option the passphrase to unlock your private key will not be stored in the database. We recommend to use this if you want to upload signed payment orders via EBICS.\n"
+"You will be prompted to enter the passphrase for every EBICS transaction, hence do not uncheck this option on a userid for automated EBICS downloads."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid "X.509 certificates must be used with EBICS 3.0."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,field_description:account_ebics.field_ebics_userid__ebics_key_x509
+msgid "X509 support"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "You can only remove EBICS files in state 'Draft'."
+msgstr ""
+
+#. module: account_ebics
+#: model:ir.model.fields,help:account_ebics.field_ebics_userid__ebics_sig_passphrase
+msgid ""
+"You can set here a different passphrase for the EBICS signing key. This "
+"passphrase will never be stored hence you'll need to specify your passphrase"
+" for each transaction that requires a digital signature."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_config.py:0
+#, python-format
+msgid "You cannot remove active EBICS configurations."
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_file.py:0
+#, python-format
+msgid "s have"
+msgstr ""
+
+#. module: account_ebics
+#. odoo-python
+#: code:addons/account_ebics/models/ebics_userid.py:0
+#, python-format
+msgid ""
+"urlopen error:\n"
+" url '%(url)s' - %(val)s"
+msgstr ""
diff --git a/account_ebics/migrations/13.0.1.1/noupdate_changes.xml b/account_ebics/migrations/13.0.1.1/noupdate_changes.xml
new file mode 100644
index 0000000..0c91541
--- /dev/null
+++ b/account_ebics/migrations/13.0.1.1/noupdate_changes.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ EBICS Configuration model company rule
+
+
+ ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]
+
+
+
+ EBICS File model company rule
+
+
+ ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]
+
+
+
diff --git a/account_ebics/migrations/13.0.1.1/post-migration.py b/account_ebics/migrations/13.0.1.1/post-migration.py
new file mode 100644
index 0000000..ad8f969
--- /dev/null
+++ b/account_ebics/migrations/13.0.1.1/post-migration.py
@@ -0,0 +1,82 @@
+# Copyright 2009-2020 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+import os
+
+from openupgradelib import openupgrade # pylint: disable=W7936
+
+
+@openupgrade.migrate()
+def migrate(env, version):
+ _ebics_config_upgrade(env, version)
+ _noupdate_changes(env, version)
+
+
+def _ebics_config_upgrade(env, version):
+ env.cr.execute("SELECT * FROM ebics_config")
+ cfg_datas = env.cr.dictfetchall()
+ for cfg_data in cfg_datas:
+ cfg = env["ebics.config"].browse(cfg_data["id"])
+ journal = env["account.journal"].search(
+ [("bank_account_id", "=", cfg_data["bank_id"])]
+ )
+ keys_fn_old = cfg_data["ebics_keys"]
+ ebics_keys_root = os.path.dirname(keys_fn_old)
+ if os.path.isfile(keys_fn_old):
+ keys_fn = ebics_keys_root + "/" + cfg_data["ebics_user"] + "_keys"
+ os.rename(keys_fn_old, keys_fn)
+ state = cfg_data["state"] == "active" and "confirm" or "draft"
+ cfg.write(
+ {
+ "company_ids": [(6, 0, [cfg_data["company_id"]])],
+ "journal_ids": [(6, 0, journal.ids)],
+ "ebics_keys": ebics_keys_root,
+ "state": state,
+ }
+ )
+
+ user_vals = {
+ "ebics_config_id": cfg_data["id"],
+ "name": cfg_data["ebics_user"],
+ }
+ for fld in [
+ "signature_class",
+ "ebics_passphrase",
+ "ebics_ini_letter_fn",
+ "ebics_public_bank_keys_fn",
+ "ebics_key_x509",
+ "ebics_key_x509_dn_cn",
+ "ebics_key_x509_dn_o",
+ "ebics_key_x509_dn_ou",
+ "ebics_key_x509_dn_c",
+ "ebics_key_x509_dn_st",
+ "ebics_key_x509_dn_l",
+ "ebics_key_x509_dn_e",
+ "ebics_file_format_ids",
+ "state",
+ ]:
+ if cfg_data.get(fld):
+ if fld == "ebics_file_format_ids":
+ user_vals[fld] = [(6, 0, cfg_data[fld])]
+ elif fld == "state" and cfg_data["state"] == "active":
+ user_vals["state"] = "active_keys"
+ else:
+ user_vals[fld] = cfg_data[fld]
+ ebics_userid = env["ebics.userid"].create(user_vals)
+ env.cr.execute(
+ """
+ UPDATE ir_attachment
+ SET res_model = 'ebics.userid', res_id = %s
+ WHERE name in ('ebics_ini_letter', 'ebics_public_bank_keys');
+ """
+ % ebics_userid.id
+ )
+
+ if len(cfg_datas) == 1:
+ env.cr.execute("UPDATE ebics_file SET ebics_userid_id = %s" % ebics_userid.id)
+
+
+def _noupdate_changes(env, version):
+ openupgrade.load_data(
+ env.cr, "account_ebics", "migrations/13.0.1.1/noupdate_changes.xml"
+ )
diff --git a/account_ebics/migrations/13.0.1.1/pre-migration.py b/account_ebics/migrations/13.0.1.1/pre-migration.py
new file mode 100644
index 0000000..53b714f
--- /dev/null
+++ b/account_ebics/migrations/13.0.1.1/pre-migration.py
@@ -0,0 +1,9 @@
+# Copyright 2009-2020 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+
+def migrate(cr, version):
+ if not version:
+ return
+
+ cr.execute("DELETE FROM ebics_xfer;")
diff --git a/account_ebics/migrations/13.0.1.3/post-migration.py b/account_ebics/migrations/13.0.1.3/post-migration.py
new file mode 100644
index 0000000..97f0878
--- /dev/null
+++ b/account_ebics/migrations/13.0.1.3/post-migration.py
@@ -0,0 +1,42 @@
+# Copyright 2009-2020 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+_FILE_FORMATS = [
+ {
+ "xml_id_name": "ebics_ff_C52",
+ "download_process_method": "camt.052",
+ },
+ {
+ "xml_id_name": "ebics_ff_C53",
+ "download_process_method": "camt.053",
+ },
+ {
+ "xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
+ "download_process_method": "cfonb120",
+ },
+]
+
+
+def migrate(cr, version):
+ for ff in _FILE_FORMATS:
+ _update_file_format(cr, ff)
+
+
+def _update_file_format(cr, ff):
+ cr.execute( # pylint: disable=E8103
+ """
+ SELECT res_id FROM ir_model_data
+ WHERE module='account_ebics' AND name='{}'
+ """.format(ff["xml_id_name"])
+ )
+ res = cr.fetchone()
+ if res:
+ cr.execute( # pylint: disable=E8103
+ """
+ UPDATE ebics_file_format
+ SET download_process_method='{download_process_method}'
+ WHERE id={ff_id};
+ """.format(
+ download_process_method=ff["download_process_method"], ff_id=res[0]
+ )
+ )
diff --git a/account_ebics/migrations/13.0.1.3/pre-migration.py b/account_ebics/migrations/13.0.1.3/pre-migration.py
new file mode 100644
index 0000000..89fcc86
--- /dev/null
+++ b/account_ebics/migrations/13.0.1.3/pre-migration.py
@@ -0,0 +1,75 @@
+# Copyright 2009-2020 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+_FILE_FORMATS = [
+ {
+ "old_xml_id_name": "ebics_ff_camt_052_001_02_stm",
+ "new_xml_id_name": "ebics_ff_C52",
+ "new_name": "camt.052",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_camt_053_001_02_stm",
+ "new_xml_id_name": "ebics_ff_C53",
+ "new_name": "camt.053",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_camt_xxx_cfonb120_stm",
+ "new_xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_001_001_03_sct",
+ "new_xml_id_name": "ebics_ff_CCT",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_001",
+ "new_xml_id_name": "ebics_ff_XE2",
+ "new_name": "pain.001.001.03",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_008_001_02_sdd",
+ "new_xml_id_name": "ebics_ff_CDD",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_008",
+ "new_xml_id_name": "ebics_ff_XE3",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_008_001_02_sbb",
+ "new_xml_id_name": "ebics_ff_CDB",
+ },
+ {
+ "old_xml_id_name": "ebics_ff_pain_001_001_02_sct",
+ "new_xml_id_name": "ebics_ff_FUL_pain_001_001_02_sct",
+ },
+]
+
+
+def migrate(cr, version):
+ if not version:
+ return
+
+ for ff in _FILE_FORMATS:
+ _update_file_format(cr, ff)
+
+
+def _update_file_format(cr, ff):
+ cr.execute( # pylint: disable=E8103
+ """
+ SELECT id, res_id FROM ir_model_data
+ WHERE module='account_ebics' AND name='{}'
+ """.format(ff["old_xml_id_name"])
+ )
+ res = cr.fetchone()
+ if res:
+ query = """
+ UPDATE ir_model_data
+ SET name='{new_xml_id_name}'
+ WHERE id={xml_id};
+ """.format(new_xml_id_name=ff["new_xml_id_name"], xml_id=res[0])
+ if ff.get("new_name"):
+ query += """
+ UPDATE ebics_file_format
+ SET name='{new_name}'
+ WHERE id={ff_id};
+ """.format(new_name=ff["new_name"], ff_id=res[1])
+ cr.execute(query) # pylint: disable=E8103
diff --git a/account_ebics/migrations/15.0.1.1/pre-migration.py b/account_ebics/migrations/15.0.1.1/pre-migration.py
new file mode 100644
index 0000000..28ac779
--- /dev/null
+++ b/account_ebics/migrations/15.0.1.1/pre-migration.py
@@ -0,0 +1,54 @@
+# Copyright 2009-2022 Noviat.
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+
+def migrate(cr, version):
+ if not version:
+ return
+
+ cr.execute("select id from ebics_config")
+ cfg_ids = [x[0] for x in cr.fetchall()]
+ for cfg_id in cfg_ids:
+ cr.execute(
+ """
+ SELECT DISTINCT aj.company_id
+ FROM account_journal_ebics_config_rel rel
+ JOIN account_journal aj ON rel.account_journal_id = aj.id
+ WHERE ebics_config_id = %s
+ """,
+ (cfg_id,),
+ )
+ new_cpy_ids = [x[0] for x in cr.fetchall()]
+ cr.execute(
+ """
+ SELECT DISTINCT res_company_id
+ FROM ebics_config_res_company_rel
+ WHERE ebics_config_id = %s
+ """,
+ (cfg_id,),
+ )
+ old_cpy_ids = [x[0] for x in cr.fetchall()]
+
+ to_add = []
+ for cid in new_cpy_ids:
+ if cid in old_cpy_ids:
+ old_cpy_ids.remove(cid)
+ else:
+ to_add.append(cid)
+ if old_cpy_ids:
+ cr.execute(
+ """
+ DELETE FROM ebics_config_res_company_rel
+ WHERE res_company_id IN %s
+ """,
+ (tuple(old_cpy_ids),),
+ )
+ if to_add:
+ for cid in to_add:
+ cr.execute(
+ """
+ INSERT INTO ebics_config_res_company_rel(ebics_config_id, res_company_id)
+ VALUES (%s, %s);
+ """,
+ (cfg_id, cid),
+ )
diff --git a/account_ebics/models/__init__.py b/account_ebics/models/__init__.py
new file mode 100644
index 0000000..0a211c1
--- /dev/null
+++ b/account_ebics/models/__init__.py
@@ -0,0 +1,6 @@
+from . import fintech_ebics_register
+from . import account_bank_statement
+from . import ebics_config
+from . import ebics_file
+from . import ebics_file_format
+from . import ebics_userid
diff --git a/account_ebics/models/account_bank_statement.py b/account_ebics/models/account_bank_statement.py
new file mode 100644
index 0000000..89c5eaf
--- /dev/null
+++ b/account_ebics/models/account_bank_statement.py
@@ -0,0 +1,11 @@
+# Copyright 2009-2023 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import fields, models
+
+
+class AccountBankStatement(models.Model):
+ _inherit = "account.bank.statement"
+
+ ebics_file_id = fields.Many2one(comodel_name="ebics.file", string="EBICS Data File")
+ import_format = fields.Char(readonly=True)
diff --git a/account_ebics/models/ebics_config.py b/account_ebics/models/ebics_config.py
new file mode 100644
index 0000000..5aca89c
--- /dev/null
+++ b/account_ebics/models/ebics_config.py
@@ -0,0 +1,221 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import logging
+import os
+import re
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+
+class EbicsConfig(models.Model):
+ """
+ EBICS configuration is stored in a separate object in order to
+ allow extra security policies on this object.
+ """
+
+ _name = "ebics.config"
+ _description = "EBICS Configuration"
+ _order = "name"
+
+ name = fields.Char(
+ required=True,
+ )
+ journal_ids = fields.Many2many(
+ comodel_name="account.journal",
+ relation="account_journal_ebics_config_rel",
+ string="Bank Accounts",
+ domain="[('type', '=', 'bank')]",
+ )
+ ebics_host = fields.Char(
+ string="EBICS HostID",
+ required=True,
+ help="Contact your bank to get the EBICS HostID."
+ "\nIn France the BIC is usually allocated to the HostID "
+ "whereas in Germany it tends to be an institute specific string "
+ "of 8 characters.",
+ )
+ ebics_url = fields.Char(
+ string="EBICS URL",
+ required=True,
+ help="Contact your bank to get the EBICS URL.",
+ )
+ ebics_version = fields.Selection(
+ selection=[
+ ("H003", "H003 (2.4)"),
+ ("H004", "H004 (2.5)"),
+ ("H005", "H005 (3.0)"),
+ ],
+ string="EBICS protocol version",
+ required=True,
+ default="H004",
+ )
+ ebics_partner = fields.Char(
+ string="EBICS PartnerID",
+ required=True,
+ help="Organizational unit (company or individual) "
+ "that concludes a contract with the bank. "
+ "\nIn this contract it will be agreed which order types "
+ "(file formats) are used, which accounts are concerned, "
+ "which of the customer's users (subscribers) "
+ "communicate with the EBICS bank server and the authorisations "
+ "that these users will possess. "
+ "\nIt is identified by the PartnerID.",
+ )
+ ebics_userid_ids = fields.One2many(
+ comodel_name="ebics.userid",
+ inverse_name="ebics_config_id",
+ string="EBICS UserID",
+ help="Human users or a technical system that is/are "
+ "assigned to a customer. "
+ "\nOn the EBICS bank server it is identified "
+ "by the combination of UserID and PartnerID. "
+ "The technical subscriber serves only for the data exchange "
+ "between customer and financial institution. "
+ "The human user also can authorise orders.",
+ )
+ # We store the EBICS keys in a separate directory in the file system.
+ # This directory requires special protection to reduce fraude.
+ ebics_keys = fields.Char(
+ string="EBICS Keys Root",
+ required=True,
+ default=lambda self: self._default_ebics_keys(),
+ help="Root Directory for storing the EBICS Keys.",
+ )
+ ebics_key_version = fields.Selection(
+ selection=[("A005", "A005 (RSASSA-PKCS1-v1_5)"), ("A006", "A006 (RSASSA-PSS)")],
+ string="EBICS key version",
+ default="A006",
+ help="The key version of the electronic signature.",
+ )
+ ebics_key_bitlength = fields.Integer(
+ string="EBICS key bitlength",
+ default=2048,
+ help="The bit length of the generated keys. "
+ "\nThe value must be between 1536 and 4096.",
+ )
+ ebics_file_format_ids = fields.Many2many(
+ comodel_name="ebics.file.format",
+ column1="config_id",
+ column2="format_id",
+ string="EBICS File Format",
+ )
+ state = fields.Selection(
+ selection=[("draft", "Draft"), ("confirm", "Confirmed")],
+ default="draft",
+ required=True,
+ readonly=True,
+ )
+ order_number = fields.Char(
+ size=4,
+ help="Specify the number for the next order."
+ "\nThis number should match the following pattern : "
+ "[A-Z]{1}[A-Z0-9]{3}",
+ )
+ active = fields.Boolean(default=True)
+ company_ids = fields.Many2many(
+ comodel_name="res.company",
+ relation="ebics_config_res_company_rel",
+ string="Companies",
+ readonly=True,
+ help="Companies sharing this EBICS contract.",
+ )
+
+ @api.model
+ def _default_ebics_keys(self):
+ return "/".join(["/etc/odoo/ebics_keys", self._cr.dbname])
+
+ @api.constrains("ebics_key_bitlength")
+ def _check_ebics_key_bitlength(self):
+ for cfg in self:
+ if cfg.ebics_version == "H005" and cfg.ebics_key_bitlength < 2048:
+ raise UserError(self.env._("EBICS key bitlength must be >= 2048."))
+
+ @api.constrains("order_number")
+ def _check_order_number(self):
+ for cfg in self:
+ nbr = cfg.order_number
+ ok = True
+ if nbr:
+ if len(nbr) != 4:
+ ok = False
+ else:
+ pattern = re.compile("[A-Z]{1}[A-Z0-9]{3}")
+ if not pattern.match(nbr):
+ ok = False
+ if not ok:
+ raise UserError(
+ self.env._( # pylint: disable=W8120
+ "Order Number should comply with the following pattern:"
+ "\n[A-Z]{1}[A-Z0-9]{3}"
+ )
+ )
+
+ def write(self, vals):
+ """
+ Due to the multi-company nature of the EBICS config we
+ need to adapt the company_ids in the write method.
+ """
+ if "journal_ids" not in vals:
+ return super().write(vals)
+ for rec in self:
+ old_company_ids = rec.journal_ids.mapped("company_id").ids
+ super(EbicsConfig, rec).write(vals)
+ new_company_ids = rec.journal_ids.mapped("company_id").ids
+ updates = []
+ for cid in new_company_ids:
+ if cid in old_company_ids:
+ old_company_ids.remove(cid)
+ else:
+ updates += [(4, cid)]
+ updates += [(3, x) for x in old_company_ids]
+ super(EbicsConfig, rec).write({"company_ids": updates})
+ return True
+
+ def unlink(self):
+ for ebics_config in self:
+ if ebics_config.state == "active":
+ raise UserError(
+ self.env._("You cannot remove active EBICS configurations.")
+ )
+ return super().unlink()
+
+ def set_to_draft(self):
+ return self.write({"state": "draft"})
+
+ def set_to_confirm(self):
+ return self.write({"state": "confirm"})
+
+ def _get_order_number(self):
+ return self.order_number
+
+ 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":
+ o_list[-i] = "0"
+ continue
+ else:
+ o_list[-i] = chr(ord(c) + 1)
+ break
+ next_order_number = "".join(o_list)
+ if next_order_number == "ZZZZ":
+ next_order_number = "A000"
+ self.order_number = next_order_number
+
+ def _check_ebics_keys(self):
+ dirname = self.ebics_keys or ""
+ if not os.path.exists(dirname):
+ raise UserError(
+ self.env._(
+ "EBICS Keys Root Directory %s is not available."
+ "\nPlease contact your system administrator."
+ )
+ % dirname
+ )
diff --git a/account_ebics/models/ebics_file.py b/account_ebics/models/ebics_file.py
new file mode 100644
index 0000000..cc5dd45
--- /dev/null
+++ b/account_ebics/models/ebics_file.py
@@ -0,0 +1,639 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import base64
+import logging
+from copy import deepcopy
+from sys import exc_info
+from traceback import format_exception
+
+from lxml import etree
+
+from odoo import fields, models
+from odoo.exceptions import UserError
+
+from odoo.addons.base.models.res_bank import sanitize_account_number
+
+_logger = logging.getLogger(__name__)
+
+DUP_CHECK_FORMATS = ["cfonb120", "camt053"]
+
+
+class EbicsFile(models.Model):
+ _name = "ebics.file"
+ _description = "Object to store EBICS Data Files"
+ _order = "date desc"
+ _sql_constraints = [
+ (
+ "name_uniq",
+ "unique (name, format_id)",
+ "This File has already been down- or uploaded !",
+ )
+ ]
+
+ name = fields.Char(string="Filename")
+ data = fields.Binary(string="File", readonly=True)
+ format_id = fields.Many2one(
+ comodel_name="ebics.file.format", string="EBICS File Formats", readonly=True
+ )
+ type = fields.Selection(related="format_id.type", readonly=True)
+ date_from = fields.Date(
+ readonly=True, help="'Date From' as entered in the download wizard."
+ )
+ date_to = fields.Date(
+ readonly=True, help="'Date To' as entered in the download wizard."
+ )
+ date = fields.Datetime(
+ required=True, readonly=True, help="File Upload/Download date"
+ )
+ bank_statement_ids = fields.One2many(
+ comodel_name="account.bank.statement",
+ inverse_name="ebics_file_id",
+ string="Generated Bank Statements",
+ readonly=True,
+ )
+ state = fields.Selection(
+ [("draft", "Draft"), ("done", "Done")],
+ default="draft",
+ required=True,
+ readonly=True,
+ )
+ user_id = fields.Many2one(
+ comodel_name="res.users",
+ string="User",
+ default=lambda self: self.env.user,
+ readonly=True,
+ )
+ ebics_userid_id = fields.Many2one(
+ comodel_name="ebics.userid",
+ string="EBICS UserID",
+ ondelete="restrict",
+ readonly=True,
+ )
+ note = fields.Text(string="Notes")
+ note_process = fields.Text(
+ string="Process Notes",
+ readonly=True,
+ )
+ company_ids = fields.Many2many(
+ comodel_name="res.company",
+ string="Companies",
+ readonly=True,
+ help="Companies sharing this EBICS file.",
+ )
+
+ def unlink(self):
+ ff_methods = self._file_format_methods()
+ for ebics_file in self:
+ if ebics_file.state == "done":
+ raise UserError(
+ self.env._("You can only remove EBICS files in state 'Draft'.")
+ )
+ # execute format specific actions
+ ff = ebics_file.format_id.download_process_method
+ if ff in ff_methods:
+ if ff_methods[ff].get("unlink"):
+ ff_methods[ff]["unlink"]()
+ # remove bank statements
+ ebics_file.bank_statement_ids.unlink()
+ return super().unlink()
+
+ def set_to_draft(self):
+ return self.write({"state": "draft"})
+
+ def set_to_done(self):
+ return self.write({"state": "done"})
+
+ def process(self):
+ self.ensure_one()
+ self = self.with_context(allowed_company_ids=self.env.user.company_ids.ids)
+ self.note_process = ""
+ ff_methods = self._file_format_methods()
+ ff = self.format_id.download_process_method
+ if ff in ff_methods:
+ if ff_methods[ff].get("process"):
+ res = ff_methods[ff]["process"]()
+ self.state = "done"
+ return res
+ else:
+ return self._process_undefined_format()
+
+ def action_open_bank_statements(self):
+ self.ensure_one()
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "account.action_bank_statement_tree"
+ )
+ domain = [("id", "in", self.env.context.get("statement_ids"))]
+ action["domain"] = domain
+ return action
+
+ def button_close(self):
+ self.ensure_one()
+ return {"type": "ir.actions.act_window_close"}
+
+ def _file_format_methods(self):
+ """
+ Extend this dictionary in order to add support
+ for extra file formats.
+ """
+ res = {
+ "cfonb120": {
+ "process": self._process_cfonb120,
+ "unlink": self._unlink_cfonb120,
+ },
+ "camt.052": {
+ "process": self._process_camt052,
+ "unlink": self._unlink_camt052,
+ },
+ "camt.053": {
+ "process": self._process_camt053,
+ "unlink": self._unlink_camt053,
+ },
+ "camt.054": {
+ "process": self._process_camt054,
+ "unlink": self._unlink_camt054,
+ },
+ "pain.002": {
+ "process": self._process_pain002,
+ "unlink": self._unlink_pain002,
+ },
+ }
+ return res
+
+ def _check_import_module(self, module, raise_if_not_found=True):
+ mod = (
+ self.env["ir.module.module"]
+ .sudo()
+ .search([("name", "=like", module), ("state", "=", "installed")])
+ )
+ if not mod:
+ if raise_if_not_found:
+ raise UserError(
+ self.env._(
+ "The module to process the '%(ebics_format)s' format is not "
+ "installed on your system. "
+ "\nPlease install module '%(module)s'",
+ ebics_format=self.format_id.name,
+ module=module,
+ )
+ )
+ return False
+ return True
+
+ def _lookup_journal(self, res, acc_number, currency_code):
+ currency = self.env["res.currency"].search(
+ [("name", "=ilike", currency_code)], limit=1
+ )
+ journal = self.env["account.journal"]
+ if not currency:
+ message = self.env._("Currency %(cc)s not found.", cc=currency_code)
+ res["notifications"].append({"type": "error", "message": message})
+ return (currency, journal)
+
+ journals = self.env["account.journal"].search(
+ [
+ ("type", "=", "bank"),
+ (
+ "bank_account_id.sanitized_acc_number",
+ "ilike",
+ acc_number,
+ ),
+ ]
+ )
+ if not journals:
+ message = self.env._(
+ "No financial journal found for Account Number %(nbr)s, "
+ "Currency %(cc)s",
+ nbr=acc_number,
+ cc=currency_code,
+ )
+ res["notifications"].append({"type": "error", "message": message})
+ return (currency, journal)
+
+ for jrnl in journals:
+ journal_currency = jrnl.currency_id or jrnl.company_id.currency_id
+ if journal_currency != currency:
+ continue
+ else:
+ journal = jrnl
+ break
+
+ if not journal:
+ message = self.env._(
+ "No financial journal found for Account Number %(nbr)s, "
+ "Currency %(cc)s",
+ nbr=acc_number,
+ cc=currency_code,
+ )
+ res["notifications"].append({"type": "error", "message": message})
+ return (currency, journal)
+
+ def _process_download_result(self, res, file_format=None):
+ """
+ We perform a duplicate statement check after the creation of the bank
+ statements since we rely on Odoo Enterprise or OCA modules for the
+ bank statement creation.
+ From a development standpoint (code creation/maintenance) a check after
+ creation is the easiest way.
+ """
+ statement_ids = res["statement_ids"]
+ notifications = res["notifications"]
+ statements = self.env["account.bank.statement"].sudo().browse(statement_ids)
+ if statements:
+ statements.write({"import_format": file_format})
+ statements = self._statement_duplicate_check(res, statements)
+ elif not notifications:
+ notifications.append(
+ {
+ "type": "warning",
+ "message": self.env._("This file doesn't contain any transaction."),
+ }
+ )
+ st_cnt = len(statements)
+ warning_cnt = error_cnt = 0
+ if notifications:
+ errors = []
+ warnings = []
+ for notif in notifications:
+ if isinstance(notif, dict) and notif["type"] == "error":
+ error_cnt += 1
+ parts = [notif[k] for k in notif if k in ("message", "details")]
+ errors.append("\n".join(parts))
+ elif isinstance(notif, dict) and notif["type"] == "warning":
+ warning_cnt += 1
+ parts = [notif[k] for k in notif if k in ("message", "details")]
+ warnings.append("\n".join(parts))
+ elif isinstance(notif, str):
+ warning_cnt += 1
+ warnings.append(notif + "\n")
+
+ self.note_process += self.env._("Process file %(fn)s results:", fn=self.name)
+ if error_cnt:
+ self.note_process += "\n\n" + self.env._("Errors") + ":\n"
+ self.note_process += "\n".join(errors)
+ self.note_process += "\n\n"
+ self.note_process += self.env._("Number of errors: %(nr)s", nr=error_cnt)
+ if warning_cnt:
+ self.note_process += "\n\n" + self.env._("Warnings") + ":\n"
+ self.note_process += "\n".join(warnings)
+ self.note_process += "\n\n"
+ self.note_process += self.env._(
+ "Number of warnings: %(nr)s", nr=warning_cnt
+ )
+ self.note_process += "\n"
+ if st_cnt:
+ self.note_process += "\n\n"
+ self.note_process += self.env._(
+ "%(st_cnt)s bank statement%(sp)s been imported: ",
+ st_cnt=st_cnt,
+ sp=st_cnt == 1 and self.env._(" has") or self.env._("s have"),
+ )
+ self.note_process += "\n"
+ for statement in statements:
+ self.note_process += "\n" + self.env._(
+ "Statement %(st)s dated %(date)s (Company: %(cpy)s)",
+ st=statement.name,
+ date=statement.date,
+ cpy=statement.company_id.name,
+ )
+ if statements:
+ self.sudo().bank_statement_ids = [(4, x) for x in statements.ids]
+ company_ids = self.sudo().bank_statement_ids.mapped("company_id").ids
+ self.company_ids = [(6, 0, company_ids)]
+ ctx = dict(self.env.context, statement_ids=statements.ids)
+ module = __name__.split("addons.")[1].split(".")[0]
+ result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
+ return {
+ "name": self.env._("Import EBICS File"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": self._name,
+ "view_id": result_view.id,
+ "target": "new",
+ "context": ctx,
+ "type": "ir.actions.act_window",
+ }
+
+ def _statement_duplicate_check(self, res, statements):
+ """
+ This check is required for import modules that do not
+ set the 'unique_import_id' on the statement lines.
+ E.g. OCA camt import
+ """
+ to_unlink = self.env["account.bank.statement"]
+ for statement in statements.filtered(
+ lambda r: r.import_format in DUP_CHECK_FORMATS
+ ):
+ dup = self.env["account.bank.statement"].search_count(
+ [
+ ("id", "!=", statement.id),
+ ("name", "=", statement.name),
+ ("company_id", "=", statement.company_id.id),
+ ("date", "=", statement.date),
+ ("import_format", "=", statement.import_format),
+ ]
+ )
+ if dup:
+ message = self.env._(
+ "Statement %(st_name)s dated %(date)s has already been imported.",
+ st_name=statement.name,
+ date=statement.date,
+ )
+ res["notifications"].append({"type": "warning", "message": message})
+ to_unlink += statement
+ res["statement_ids"] = [
+ x for x in res["statement_ids"] if x not in to_unlink.ids
+ ]
+ statements -= to_unlink
+ to_unlink.unlink()
+ return statements
+
+ def _process_cfonb120(self):
+ import_module = "account_statement_import_fr_cfonb"
+ self._check_import_module(import_module)
+ res = {"statement_ids": [], "notifications": []}
+ st_datas = self._split_cfonb(res)
+ if st_datas:
+ self._process_bank_statement_oca(res, st_datas)
+ return self._process_download_result(res, file_format="cfonb120")
+
+ def _unlink_cfonb120(self):
+ """
+ Placeholder for cfonb120 specific actions before removing the
+ EBICS data file and its related bank statements.
+ """
+
+ def _split_cfonb(self, res):
+ """
+ Split CFONB file received via EBICS per statement.
+ Statements without transactions are removed.
+ """
+ datas = []
+ file_data = base64.b64decode(self.data)
+ file_data.replace(b"\n", b"").replace(b"\r", b"")
+ if len(file_data) % 120:
+ message = self.env._(
+ "Incorrect CFONB120 file:\n"
+ "the file is not divisible in 120 char lines"
+ )
+ res["notifications"].append({"type": "error", "message": message})
+ return datas
+
+ lines = []
+ for i in range(0, len(file_data), 120):
+ lines.append(file_data[i : i + 120])
+
+ st_lines = b""
+ transactions = False
+ for line in lines:
+ rec_type = line[0:2]
+ currency_code = line[16:19].decode()
+ acc_number = line[21:32].decode()
+ st_lines += line + b"\n"
+ if rec_type == b"04":
+ transactions = True
+ if rec_type == b"07":
+ if transactions:
+ currency, journal = self._lookup_journal(
+ res, acc_number, currency_code
+ )
+ if currency and journal:
+ datas.append(
+ {
+ "acc_number": acc_number,
+ "journal_id": journal.id,
+ "company_id": journal.company_id.id,
+ "data": base64.b64encode(st_lines),
+ }
+ )
+ st_lines = b""
+ transactions = False
+ return datas
+
+ def _process_camt052(self):
+ import_module = "account_statement_import_camt"
+ self._check_import_module(import_module)
+ return self._process_camt053(file_format="camt052")
+
+ def _unlink_camt052(self):
+ """
+ Placeholder for camt052 specific actions before removing the
+ EBICS data file and its related bank statements.
+ """
+
+ def _process_camt054(self):
+ import_module = "account_statement_import_camt"
+ self._check_import_module(import_module)
+ return self._process_camt053(file_format="camt054")
+
+ def _unlink_camt054(self):
+ """
+ Placeholder for camt054 specific actions before removing the
+ EBICS data file and its related bank statements.
+ """
+
+ def _process_camt053(self, file_format=None):
+ """
+ The Odoo standard statement import is based on manual selection
+ of a financial journal before importing the electronic statement file.
+ An EBICS download may return a single file containing a large number of
+ statements from different companies/journals.
+ Hence we need to split the CAMT file into
+ single statement CAMT files before we can call the logic
+ implemented by the Odoo OE or Community CAMT parsers.
+ """
+ modules = [
+ ("oca", "account_statement_import_camt"),
+ ("oe", "account_bank_statement_import_camt"),
+ ]
+ author = False
+ for entry in modules:
+ if self._check_import_module(entry[1], raise_if_not_found=False):
+ author = entry[0]
+ break
+ if not author:
+ raise UserError(
+ self.env._(
+ "The module to process the '%(ebics_format)s' format is "
+ "not installed on your system. "
+ "\nPlease install one of the following modules: \n%(modules)s.",
+ ebics_format=self.format_id.name,
+ modules=", ".join([x[1] for x in modules]),
+ )
+ )
+ res = {"statement_ids": [], "notifications": []}
+ st_datas = self._split_camt(res)
+ if author == "oca":
+ self._process_bank_statement_oca(res, st_datas)
+ else:
+ self._process_bank_statement_oe(res, st_datas)
+ file_format = file_format or "camt053"
+ return self._process_download_result(res, file_format=file_format)
+
+ def _process_bank_statement_oca(self, res, st_datas):
+ for st_data in st_datas:
+ try:
+ with self.env.cr.savepoint():
+ self._create_bank_statement_oca(res, st_data)
+ except UserError as e:
+ res["notifications"].append(
+ {"type": "error", "message": "".join(e.args)}
+ )
+ except Exception:
+ tb = "".join(format_exception(*exc_info()))
+ res["notifications"].append({"type": "error", "message": tb})
+
+ def _create_bank_statement_oca(self, res, st_data):
+ wiz = (
+ self.env["account.statement.import"]
+ .with_company(st_data["company_id"])
+ .with_context(active_model="ebics.file")
+ .create({"statement_filename": self.name})
+ )
+ wiz.import_single_file(base64.b64decode(st_data["data"]), res)
+
+ def _process_bank_statement_oe(self, res, st_datas):
+ """
+ We execute a cr.commit() after every statement import since we get a
+ 'savepoint does not exist' error when using 'with self.env.cr.savepoint()'.
+ """
+ for st_data in st_datas:
+ try:
+ self._create_bank_statement_oe(res, st_data)
+ self.env.cr.commit() # pylint: disable=E8102
+ except UserError as e:
+ msg = "".join(e.args)
+ msg += "\n"
+ msg += self.env._(
+ "Statement for Account Number %(nr)s has not been processed.",
+ nr=st_data["acc_number"],
+ )
+ res["notifications"].append({"type": "error", "message": msg})
+ except Exception:
+ tb = "".join(format_exception(*exc_info()))
+ res["notifications"].append({"type": "error", "message": tb})
+
+ def _create_bank_statement_oe(self, res, st_data):
+ attachment = (
+ self.env["ir.attachment"]
+ .with_company(st_data["company_id"])
+ .create(
+ {
+ "name": self.name,
+ "datas": st_data["data"],
+ "store_fname": self.name,
+ }
+ )
+ )
+ journal = (
+ self.env["account.journal"]
+ .with_company(st_data["company_id"])
+ .browse(st_data["journal_id"])
+ )
+ act = journal._import_bank_statement(attachment)
+ for entry in act["domain"]:
+ if (
+ isinstance(entry, tuple)
+ and entry[0] == "statement_id"
+ and entry[1] == "in"
+ ):
+ res["statement_ids"].extend(entry[2])
+ break
+ notifications = act["context"]["notifications"]
+ if notifications:
+ res["notifications"].append(act["context"]["notifications"])
+
+ def _unlink_camt053(self):
+ """
+ Placeholder for camt053 specific actions before removing the
+ EBICS data file and its related bank statements.
+ """
+
+ def _split_camt(self, res):
+ """
+ Split CAMT file received via EBICS per statement.
+ Statements without transactions are removed.
+ """
+ datas = []
+ file_data = base64.b64decode(self.data)
+ root = etree.fromstring(file_data, parser=etree.XMLParser(recover=True))
+ if root is None:
+ message = self.env._("Invalid XML file.")
+ res["notifications"].append({"type": "error", "message": message})
+ ns = {k or "ns": v for k, v in root.nsmap.items()}
+ camt_variant = ns["ns"].split("camt.")[1][:3]
+ variant_tags = {
+ "052": "Rpt",
+ "053": "Stmt",
+ "054": "Ntfctn",
+ }
+ camt_tag = variant_tags[camt_variant]
+ stmts = root[0].findall(f"ns:{camt_tag}", ns)
+ for i, stmt in enumerate(stmts):
+ acc_number = sanitize_account_number(
+ stmt.xpath(
+ "ns:Acct/ns:Id/ns:IBAN/text() | ns:Acct/ns:Id/ns:Othr/ns:Id/text()",
+ namespaces=ns,
+ )[0]
+ )
+ if not acc_number:
+ message = self.env._("No bank account number found.")
+ res["notifications"].append({"type": "error", "message": message})
+ continue
+ currency_code = stmt.xpath(
+ "ns:Acct/ns:Ccy/text() | ns:Bal/ns:Amt/@Ccy", namespaces=ns
+ )[0]
+ # some banks (e.g. COMMERZBANK) add the currency as the last 3 digits
+ # of the bank account number hence we need to remove this since otherwise
+ # the journal matching logic fails
+ if acc_number[-3:] == currency_code:
+ acc_number = acc_number[:-3]
+
+ root_new = deepcopy(root)
+ entries = False
+ for j, el in enumerate(root_new[0].findall(f"ns:{camt_tag}", ns)):
+ if j != i:
+ el.getparent().remove(el)
+ else:
+ entries = el.findall("ns:Ntry", ns)
+ if not entries:
+ continue
+ else:
+ currency, journal = self._lookup_journal(res, acc_number, currency_code)
+ if not (currency and journal):
+ continue
+ datas.append(
+ {
+ "acc_number": acc_number,
+ "journal_id": journal.id,
+ "company_id": journal.company_id.id,
+ "data": base64.b64encode(etree.tostring(root_new)),
+ }
+ )
+
+ return datas
+
+ def _process_pain002(self):
+ """
+ Placeholder for processing pain.002 files.
+ TODO:
+ add import logic based upon OCA 'account_payment_return_import'
+ """
+
+ def _unlink_pain002(self):
+ """
+ Placeholder for pain.002 specific actions before removing the
+ EBICS data file.
+ """
+ raise NotImplementedError
+
+ def _process_undefined_format(self):
+ raise UserError(
+ self.env._(
+ "The current version of the 'account_ebics' module "
+ "has no support to automatically process EBICS files "
+ "with format %s."
+ )
+ % self.format_id.name
+ )
diff --git a/account_ebics/models/ebics_file_format.py b/account_ebics/models/ebics_file_format.py
new file mode 100644
index 0000000..5c88f1d
--- /dev/null
+++ b/account_ebics/models/ebics_file_format.py
@@ -0,0 +1,119 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+from odoo import api, fields, models
+
+
+class EbicsFileFormat(models.Model):
+ _name = "ebics.file.format"
+ _description = "EBICS File Formats"
+ _order = "type,name,order_type"
+
+ ebics_version = fields.Selection(
+ selection=[
+ ("2", "2"),
+ ("3", "3"),
+ ],
+ string="EBICS protocol version",
+ required=True,
+ default="2",
+ )
+ name = fields.Char(
+ string="Request Type",
+ help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
+ "Specify camt.052, camt.053, camt.054 for camt "
+ "Order Types such as C53, Z53, C54, Z54.\n"
+ "This name has to match the 'Request Type' in your "
+ "EBICS contract for Order Type 'FDL' or 'FUL'.\n",
+ )
+ type = fields.Selection(
+ selection=[("down", "Download"), ("up", "Upload")], required=True
+ )
+ order_type = fields.Char(
+ required=True,
+ help="EBICS 3.0: BTD (download) or BTU (upload).\n"
+ "EBICS 2.0: E.g. C53 (check your EBICS contract). "
+ "For most banks in France you should use the "
+ "format neutral Order Types 'FUL' for upload "
+ "and 'FDL' for download.",
+ )
+ download_process_method = fields.Selection(
+ selection="_selection_download_process_method",
+ help="Enable processing within Odoo of the downloaded file "
+ "via the 'Process' button."
+ "E.g. specify camt.053 to import a camt.053 file and create "
+ "a bank statement.",
+ )
+ # TODO:
+ # move signature_class parameter so that it can be set per EBICS config
+ signature_class = fields.Selection(
+ selection=[("E", "Single signature"), ("T", "Transport signature")],
+ help="Please doublecheck the security of your Odoo "
+ "ERP system when using class 'E' to prevent unauthorised "
+ "users to make supplier payments."
+ "\nLeave this field empty to use the default "
+ "defined for your EBICS UserID.",
+ )
+ description = fields.Char()
+ suffix = fields.Char(
+ help="Specify the filename suffix for this File Format.\nE.g. c53.xml",
+ )
+ # EBICS 3.0 BTF
+ btf_service = fields.Char(
+ string="BTF Service",
+ help="BTF Service Name)\n"
+ "The service code name consisting of 3 alphanumeric characters "
+ "[A-Z0-9] (e.g. SCT, SDD, STM, EOP)",
+ )
+ btf_message = fields.Char(
+ string="BTF Message Name",
+ help="BTF Message Name\n"
+ "The message name consisting of up to 10 alphanumeric characters "
+ "[a-z0-9.] (eg. pain.001, pain.008, camt.053)",
+ )
+ btf_scope = fields.Char(
+ string="BTF Scope",
+ help="Scope of service.\n"
+ "Either an ISO-3166 ALPHA 2 country code or an issuer code "
+ "of 3 alphanumeric characters [A-Z0-9].",
+ )
+ btf_option = fields.Char(
+ string="BTF Option",
+ help="The service option code consisting of 3-10 alphanumeric "
+ "characters [A-Z0-9] (eg. COR, B2B)",
+ )
+ btf_container = fields.Char(
+ string="BTF Container",
+ help="Type of container consisting of 3 characters [A-Z] (eg. XML, ZIP).",
+ )
+ btf_version = fields.Char(
+ string="BTF Version",
+ help="Message version consisting of 2 numeric characters [0-9] (eg. 03).",
+ )
+ btf_variant = fields.Char(
+ string="BTF Variant",
+ help="Message variant consisting of 3 numeric characters [0-9] (eg. 001).",
+ )
+ btf_format = fields.Char(
+ string="BTF Format",
+ help="Message format consisting of 1-4 alphanumeric characters [A-Z0-9] "
+ "(eg. XML, JSON, PDF).",
+ )
+
+ @api.model
+ def _selection_download_process_method(self):
+ methods = self.env["ebics.file"]._file_format_methods().keys()
+ return [(x, x) for x in methods]
+
+ @api.onchange("type")
+ def _onchange_type(self):
+ if self.type == "up":
+ self.download_process_method = False
+
+ @api.depends("ebics_version", "name", "btf_message", "description")
+ def _compute_display_name(self):
+ for rec in self:
+ name = rec.ebics_version == "2" and rec.name or rec.btf_message
+ if rec.description:
+ name += " - " + rec.description
+ rec.display_name = name
diff --git a/account_ebics/models/ebics_userid.py b/account_ebics/models/ebics_userid.py
new file mode 100644
index 0000000..bba605b
--- /dev/null
+++ b/account_ebics/models/ebics_userid.py
@@ -0,0 +1,607 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import base64
+import logging
+import os
+from sys import exc_info
+from traceback import format_exception
+from urllib.error import URLError
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import fintech
+ from fintech.ebics import (
+ EbicsBank,
+ EbicsClient,
+ EbicsFunctionalError,
+ EbicsKeyRing,
+ EbicsTechnicalError,
+ EbicsUser,
+ )
+
+ fintech.cryptolib = "cryptography"
+except ImportError:
+ _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 EbicsUserID(models.Model):
+ _name = "ebics.userid"
+ _description = "EBICS UserID"
+ _order = "name"
+
+ name = fields.Char(
+ string="EBICS UserID",
+ required=True,
+ help="Human users or a technical system that is/are "
+ "assigned to a customer. "
+ "\nOn the EBICS bank server it is identified "
+ "by the combination of UserID and PartnerID. "
+ "The technical subscriber serves only for the data exchange "
+ "between customer and financial institution. "
+ "The human user also can authorise orders.",
+ )
+ ebics_config_id = fields.Many2one(
+ comodel_name="ebics.config",
+ string="EBICS Configuration",
+ ondelete="cascade",
+ required=True,
+ )
+ ebics_version = fields.Selection(related="ebics_config_id.ebics_version")
+ user_ids = fields.Many2many(
+ comodel_name="res.users",
+ string="Users",
+ required=True,
+ help="Users who are allowed to use this EBICS UserID for "
+ " bank transactions.",
+ )
+ signature_class = fields.Selection(
+ selection=[("E", "Single signature"), ("T", "Transport signature")],
+ required=True,
+ default="T",
+ help="Default signature class."
+ "This default can be overriden for specific "
+ "EBICS transactions (cf. File Formats).",
+ )
+ transaction_rights = fields.Selection(
+ selection=[
+ ("both", "Download and Upload"),
+ ("down", "Download Only"),
+ ("up", "Upload Only"),
+ ],
+ string="Allowed Transactions",
+ default="both",
+ required=True,
+ help="Use this parameter to limit the transactions for this User "
+ "to downloads or uploads.",
+ )
+ ebics_keys_fn = fields.Char(compute="_compute_ebics_keys_fn")
+ ebics_keys_found = fields.Boolean(compute="_compute_ebics_keys_found")
+ ebics_passphrase = fields.Char(string="EBICS Passphrase")
+ ebics_passphrase_store = fields.Boolean(
+ string="Store EBICS Passphrase",
+ default=True,
+ help="When you uncheck this option the passphrase to unlock "
+ "your private key will not be stored in the database. "
+ "We recommend to use this if you want to upload signed "
+ "payment orders via EBICS.\nYou will be prompted to enter the "
+ "passphrase for every EBICS transaction, hence do not uncheck this "
+ "option on a userid for automated EBICS downloads.",
+ )
+ ebics_passphrase_required = fields.Boolean(
+ compute="_compute_ebics_passphrase_view_modifiers"
+ )
+ ebics_passphrase_invisible = fields.Boolean(
+ compute="_compute_ebics_passphrase_view_modifiers"
+ )
+ ebics_passphrase_store_readonly = fields.Boolean(
+ compute="_compute_ebics_passphrase_view_modifiers"
+ )
+ ebics_sig_passphrase = fields.Char(
+ string="EBICS Signature Passphrase",
+ help="You can set here a different passphrase for the EBICS "
+ "signing key. This passphrase will never be stored hence "
+ "you'll need to specify your passphrase for each transaction that "
+ "requires a digital signature.",
+ )
+ ebics_sig_passphrase_invisible = fields.Boolean(
+ compute="_compute_ebics_sig_passphrase_invisible"
+ )
+ ebics_ini_letter = fields.Binary(
+ string="EBICS INI Letter",
+ readonly=True,
+ help="INI-letter PDF document to be sent to your bank.",
+ )
+ ebics_ini_letter_fn = fields.Char(string="INI-letter Filename", readonly=True)
+ ebics_public_bank_keys = fields.Binary(
+ string="EBICS Public Bank Keys",
+ readonly=True,
+ help="EBICS Public Bank Keys to be checked for consistency.",
+ )
+ ebics_public_bank_keys_fn = fields.Char(
+ string="EBICS Public Bank Keys Filename", readonly=True
+ )
+ swift_3skey = fields.Boolean(
+ string="Enable 3SKey support",
+ help="Transactions for this user will be signed "
+ "by means of the SWIFT 3SKey token.",
+ )
+ swift_3skey_certificate = fields.Binary(string="3SKey Certficate")
+ swift_3skey_certificate_fn = fields.Char(string="3SKey Certificate Filename")
+ # X.509 Distinguished Name attributes used to
+ # create self-signed X.509 certificates
+ ebics_key_x509 = fields.Boolean(
+ string="X509 support",
+ help="Set this flag in order to work with " "self-signed X.509 certificates",
+ )
+ ebics_key_x509_dn_cn = fields.Char(
+ string="Common Name [CN]",
+ )
+ ebics_key_x509_dn_o = fields.Char(
+ string="Organization Name [O]",
+ )
+ ebics_key_x509_dn_ou = fields.Char(
+ string="Organizational Unit Name [OU]",
+ )
+ ebics_key_x509_dn_c = fields.Char(
+ string="Country Name [C]",
+ )
+ ebics_key_x509_dn_st = fields.Char(
+ string="State Or Province Name [ST]",
+ )
+ ebics_key_x509_dn_l = fields.Char(
+ string="Locality Name [L]",
+ )
+ ebics_key_x509_dn_e = fields.Char(
+ string="Email Address",
+ )
+ state = fields.Selection(
+ [
+ ("draft", "Draft"),
+ ("init", "Initialisation"),
+ ("get_bank_keys", "Get Keys from Bank"),
+ ("to_verify", "Verification"),
+ ("active_keys", "Active Keys"),
+ ],
+ default="draft",
+ required=True,
+ readonly=True,
+ )
+ active = fields.Boolean(default=True)
+ company_ids = fields.Many2many(
+ comodel_name="res.company",
+ string="Companies",
+ required=True,
+ help="Companies sharing this EBICS contract.",
+ )
+
+ @api.depends("name", "ebics_config_id.ebics_keys")
+ def _compute_ebics_keys_fn(self):
+ for rec in self:
+ keys_dir = rec.ebics_config_id.ebics_keys
+ rec.ebics_keys_fn = (
+ rec.name
+ and keys_dir
+ and (keys_dir + "/" + rec.name.replace(" ", "_") + "_keys")
+ )
+
+ @api.depends("ebics_keys_fn")
+ def _compute_ebics_keys_found(self):
+ for rec in self:
+ rec.ebics_keys_found = rec.ebics_keys_fn and os.path.isfile(
+ rec.ebics_keys_fn
+ )
+
+ @api.depends("state", "ebics_passphrase", "ebics_keys_found")
+ def _compute_ebics_passphrase_view_modifiers(self):
+ for rec in self:
+ rec.ebics_passphrase_required = False
+ rec.ebics_passphrase_invisible = True
+ rec.ebics_passphrase_store_readonly = True
+ if rec.state == "draft":
+ rec.ebics_passphrase_required = True
+ rec.ebics_passphrase_invisible = rec.ebics_keys_found and True or False
+ rec.ebics_passphrase_store_readonly = False
+ elif rec.state == "init":
+ rec.ebics_passphrase_required = False
+ rec.ebics_passphrase_invisible = True
+ elif rec.state in ("get_bank_keys", "to_verify"):
+ rec.ebics_passphrase_required = not rec.ebics_passphrase
+ rec.ebics_passphrase_invisible = rec.ebics_passphrase
+
+ @api.depends("state")
+ def _compute_ebics_sig_passphrase_invisible(self):
+ for rec in self:
+ rec.ebics_sig_passphrase_invisible = True
+ if fintech.__version_info__ < (7, 3, 1):
+ continue
+ if rec.transaction_rights != "down" and rec.state == "draft":
+ rec.ebics_sig_passphrase_invisible = False
+
+ @api.constrains("ebics_key_x509")
+ def _check_ebics_key_x509(self):
+ for cfg in self:
+ if cfg.ebics_version == "H005" and not cfg.ebics_key_x509:
+ raise UserError(
+ self.env._("X.509 certificates must be used with EBICS 3.0.")
+ )
+
+ @api.constrains("ebics_passphrase")
+ def _check_ebics_passphrase(self):
+ for rec in self:
+ if rec.ebics_passphrase and len(rec.ebics_passphrase) < 8:
+ raise UserError(
+ self.env._("The Passphrase must be at least 8 characters long")
+ )
+
+ @api.constrains("ebics_sig_passphrase")
+ def _check_ebics_sig_passphrase(self):
+ for rec in self:
+ if rec.ebics_sig_passphrase and len(rec.ebics_sig_passphrase) < 8:
+ raise UserError(
+ self.env._(
+ "The Signature Passphrase must be at least 8 characters long"
+ )
+ )
+
+ @api.onchange("ebics_version")
+ def _onchange_ebics_version(self):
+ if self.ebics_version == "H005":
+ self.ebics_key_x509 = True
+
+ @api.onchange("signature_class")
+ def _onchange_signature_class(self):
+ if self.signature_class == "T":
+ self.swift_3skey = False
+
+ @api.onchange("ebics_passphrase_store", "ebics_passphrase")
+ def _onchange_ebics_passphrase_store(self):
+ if self.ebics_passphrase_store:
+ if self.ebics_passphrase:
+ # check passphrase before db store
+ keyring_params = {
+ "keys": self.ebics_keys_fn,
+ "passphrase": self.ebics_passphrase,
+ }
+ keyring = EbicsKeyRing(**keyring_params)
+ try:
+ # fintech <= 7.4.3 does not have a call to check if a
+ # passphrase matches with the value stored in the keyfile.
+ # We get around this limitation as follows:
+ # Get user keys to check for valid passphrases
+ # It will raise a ValueError on invalid passphrases
+ keyring["#USER"]
+ except ValueError as err: # noqa: F841
+ raise UserError(self.env._("Passphrase mismatch.")) # noqa: B904
+ else:
+ if self.state != "draft":
+ self.ebics_passphrase = False
+
+ @api.onchange("swift_3skey")
+ def _onchange_swift_3skey(self):
+ if self.swift_3skey:
+ self.ebics_key_x509 = True
+
+ def set_to_draft(self):
+ return self.write({"state": "draft"})
+
+ def set_to_active_keys(self):
+ vals = {"state": "active_keys"}
+ self._update_passphrase_vals(vals)
+ return self.write(vals)
+
+ def set_to_get_bank_keys(self):
+ self.ensure_one()
+ if self.ebics_config_id.state != "draft":
+ raise UserError(
+ self.env._(
+ "Set the EBICS Configuation record to 'Draft' "
+ "before starting the Key Renewal process."
+ )
+ )
+ return self.write({"state": "get_bank_keys"})
+
+ def ebics_init_1(self): # noqa: C901
+ """
+ Initialization of bank keys - Step 1:
+ Create new keys and certificates for this user
+ """
+ self.ensure_one()
+ if self.state != "draft":
+ raise UserError(
+ self.env._("Set state to 'draft' before Bank Key (re)initialisation.")
+ )
+
+ if not self.ebics_passphrase:
+ raise UserError(self.env._("Set a passphrase."))
+
+ if self.swift_3skey and not self.swift_3skey_certificate:
+ raise UserError(self.env._("3SKey certificate missing."))
+
+ ebics_version = self.ebics_config_id.ebics_version
+ try:
+ keyring_params = {
+ "keys": self.ebics_keys_fn,
+ "passphrase": self.ebics_passphrase,
+ }
+ if self.ebics_sig_passphrase:
+ keyring_params["sig_passphrase"] = self.ebics_sig_passphrase
+ keyring = EbicsKeyRing(**keyring_params)
+ bank = EbicsBank(
+ keyring=keyring,
+ hostid=self.ebics_config_id.ebics_host,
+ url=self.ebics_config_id.ebics_url,
+ )
+ user = EbicsUser(
+ keyring=keyring,
+ partnerid=self.ebics_config_id.ebics_partner,
+ userid=self.name,
+ )
+ except Exception as err:
+ exctype, value = exc_info()[:2]
+ error = self.env._("EBICS Initialisation Error:")
+ error += "\n" + str(exctype) + "\n" + str(value)
+ raise UserError(error) from err
+
+ self.ebics_config_id._check_ebics_keys()
+ if not os.path.isfile(self.ebics_keys_fn):
+ try:
+ # TODO:
+ # enable import of all type of certicates: A00x, X002, E002
+ if self.swift_3skey:
+ kwargs = {
+ self.ebics_config_id.ebics_key_version: base64.decodebytes(
+ self.swift_3skey_certificate
+ ),
+ }
+ user.import_certificates(**kwargs)
+ user.create_keys(
+ keyversion=self.ebics_config_id.ebics_key_version,
+ bitlength=self.ebics_config_id.ebics_key_bitlength,
+ )
+ except Exception as err:
+ exctype, value = exc_info()[:2]
+ error = self.env._("EBICS Initialisation Error:")
+ error += "\n" + str(exctype) + "\n" + str(value)
+ raise UserError(error) from err
+
+ if self.swift_3skey and not self.ebics_key_x509:
+ raise UserError(
+ self.env._(
+ "The current version of this module "
+ "requires to X509 support when enabling 3SKey"
+ )
+ )
+
+ if self.ebics_key_x509:
+ dn_attrs = {
+ "commonName": self.ebics_key_x509_dn_cn,
+ "organizationName": self.ebics_key_x509_dn_o,
+ "organizationalUnitName": self.ebics_key_x509_dn_ou,
+ "countryName": self.ebics_key_x509_dn_c,
+ "stateOrProvinceName": self.ebics_key_x509_dn_st,
+ "localityName": self.ebics_key_x509_dn_l,
+ "emailAddress": self.ebics_key_x509_dn_e,
+ }
+ kwargs = {k: v for k, v in dn_attrs.items() if v}
+ user.create_certificates(**kwargs)
+
+ try:
+ client = EbicsClient(bank, user, version=ebics_version)
+ except RuntimeError as err:
+ e = exc_info()
+ error = self.env._("EBICS Initialization Error:")
+ error += "\n"
+ error += err.args[0]
+ raise UserError(error) from err
+
+ # Send the public electronic signature key to the bank.
+ ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
+ if not ebics_config_bank:
+ raise UserError(
+ self.env._(
+ "No bank defined for the financial journal " "of the EBICS Config"
+ )
+ )
+ try:
+ supported_versions = client.HEV()
+ if supported_versions and ebics_version not in supported_versions:
+ err_msg = self.env._("EBICS version mismatch.") + "\n"
+ err_msg += self.env._("Versions supported by your bank:")
+ for k in supported_versions:
+ err_msg += f"\n{k}: {supported_versions[k]} "
+ raise UserError(err_msg)
+ if ebics_version == "H003":
+ bank._order_number = self.ebics_config_id._get_order_number()
+ OrderID = client.INI()
+ _logger.info("%s, EBICS INI command, OrderID=%s", self._name, OrderID)
+ if ebics_version == "H003":
+ self.ebics_config_id._update_order_number(OrderID)
+ except URLError as err:
+ exctype, value = exc_info()[:2]
+ tb = "".join(format_exception(*exc_info()))
+ _logger.error(
+ "EBICS INI command error\nUserID: %s\n%s",
+ self.name,
+ tb,
+ )
+ raise UserError(
+ self.env._(
+ "urlopen error:\n url '%(url)s' - %(val)s",
+ url=self.ebics_config_id.ebics_url,
+ val=str(value),
+ )
+ ) from err
+ except EbicsFunctionalError as err:
+ e = exc_info()
+ error = self.env._("EBICS Functional Error:")
+ error += "\n"
+ error += f"{e[1].message} (code: {e[1].code})"
+ raise UserError(error) from err
+ except EbicsTechnicalError as err:
+ e = exc_info()
+ error = self.env._("EBICS Technical Error:")
+ error += "\n"
+ error += f"{e[1].message} (code: {e[1].code})"
+ raise UserError(error) from err
+
+ # Send the public authentication and encryption keys to the bank.
+ if ebics_version == "H003":
+ bank._order_number = self.ebics_config_id._get_order_number()
+ OrderID = client.HIA()
+ _logger.info("%s, EBICS HIA command, OrderID=%s", self._name, OrderID)
+ if ebics_version == "H003":
+ self.ebics_config_id._update_order_number(OrderID)
+
+ # Create an INI-letter which must be printed and sent to the bank.
+ ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
+ cc = ebics_config_bank.country.code
+ if cc in ["FR", "DE"]:
+ lang = cc
+ else:
+ lang = self.env.user.lang or self.env["res.lang"].search([])[0].code
+ lang = lang[:2]
+ fn_date = fields.Date.today().isoformat()
+ fn = "_".join([self.ebics_config_id.ebics_host, "ini_letter", fn_date]) + ".pdf"
+ letter = user.create_ini_letter(bankname=ebics_config_bank.name, lang=lang)
+ vals = {
+ "ebics_ini_letter": base64.encodebytes(letter),
+ "ebics_ini_letter_fn": fn,
+ "state": "init",
+ }
+ self._update_passphrase_vals(vals)
+ return self.write(vals)
+
+ def ebics_init_2(self):
+ """
+ Initialization of bank keys - Step 2:
+ Activation of the account by the bank.
+ """
+ self.ensure_one()
+ if self.state != "init":
+ raise UserError(self.env._("Set state to 'Initialisation'."))
+ vals = {"state": "get_bank_keys"}
+ self._update_passphrase_vals(vals)
+ return self.write(vals)
+
+ def ebics_init_3(self):
+ """
+ Initialization of bank keys - Step 3:
+
+ After the account has been activated the public bank keys
+ must be downloaded and checked for consistency.
+ """
+ self.ensure_one()
+ if self.state != "get_bank_keys":
+ raise UserError(self.env._("Set state to 'Get Keys from Bank'."))
+ try:
+ keyring = EbicsKeyRing(
+ keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
+ )
+ bank = EbicsBank(
+ keyring=keyring,
+ hostid=self.ebics_config_id.ebics_host,
+ url=self.ebics_config_id.ebics_url,
+ )
+ user = EbicsUser(
+ keyring=keyring,
+ partnerid=self.ebics_config_id.ebics_partner,
+ userid=self.name,
+ )
+ client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
+ except Exception as err:
+ exctype, value = exc_info()[:2]
+ error = self.env._("EBICS Initialisation Error:")
+ error += "\n" + str(exctype) + "\n" + str(value)
+ raise UserError(error) from err
+
+ try:
+ public_bank_keys = client.HPB()
+ except EbicsFunctionalError as err:
+ e = exc_info()
+ error = self.env._("EBICS Functional Error:")
+ error += "\n"
+ error += f"{e[1].message} (code: {e[1].code})"
+ raise UserError(error) from err
+ except Exception as err:
+ exctype, value = exc_info()[:2]
+ error = self.env._("EBICS Initialisation Error:")
+ error += "\n" + str(exctype) + "\n" + str(value)
+ raise UserError(error) from err
+
+ public_bank_keys = public_bank_keys.encode()
+ fn_date = fields.Date.today().isoformat()
+ fn = (
+ "_".join([self.ebics_config_id.ebics_host, "public_bank_keys", fn_date])
+ + ".txt"
+ )
+ vals = {
+ "ebics_public_bank_keys": base64.encodebytes(public_bank_keys),
+ "ebics_public_bank_keys_fn": fn,
+ "state": "to_verify",
+ }
+ self._update_passphrase_vals(vals)
+ return self.write(vals)
+
+ def ebics_init_4(self):
+ """
+ Initialization of bank keys - Step 2:
+ Confirm Verification of the public bank keys
+ and activate the bank keys.
+ """
+ self.ensure_one()
+ if self.state != "to_verify":
+ raise UserError(self.env._("Set state to 'Verification'."))
+
+ keyring = EbicsKeyRing(
+ keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
+ )
+ bank = EbicsBank(
+ keyring=keyring,
+ hostid=self.ebics_config_id.ebics_host,
+ url=self.ebics_config_id.ebics_url,
+ )
+ bank.activate_keys()
+ vals = {"state": "active_keys"}
+ self._update_passphrase_vals(vals)
+ return self.write(vals)
+
+ def change_passphrase(self):
+ self.ensure_one()
+ ctx = dict(self.env.context, default_ebics_userid_id=self.id)
+ module = __name__.split("addons.")[1].split(".")[0]
+ view = self.env.ref("%s.ebics_change_passphrase_view_form" % module)
+ return {
+ "name": self.env._("EBICS keys change passphrase"),
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.change.passphrase",
+ "view_id": view.id,
+ "target": "new",
+ "context": ctx,
+ "type": "ir.actions.act_window",
+ }
+
+ def _update_passphrase_vals(self, vals):
+ """
+ Remove non-stored passphrases from db after e.g. successfull init_1
+ """
+ if vals["state"] in ("init", "get_bank_keys", "to_verify", "active_keys"):
+ if not self.ebics_passphrase_store:
+ vals["ebics_passphrase"] = False
+ if self.ebics_sig_passphrase:
+ vals["ebics_sig_passphrase"] = False
diff --git a/account_ebics/models/fintech_ebics_register.py b/account_ebics/models/fintech_ebics_register.py
new file mode 100644
index 0000000..ed6be8e
--- /dev/null
+++ b/account_ebics/models/fintech_ebics_register.py
@@ -0,0 +1,46 @@
+# Copyright 2009-2020 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import logging
+from sys import exc_info
+from traceback import format_exception
+
+from odoo.tools import config
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import fintech
+except ImportError:
+ fintech = None
+ _logger.warning("Failed to import fintech")
+
+fintech_register_name = config.get("fintech_register_name")
+fintech_register_keycode = config.get("fintech_register_keycode")
+fintech_register_users = config.get("fintech_register_users")
+
+try:
+ if fintech:
+ fintech_register_users = (
+ fintech_register_users
+ and [x.strip() for x in fintech_register_users.split(",")]
+ or None
+ )
+ fintech.cryptolib = "cryptography"
+ fintech.register(
+ name=fintech_register_name,
+ keycode=fintech_register_keycode,
+ users=fintech_register_users,
+ )
+except RuntimeError as e:
+ if str(e) == "'register' can be called only once":
+ pass
+ else:
+ _logger.error(str(e))
+ fintech.register()
+except Exception:
+ msg = "fintech.register error"
+ tb = "".join(format_exception(*exc_info()))
+ msg += "\n%s" % tb
+ _logger.error(msg)
+ fintech.register()
diff --git a/account_ebics/pyproject.toml b/account_ebics/pyproject.toml
new file mode 100644
index 0000000..4231d0c
--- /dev/null
+++ b/account_ebics/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/account_ebics/security/ebics_security.xml b/account_ebics/security/ebics_security.xml
new file mode 100644
index 0000000..7b11eac
--- /dev/null
+++ b/account_ebics/security/ebics_security.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ EBICS Manager
+
+
+
+
+
+
+ EBICS Configuration model company rule
+
+
+ ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]
+
+
+
+ EBICS UserID model company rule
+
+
+ ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]
+
+
+
+ EBICS File model company rule
+
+
+ ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]
+
+
+
+
+
diff --git a/account_ebics/security/ir.model.access.csv b/account_ebics/security/ir.model.access.csv
new file mode 100644
index 0000000..97d1f3f
--- /dev/null
+++ b/account_ebics/security/ir.model.access.csv
@@ -0,0 +1,13 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_ebics_config_manager,ebics_config manager,model_ebics_config,group_ebics_manager,1,1,1,1
+access_ebics_config_user,ebics_config user,model_ebics_config,account.group_account_invoice,1,0,0,0
+access_ebics_userid_manager,ebics_userid manager,model_ebics_userid,group_ebics_manager,1,1,1,1
+access_ebics_userid_user,ebics_userid user,model_ebics_userid,account.group_account_invoice,1,0,0,0
+access_ebics_file_format_manager,ebics_file_format manager,model_ebics_file_format,group_ebics_manager,1,1,1,1
+access_ebics_file_format_user,ebics_file_format user,model_ebics_file_format,account.group_account_invoice,1,0,0,0
+access_ebics_file_manager,ebics_file manager,model_ebics_file,group_ebics_manager,1,1,1,1
+access_ebics_file_user,ebics_file user,model_ebics_file,account.group_account_invoice,1,1,1,0
+
+access_ebics_change_passphrase,access_ebics_change_passphrase,model_ebics_change_passphrase,group_ebics_manager,1,1,1,0
+access_ebics_xfer,access_ebics_xfer,model_ebics_xfer,account.group_account_invoice,1,1,1,0
+access_ebics_admin_order,access_ebics_admin_order,model_ebics_admin_order,group_ebics_manager,1,1,1,0
diff --git a/account_ebics/static/description/cover.png b/account_ebics/static/description/cover.png
new file mode 100644
index 0000000..41c4cec
Binary files /dev/null and b/account_ebics/static/description/cover.png differ
diff --git a/account_ebics/static/description/icon.png b/account_ebics/static/description/icon.png
new file mode 100644
index 0000000..889d129
Binary files /dev/null and b/account_ebics/static/description/icon.png differ
diff --git a/account_ebics/static/description/index.html b/account_ebics/static/description/index.html
new file mode 100644
index 0000000..85f8856
--- /dev/null
+++ b/account_ebics/static/description/index.html
@@ -0,0 +1,573 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
EBICS banking protocol
+
Implementation of the EBICS banking protocol.
+
This module facilitates the exchange of files with banks via the EBICS protocol.
+
+
+
Installation
+
The module depends upon
+
+
Remark:
+
The EBICS 'Test Mode' for uploading orders requires fintech 4.3.4 or higher for EBICS 2.x
+and fintech 7.2.7 or higher for EBICS 3.0.
+
SWIFT 3SKey support requires fintech 6.4 or higher.
+
+
We also recommend to consider the installation of the following modules:
+
+
+
+
+
+
+
+
+account_usability
+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.
+Cf. https://github.com/OCA/account-financial-tools
+
+
+
+
+
+
+
+
+
+
+
+
+
Fintech license
+
If you have a valid Fintech.ebics license, you should add the following
+licensing parameters to the odoo server configuration file:
+
+
The name of the licensee.
+
+fintech_register_keycode
+
+
The keycode of the licensed version.
+
+
+; fintech
+fintech_register_name = MyCompany
+fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
+
+
+
Cf. https://www.joonis.de/en/fintech/prices/
+
+
+
+
+
Configuration
+
Go to Settings > Users
+
Add the users that are authorised to maintain the EBICS configuration to the 'EBICS Manager' Group.
+
+
Go to Accounting > Configuration > Miscellaneous > EBICS > EBICS File Formats
+
Check if the EBICS File formats that you want to process in Odoo are defined.
+
Most commonly used formats for which support is available in Odoo should be there already.
+
Please open an issue on https://github.com/Noviat/account_ebics to report missing EBICS File Formats.
+
For File Formats of type 'Downloads' you can also specify a 'Download Process Method'.
+
This is the method that will be executed when hitting the 'Process' button on the downloaded file.
+
The following methods are currently available:
+
+cfonb120
+camt.053
+camt.052
+camt.054
+
+
All these methods require complimentary modules to be installed (cf. Installation section supra).
+
You'll get an error message when the required module is not installed on your Odoo instance.
+
+
Go to Accounting > Configuration > Miscellaneous > EBICS > EBICS Configuration
+
Configure your EBICS configuration according to the contract with your bank.
+
+
+
+
Usage
+
Go to Accounting > Bank and Cash > EBICS Processing
+
+
+
+
Diagnostics
+
Add the following to your Odoo config file in order to diagnose
+issues with the EBICS connection with your bank:
+
log_handler = fintech.ebics:DEBUG
+
+
+
EBICS Return Codes
+
During the processing of your EBICS upload/download, your bank may return an Error Code, e.g.
+
EBICS Functional Error:
+EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 90005)
+
A detailed 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).
+
+
+
+
Electronic Distributed Signature (EDS)
+
This is supported via external signing apps, e.g. BankingVEU:
+
+
+
+
+
Known Issues / Roadmap
+
+The end user is currently not able to change his passphrases (only the users with 'EBICS Manager' rights can do so).
+Add support to import externally generated keys & certificates (currently only 3SKey signature certificate).
+Add support for SWIFT 3SKey signing javascript lib (SConnect, cf https://www2.swift.com/3skey/help/sconnect.html ).
+
+
+
+
+
+
diff --git a/account_ebics/views/ebics_config_views.xml b/account_ebics/views/ebics_config_views.xml
new file mode 100644
index 0000000..7f15850
--- /dev/null
+++ b/account_ebics/views/ebics_config_views.xml
@@ -0,0 +1,92 @@
+
+
+
+
+ ebics.config.list
+ ebics.config
+
+
+
+
+
+
+
+
+
+
+
+ ebics.config.form
+ ebics.config
+
+
+
+
+
+
+ EBICS Configuration
+ ebics.config
+ list,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..7699a69
--- /dev/null
+++ b/account_ebics/views/ebics_file_format_views.xml
@@ -0,0 +1,74 @@
+
+
+
+
+ ebics.file.format.list
+ ebics.file.format
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.format.form
+ ebics.file.format
+
+
+
+
+
+
+ EBICS File Formats
+ ebics.file.format
+ list,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..465ccbb
--- /dev/null
+++ b/account_ebics/views/ebics_file_views.xml
@@ -0,0 +1,253 @@
+
+
+
+
+ ebics.file.search
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.list
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.form
+ ebics.file
+ 1
+
+
+
+
+
+
+ ebics.file.process.result
+ ebics.file
+ 2
+
+
+
+
+
+
+ EBICS Download Files
+ ir.actions.act_window
+ ebics.file
+ list,form
+
+ [('type','=','down')]
+
+
+
+
+
+ list
+
+
+
+
+
+
+ form
+
+
+
+
+
+
+
+ ebics.file.list
+ ebics.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ebics.file.form
+ ebics.file
+ 1
+
+
+
+
+
+
+ EBICS Upload Files
+ ir.actions.act_window
+ ebics.file
+ list,form
+
+ [('type','=','up')]
+
+
+
+
+
+ list
+
+
+
+
+
+
+ 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..03ef2c7
--- /dev/null
+++ b/account_ebics/views/ebics_userid_views.xml
@@ -0,0 +1,164 @@
+
+
+
+
+ ebics.userid.list
+ 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..1ad1b0c
--- /dev/null
+++ b/account_ebics/views/menu.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_ebics/wizards/__init__.py b/account_ebics/wizards/__init__.py
new file mode 100644
index 0000000..55db4e9
--- /dev/null
+++ b/account_ebics/wizards/__init__.py
@@ -0,0 +1,3 @@
+from . import ebics_change_passphrase
+from . import ebics_xfer
+from . import ebics_admin_order
diff --git a/account_ebics/wizards/ebics_admin_order.py b/account_ebics/wizards/ebics_admin_order.py
new file mode 100644
index 0000000..8d4b9a9
--- /dev/null
+++ b/account_ebics/wizards/ebics_admin_order.py
@@ -0,0 +1,52 @@
+# Copyright 2009-2023 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import pprint
+
+from odoo import api, fields, models
+
+
+class EbicsAdminOrder(models.TransientModel):
+ _inherit = "ebics.xfer"
+ _name = "ebics.admin.order"
+ _description = "EBICS Administrative Order"
+
+ admin_order_type = fields.Selection(
+ selection=lambda self: self._selection_admin_order_type(),
+ string="Order",
+ )
+
+ @api.model
+ def _selection_admin_order_type(self):
+ return [
+ ("HAA", "HAA - Business transaction formats BTF"),
+ ("HPD", "HPD - Bank parameters"),
+ ("HKD", "HKD - Subscriber information"),
+ ("HTD", "HTD - Customer properties and settings"),
+ ]
+
+ def ebics_admin_order(self):
+ self.ensure_one()
+ client = self._setup_client()
+ if not client:
+ self.note += (
+ self.env._("EBICS client setup failed for connection '%s'")
+ % self.ebics_config_id.name
+ )
+ else:
+ data = getattr(client, self.admin_order_type)(parsed=True)
+ pp = pprint.PrettyPrinter()
+ self.note = pp.pformat(data)
+ module = __name__.split("addons.")[1].split(".")[0]
+ result_view = self.env.ref("%s.ebics_admin_order_view_form_result" % module)
+ return {
+ "name": self.env._("EBICS Administrative Order result"),
+ "res_id": self.id,
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "ebics.admin.order",
+ "view_id": result_view.id,
+ "target": "new",
+ "context": self.env.context,
+ "type": "ir.actions.act_window",
+ }
diff --git a/account_ebics/wizards/ebics_admin_order.xml b/account_ebics/wizards/ebics_admin_order.xml
new file mode 100644
index 0000000..1c39f4d
--- /dev/null
+++ b/account_ebics/wizards/ebics_admin_order.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ EBICS Administrative Order
+ ebics.admin.order
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+
+
+
+ ebics_admin_order
+ Execute
+
+
+
+
+
+ EBICS Administrative Order result
+ ebics.admin.order
+ 2
+
+
+
+
+
+
+ EBICS Administrative Order
+ ir.actions.act_window
+ ebics.admin.order
+ form
+ new
+
+
+
+
diff --git a/account_ebics/wizards/ebics_change_passphrase.py b/account_ebics/wizards/ebics_change_passphrase.py
new file mode 100644
index 0000000..728dd0c
--- /dev/null
+++ b/account_ebics/wizards/ebics_change_passphrase.py
@@ -0,0 +1,122 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+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")
+ new_pass = fields.Char(string="New Passphrase")
+ new_pass_check = fields.Char(string="New Passphrase (verification)")
+ old_sig_pass = fields.Char(string="Old Signature Passphrase")
+ new_sig_pass = fields.Char(string="New Signature Passphrase")
+ new_sig_pass_check = fields.Char(string="New Signature Passphrase (verification)")
+ ebics_sig_passphrase_invisible = fields.Boolean(
+ compute="_compute_ebics_sig_passphrase_invisible"
+ )
+ note = fields.Text(string="Notes", readonly=True)
+
+ def _compute_ebics_sig_passphrase_invisible(self):
+ for rec in self:
+ if fintech.__version_info__ < (7, 3, 1):
+ rec.ebics_sig_passphrase_invisible = True
+ else:
+ rec.ebics_sig_passphrase_invisible = False
+
+ def change_passphrase(self):
+ self.ensure_one()
+ self.note = ""
+ if (
+ self.ebics_userid_id.ebics_passphrase_store
+ and self.old_pass
+ and self.old_pass != self.ebics_userid_id.ebics_passphrase
+ ):
+ raise UserError(self.env._("Incorrect old passphrase."))
+ if self.new_pass != self.new_pass_check:
+ raise UserError(self.env._("New passphrase verification error."))
+ if self.new_pass and self.new_pass == self.ebics_userid_id.ebics_passphrase:
+ raise UserError(self.env._("New passphrase equal to old passphrase."))
+ if (
+ self.new_sig_pass
+ and self.old_sig_pass
+ and self.new_sig_pass == self.old_sig_pass
+ ):
+ raise UserError(
+ self.env._(
+ "New signature passphrase equal to old signature passphrase."
+ )
+ )
+ if self.new_sig_pass != self.new_sig_pass_check:
+ raise UserError(self.env._("New signature passphrase verification error."))
+ passphrase = (
+ self.ebics_userid_id.ebics_passphrase_store
+ and self.ebics_userid_id.ebics_passphrase
+ or self.old_pass
+ )
+ try:
+ keyring_params = {
+ "keys": self.ebics_userid_id.ebics_keys_fn,
+ "passphrase": passphrase,
+ }
+ if self.new_sig_pass:
+ keyring_params["sig_passphrase"] = self.old_sig_pass or None
+ keyring = EbicsKeyRing(**keyring_params)
+ change_params = {}
+ if self.new_pass:
+ change_params["passphrase"] = self.new_pass
+ if self.new_sig_pass:
+ change_params["sig_passphrase"] = self.new_sig_pass
+ if change_params:
+ keyring.change_passphrase(**change_params)
+ except (ValueError, RuntimeError) as err:
+ raise UserError(str(err)) from err
+
+ if self.new_pass:
+ self.ebics_userid_id.ebics_passphrase = (
+ self.ebics_userid_id.ebics_passphrase_store and self.new_pass
+ )
+ self.note += "The EBICS Passphrase has been changed."
+ if self.new_sig_pass:
+ # removing ebics_sig_passphrase from db should not be required
+ # but we do it for double safety
+ if self.ebics_userid_id.ebics_sig_passphrase:
+ self.ebics_userid_id.ebics_sig_passphrase = False
+ self.note += "The EBICS Signature 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": self.env._("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..b556072
--- /dev/null
+++ b/account_ebics/wizards/ebics_change_passphrase.xml
@@ -0,0 +1,61 @@
+
+
+
+
+ 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..456df24
--- /dev/null
+++ b/account_ebics/wizards/ebics_xfer.py
@@ -0,0 +1,682 @@
+# Copyright 2009-2024 Noviat.
+# License LGPL-3 or later (http://www.gnu.org/licenses/lgpl).
+
+import base64
+import logging
+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")
+ ebics_passphrase_stored = fields.Char(
+ string="EBICS Stored Passphrase", related="ebics_userid_id.ebics_passphrase"
+ )
+ ebics_passphrase_store = fields.Boolean(
+ related="ebics_userid_id.ebics_passphrase_store"
+ )
+ ebics_sig_passphrase = fields.Char(
+ string="EBICS Signature Passphrase",
+ )
+ ebics_sig_passphrase_invisible = fields.Boolean(
+ compute="_compute_ebics_sig_passphrase_invisible"
+ )
+ 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="Dummy 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.",
+ )
+ upload_format_ids = fields.Many2many(
+ comodel_name="ebics.file.format", compute="_compute_upload_format_ids"
+ )
+ 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
+
+ def _compute_ebics_sig_passphrase_invisible(self):
+ for rec in self:
+ rec.ebics_sig_passphrase_invisible = True
+ if fintech.__version_info__ < (7, 3, 1):
+ rec.ebics_sig_passphrase_invisible = True
+ else:
+ rec.ebics_sig_passphrase_invisible = False
+
+ @api.depends("ebics_config_id")
+ def _compute_upload_format_ids(self):
+ for rec in self:
+ rec.upload_format_ids = False
+ if not self.env.context.get("ebics_download"):
+ rec.upload_format_ids = (
+ rec.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "up"
+ )
+ )
+
+ @api.onchange("ebics_config_id")
+ def _onchange_ebics_config_id(self):
+ avail_userids = self.ebics_config_id.ebics_userid_ids.filtered(
+ lambda r: self.env.user.id in r.user_ids.ids
+ )
+
+ if self.env.context.get("ebics_download"): # Download Form
+ avail_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "down"
+ )
+ if avail_formats and len(avail_formats) == 1:
+ self.format_id = avail_formats
+ else:
+ self.format_id = False
+ avail_userids = avail_userids.filtered(
+ lambda r: r.transaction_rights in ["both", "down"]
+ )
+ else: # Upload Form
+ if not self.env.context.get("active_model") == "account.payment.order":
+ avail_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
+ lambda r: r.type == "up"
+ )
+ if avail_formats and len(avail_formats) == 1:
+ self.format_id = avail_formats
+ else:
+ self.format_id = False
+ avail_userids = avail_userids.filtered(
+ lambda r: r.transaction_rights in ["both", "up"]
+ )
+
+ if avail_userids:
+ if len(avail_userids) == 1:
+ self.ebics_userid_id = avail_userids
+ else:
+ with_passphrase_userids = avail_userids.filtered(
+ lambda r: r.ebics_passphrase_store
+ )
+ if len(with_passphrase_userids) == 1:
+ self.ebics_userid_id = with_passphrase_userids
+ else:
+ self.ebics_userid_id = False
+
+ @api.onchange("upload_data")
+ def _onchange_upload_data(self):
+ if self.env.context.get("active_model") == "account.payment.order":
+ return
+ 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 or "")
+ )
+ if len(upload_formats) == 1:
+ self.format_id = upload_formats
+
+ 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": self.env._("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()
+ ctx = self.env.context.copy()
+ self.note = ""
+ err_cnt = 0
+ client = self._setup_client()
+ if not client:
+ err_cnt += 1
+ self.note += (
+ self.env._("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 += self.env._(
+ "EBICS Functional Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name or df.description,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += f"{e[1].message} (code: {e[1].code})"
+ except EbicsTechnicalError:
+ err_cnt += 1
+ e = exc_info()
+ self.note += "\n"
+ self.note += self.env._(
+ "EBICS Technical Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name or df.description,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += f"{e[1].message} (code: {e[1].code})"
+ except EbicsVerificationError:
+ err_cnt += 1
+ self.note += "\n"
+ self.note += self.env._(
+ "EBICS Verification Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name or df.description,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += self.env._("The EBICS response could not be verified.")
+ except UserError as e:
+ err_cnt += 1
+ self.note += "\n"
+ self.note += self.env._(
+ "Error detected during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name or df.description,
+ order_type=df.order_type,
+ )
+ self.note += "\n"
+ self.note += " ".join(e.args)
+ except Exception:
+ err_cnt += 1
+ self.note += "\n"
+ self.note += self.env._(
+ "Unknown Error during download of "
+ "File Format %(name)s (%(order_type)s):",
+ name=df.name or df.description,
+ 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 += (
+ self.env._(
+ "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": self.env._("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 view_ebics_file(self):
+ self.ensure_one()
+ module = __name__.split("addons.")[1].split(".")[0]
+ act = self.env["ir.actions.act_window"]._for_xml_id(
+ f"{module}.ebics_file_action_download"
+ )
+ 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 += (
+ self.env._("EBICS File has been uploaded (OrderID %s).")
+ % OrderID
+ )
+ ef_note = self.env._("EBICS OrderID: %s") % OrderID
+ if self.env.context.get("origin"):
+ ef_note += (
+ "\n" + self.env._("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 += self.env._("EBICS Functional Error:")
+ self.note += "\n"
+ self.note += f"{e[1].message} (code: {e[1].code})"
+ except EbicsTechnicalError:
+ e = exc_info()
+ self.note += "\n"
+ self.note += self.env._("EBICS Technical Error:")
+ self.note += "\n"
+ self.note += f"{e[1].message} (code: {e[1].code})"
+ except EbicsVerificationError:
+ self.note += "\n"
+ self.note += self.env._("EBICS Verification Error:")
+ self.note += "\n"
+ self.note += self.env._("The EBICS response could not be verified.")
+ except Exception:
+ self.note += "\n"
+ self.note += self.env._("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)
+
+ ebics_file and self._payment_order_postprocess(ebics_file)
+ return ebics_file
+
+ def _payment_order_postprocess(self, ebics_file):
+ active_model = self.env.context.get("model")
+ if active_model == "account.payment.order":
+ order = self.env["account.payment.order"].browse(
+ self.env.context["active_id"]
+ )
+ order.generated2uploaded()
+
+ def _setup_client(self):
+ self.ebics_config_id._check_ebics_keys()
+ passphrase = self._get_passphrase()
+ keyring_params = {
+ "keys": self.ebics_userid_id.ebics_keys_fn,
+ "passphrase": passphrase,
+ }
+ if self.ebics_sig_passphrase:
+ keyring_params["sig_passphrase"] = self.ebics_sig_passphrase
+ try:
+ keyring = EbicsKeyRing(**keyring_params)
+ except (RuntimeError, ValueError) as err:
+ error = self.env._("Error while accessing the EBICS Keys:")
+ error += "\n"
+ error += err.args[0]
+ raise UserError(error) from err
+
+ 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()
+
+ signature_class = (
+ self.format_id.signature_class or self.ebics_userid_id.signature_class
+ )
+
+ user_params = {
+ "keyring": keyring,
+ "partnerid": self.ebics_config_id.ebics_partner,
+ "userid": self.ebics_userid_id.name,
+ }
+ # manual_approval replaced by transport_only class param in fintech 7.4
+ fintech74 = hasattr(EbicsUser, "transport_only")
+ if fintech74:
+ user_params["transport_only"] = signature_class == "T" and True or False
+ try:
+ user = EbicsUser(**user_params)
+ except ValueError as err:
+ error = self.env._("Error while accessing the EBICS UserID:")
+ error += "\n"
+ err_str = err.args[0]
+ error += err.args[0]
+ if err_str == "unknown key format":
+ error += "\n"
+ error += self.env._(
+ "Doublecheck your EBICS Passphrase and UserID settings."
+ )
+ raise UserError(error) from err
+ # manual_approval replaced by transport_only class param in fintech 7.4
+ if not fintech74 and 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 += self.env._("Unknown Error")
+ tb = "".join(format_exception(*exc_info()))
+ self.note += "\n%s" % tb
+ client = False
+
+ return client
+
+ def _get_passphrase(self):
+ return self.ebics_passphrase or self.ebics_passphrase_stored
+
+ 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):
+ 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())
+ fn = "_".join(fn_parts)
+ ff_methods = self._file_format_methods()
+ if file_format.name in ff_methods:
+ data = ff_methods[file_format.name](data)
+
+ 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(
+ self.env._(
+ "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..f763894
--- /dev/null
+++ b/account_ebics/wizards/ebics_xfer.xml
@@ -0,0 +1,166 @@
+
+
+
+
+ EBICS File Download
+ ebics.xfer
+ 1
+
+
+
+
+
+
+ EBICS File Upload
+ ebics.xfer
+ 1
+
+
+
+
+
+
+ EBICS File Transfer
+ ebics.xfer
+ 2
+
+
+
+
+
+
+ EBICS File Transfer - Download
+ ir.actions.act_window
+ ebics.xfer
+ form
+ new
+ {'ebics_download': 1}
+
+
+
+
+ EBICS File Transfer - Upload
+ ir.actions.act_window
+ ebics.xfer
+ form
+ new
+ {'ebics_upload': 1}
+
+
+
+