Merge pull request #66 from Noviat/16.0-mig-account_ebics

16.0 mig account ebics
This commit is contained in:
Luc De Meyer 2023-02-12 20:41:18 +01:00 committed by GitHub
commit eb09553769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 5461 additions and 11 deletions

View File

@ -14,10 +14,10 @@ odoo_version: 16.0
org_name: Noviat org_name: Noviat
org_slug: Noviat org_slug: Noviat
rebel_module_groups: [] rebel_module_groups: []
repo_description: 'Noviat Generic - Accounting Repository' repo_description: 'EBICS support for Odoo'
repo_name: accounting repo_name: account_ebics
repo_slug: accounting repo_slug: account_ebics
repo_website: https://picasso.noviat.com/Noviat/Noviat_Generic/accounting/ repo_website: https://www.noviat.com
travis_apt_packages: [] travis_apt_packages: []
travis_apt_sources: [] travis_apt_sources: []

View File

@ -1,7 +1,8 @@
exclude: | exclude: |
(?x) (?x)
# NOT INSTALLABLE ADDONS # NOT INSTALLABLE ADDONS
^server_environment_files/| ^account_ebics_oca_statement_import/|
^account_ebics_payment_order/|
# END NOT INSTALLABLE ADDONS # END NOT INSTALLABLE ADDONS
# Files and folders generated by bots, to avoid loops # Files and folders generated by bots, to avoid loops
^setup/|/static/description/index\.html$| ^setup/|/static/description/index\.html$|
@ -19,7 +20,7 @@ exclude: |
/TODO/ /TODO/
default_language_version: default_language_version:
python: python3 python: python3
node: "16.10.0" node: "16.17.0"
repos: repos:
- repo: local - repo: local
hooks: hooks:
@ -36,7 +37,7 @@ repos:
# update the NOT INSTALLABLE ADDONS section above # update the NOT INSTALLABLE ADDONS section above
- id: oca-update-pre-commit-excluded-addons - id: oca-update-pre-commit-excluded-addons
- id: oca-fix-manifest-website - id: oca-fix-manifest-website
args: ["https://www.noviat.com/"] args: ["https://www.noviat.com"]
- repo: https://github.com/myint/autoflake - repo: https://github.com/myint/autoflake
rev: v1.7.7 rev: v1.7.7
hooks: hooks:
@ -118,7 +119,7 @@ repos:
name: flake8 name: flake8
additional_dependencies: ["flake8-bugbear==20.1.4"] additional_dependencies: ["flake8-bugbear==20.1.4"]
- repo: https://github.com/OCA/pylint-odoo - repo: https://github.com/OCA/pylint-odoo
rev: v8.0.13 rev: 7.0.2
hooks: hooks:
- id: pylint_odoo - id: pylint_odoo
name: pylint with optional checks name: pylint with optional checks

View File

@ -67,6 +67,39 @@ enable=anomalous-backslash-in-string,
use-vim-comment, use-vim-comment,
wrong-tabs-instead-of-spaces, wrong-tabs-instead-of-spaces,
xml-syntax-error, xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-readme,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout,
# messages that do not cause the lint step to fail # messages that do not cause the lint step to fail
consider-merging-classes-inherited, consider-merging-classes-inherited,
create-user-wo-reset-password, create-user-wo-reset-password,

View File

@ -41,9 +41,8 @@ enable=anomalous-backslash-in-string,
method-inverse, method-inverse,
method-required-super, method-required-super,
method-search, method-search,
missing-import-error,
missing-manifest-dependency,
openerp-exception-warning, openerp-exception-warning,
pointless-statement,
pointless-string-statement, pointless-string-statement,
print-used, print-used,
redundant-keyword-arg, redundant-keyword-arg,
@ -59,7 +58,40 @@ enable=anomalous-backslash-in-string,
unreachable, unreachable,
use-vim-comment, use-vim-comment,
wrong-tabs-instead-of-spaces, wrong-tabs-instead-of-spaces,
xml-syntax-error xml-syntax-error,
attribute-string-redundant,
character-not-valid-in-resource-link,
consider-merging-classes-inherited,
context-overridden,
create-user-wo-reset-password,
dangerous-filter-wo-user,
dangerous-qweb-replace-wo-priority,
deprecated-data-xml-node,
deprecated-openerp-xml-node,
duplicate-po-message-definition,
except-pass,
file-not-used,
invalid-commit,
manifest-maintainers-list,
missing-newline-extrafiles,
missing-readme,
missing-return,
odoo-addons-relative-import,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
renamed-field-parameter,
resource-not-exist,
str-format-used,
test-folder-imported,
translation-contains-variable,
translation-positional-used,
unnecessary-utf8-coding-comment,
website-manifest-key-not-valid-uri,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
external-request-timeout
[REPORTS] [REPORTS]
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}

218
account_ebics/README.rst Normal file
View File

@ -0,0 +1,218 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: https://www.gnu.org/licenses/lpgl
: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
Recommended if you are using the OCA account_payment_order module.
Cf. https://github.com/Noviat/account_ebics and https://github.com/OCA/bank-payment
|
- 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
|
- account_statement_import_helper
Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files)
and using import parsers based upon the OCA account_statement_import module.
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
Cf. https://github.com/Noviat/noviat-apps
|
- account_bank_statement_import_helper
Required if you are processing bank statements with local bank account numbers
and using import parsers based upon the Odoo Enterprise account_bank_statement_import module.
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
Cf. https://github.com/Noviat/noviat-apps
|
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.
- fintech_register_users
The licensed EBICS user ids. It must be a string or a list of user ids.
You should NOT specify this parameter if your license is subsciption
based (with monthly recurring billing).
|
| Example:
|
::
; fintech
fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
fintech_register_users = USER1, USER2
|
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 specifiy 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**
|
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 detailled explanation of the codes can be found on http://www.ebics.org.
You can also find this information in the doc folder of this module (file EBICS_Annex1_ReturnCodes).
|
Known Issues / Roadmap
======================
- add support to import externally generated keys & certificates (currently only 3SKey signature certificate)
- For Odoo 16.0 the interaction with the OCA payment order and bank statement import modules (e.g. french CFONB) is not yet available.

View File

@ -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")

View File

@ -0,0 +1,32 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
"name": "EBICS banking protocol",
"version": "16.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",
"views/menu.xml",
],
"installable": True,
"application": True,
"external_dependencies": {
"python": [
"fintech",
"cryptography",
]
},
}

View File

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

Binary file not shown.

View File

@ -0,0 +1,22 @@
<?xml version='1.0' encoding='utf-8' ?>
<odoo>
<record id="ebics_config_comp_rule" model="ir.rule">
<field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
</odoo>

View File

@ -0,0 +1,83 @@
# 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"
)

View File

@ -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;")

View File

@ -0,0 +1,44 @@
# 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]
)
)

View File

@ -0,0 +1,81 @@
# 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

View File

@ -0,0 +1,54 @@
# Copyright 2009-2022 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
def migrate(cr, version):
if not version:
return
cr.execute("select id from ebics_config")
cfg_ids = [x[0] for x in cr.fetchall()]
for cfg_id in cfg_ids:
cr.execute(
"""
SELECT DISTINCT aj.company_id
FROM account_journal_ebics_config_rel rel
JOIN account_journal aj ON rel.account_journal_id = aj.id
WHERE ebics_config_id = %s
""",
(cfg_id,),
)
new_cpy_ids = [x[0] for x in cr.fetchall()]
cr.execute(
"""
SELECT DISTINCT res_company_id
FROM ebics_config_res_company_rel
WHERE ebics_config_id = %s
""",
(cfg_id,),
)
old_cpy_ids = [x[0] for x in cr.fetchall()]
to_add = []
for cid in new_cpy_ids:
if cid in old_cpy_ids:
old_cpy_ids.remove(cid)
else:
to_add.append(cid)
if old_cpy_ids:
cr.execute(
"""
DELETE FROM ebics_config_res_company_rel
WHERE res_company_id IN %s
""",
(tuple(old_cpy_ids),),
)
if to_add:
for cid in to_add:
cr.execute(
"""
INSERT INTO ebics_config_res_company_rel(ebics_config_id, res_company_id)
VALUES (%s, %s);
""",
(cfg_id, cid),
)

View File

@ -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

View File

@ -0,0 +1,10 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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")

View File

@ -0,0 +1,265 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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(
readonly=True,
states={"draft": [("readonly", False)]},
required=True,
)
journal_ids = fields.Many2many(
comodel_name="account.journal",
relation="account_journal_ebics_config_rel",
readonly=True,
states={"draft": [("readonly", False)]},
string="Bank Accounts",
domain="[('type', '=', 'bank')]",
)
ebics_host = fields.Char(
string="EBICS HostID",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
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,
readonly=True,
states={"draft": [("readonly", False)]},
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",
readonly=True,
states={"draft": [("readonly", False)]},
required=True,
default="H004",
)
ebics_partner = fields.Char(
string="EBICS PartnerID",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
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",
readonly=True,
states={"draft": [("readonly", False)]},
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_files = fields.Char(
string="EBICS Files Root",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
default=lambda self: self._default_ebics_files(),
help="Root Directory for EBICS File Transfer Folders.",
)
# 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,
readonly=True,
states={"draft": [("readonly", False)]},
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",
readonly=True,
states={"draft": [("readonly", False)]},
help="The key version of the electronic signature.",
)
ebics_key_bitlength = fields.Integer(
string="EBICS key bitlength",
default=2048,
readonly=True,
states={"draft": [("readonly", False)]},
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 Formats",
readonly=True,
states={"draft": [("readonly", False)]},
)
state = fields.Selection(
[("draft", "Draft"), ("confirm", "Confirmed")],
default="draft",
required=True,
readonly=True,
)
order_number = fields.Char(
size=4,
readonly=True,
states={"draft": [("readonly", False)]},
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_files(self):
return "/".join(["/home/odoo/ebics_files", self._cr.dbname])
@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(_("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(
_(
"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(_("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(
_(
"EBICS Keys Root Directory %s is not available."
"\nPlease contact your system administrator."
)
% dirname
)
def _check_ebics_files(self):
dirname = self.ebics_files or ""
if not os.path.exists(dirname):
raise UserError(
_(
"EBICS Files Root Directory %s is not available."
"\nPlease contact your system administrator."
)
% dirname
)

View File

@ -0,0 +1,625 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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.tools.safe_eval import safe_eval
from odoo.addons.base.models.res_bank import sanitize_account_number
_logger = logging.getLogger(__name__)
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="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(_("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"](ebics_file)
# 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(
_(
"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 _process_download_result(self, res):
statement_ids = res["statement_ids"]
notifications = res["notifications"]
sts_data = []
if statement_ids:
self.env.flush_all()
self.env.cr.execute(
"""
SELECT abs.name, abs.date, abs.company_id, rc.name AS company_name
FROM account_bank_statement abs
JOIN res_company rc ON rc.id = abs.company_id
WHERE abs.id in %s
ORDER BY abs.date, rc.id
""",
(tuple(res["statement_ids"]),),
)
sts_data = self.env.cr.dictfetchall()
st_cnt = len(statement_ids)
warning_cnt = error_cnt = 0
if notifications:
for notif in notifications:
if notif["type"] == "error":
error_cnt += 1
elif notif["type"] == "warning":
warning_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")]
self.note_process += "\n".join(parts)
self.note_process += "\n\n"
self.note_process += "\n"
if error_cnt:
self.note_process += (
_("Number of errors detected during import: %s: ") % error_cnt
)
self.note_process += "\n"
if warning_cnt:
self.note_process += (
_("Number of watnings detected during import: %s: ") % warning_cnt
)
if st_cnt:
self.note_process += "\n\n"
self.note_process += _(
"%(st_cnt)s bank statement%(sp)s been imported: ",
st_cnt=st_cnt,
sp=st_cnt == 1 and _(" has") or _("s have"),
)
self.note_process += "\n"
for st_data in sts_data:
self.note_process += ("\n%s, %s (%s)") % (
st_data["date"],
st_data["name"],
st_data["company_name"],
)
if statement_ids:
self.sudo().bank_statement_ids = [(4, x) for x in statement_ids]
company_ids = self.sudo().bank_statement_ids.mapped("company_id").ids
self.company_ids = [(6, 0, company_ids)]
ctx = dict(self.env.context, statement_ids=statement_ids)
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
return {
"name": _("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 _process_cfonb120(self):
"""
Disable this code while waiting on OCA cfonb release for 16.0
"""
# pylint: disable=W0101
raise NotImplementedError
import_module = "account_statement_import_fr_cfonb"
self._check_import_module(import_module)
wiz_model = "account.statement.import"
data_file = base64.b64decode(self.data)
lines = data_file.split(b"\n")
wiz_vals_list = []
st_lines = b""
transactions = False
for line in lines:
rec_type = line[0:2]
acc_number = line[21:32]
st_lines += line + b"\n"
if rec_type == b"04":
transactions = True
if rec_type == b"07":
if transactions:
fn = "_".join([acc_number.decode(), self.name])
wiz_vals_list.append(
{
"statement_filename": fn,
"statement_file": base64.b64encode(st_lines),
}
)
st_lines = b""
transactions = False
result_action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
result_action["context"] = safe_eval(result_action["context"])
statement_ids = []
notifications = []
for i, wiz_vals in enumerate(wiz_vals_list, start=1):
result = {
"statement_ids": [],
"notifications": [],
}
statement_filename = wiz_vals["statement_filename"]
wiz = (
self.env[wiz_model]
.with_context(active_model="ebics.file")
.create(wiz_vals)
)
try:
with self.env.cr.savepoint():
file_data = base64.b64decode(wiz_vals["statement_file"])
msg_hdr = _(
"{} : Import failed for statement number %(index)s, filename %(fn)s:\n",
index=i,
fn=statement_filename,
)
wiz.import_single_file(file_data, result)
if not result["statement_ids"]:
message = msg_hdr.format(_("Warning"))
message += _(
"You have already imported this file, or this file "
"only contains already imported transactions."
)
notifications += [
{
"type": "warning",
"message": message,
}
]
else:
statement_ids.extend(result["statement_ids"])
notifications.extend(result["notifications"])
except UserError as e:
message = msg_hdr.format(_("Error"))
message += "".join(e.args)
notifications += [
{
"type": "error",
"message": message,
}
]
except Exception:
tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error"))
message += tb
notifications += [
{
"type": "error",
"message": message,
}
]
result_action["context"]["notifications"] = notifications
result_action["domain"] = [("id", "in", statement_ids)]
return self._process_result_action(result_action)
def _unlink_cfonb120(self):
"""
Placeholder for cfonb120 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _process_camt052(self):
import_module = "account_statement_import_camt"
self._check_import_module(import_module)
return self._process_camt053(self)
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(self)
def _unlink_camt054(self):
"""
Placeholder for camt054 specific actions before removing the
EBICS data file and its related bank statements.
"""
def _process_camt053(self): # noqa C901
"""
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.
TODO: refactor method to enable removal of noqa C901
"""
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(
_(
"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": []}
try:
with self.env.cr.savepoint():
msg_hdr = _("{} : Import failed for file %(fn)s:\n", fn=self.name)
file_data = base64.b64decode(self.data)
root = etree.fromstring(file_data, parser=etree.XMLParser(recover=True))
if root is None:
message = msg_hdr.format(_("Error"))
message += _("Invalid XML file.")
res["notifications"].append({"type": "error", "message": message})
ns = {k or "ns": v for k, v in root.nsmap.items()}
for i, stmt in enumerate(root[0].findall("ns:Stmt", ns), start=1):
msg_hdr = _(
"{} : Import failed for statement number %(index)s, filename %(fn)s:\n",
index=i,
fn=self.name,
)
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 = msg_hdr.format(_("Error"))
message += _("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]
currency = self.env["res.currency"].search(
[("name", "=ilike", currency_code)], limit=1
)
if not currency:
message = msg_hdr.format(_("Error"))
message += _("Currency %(cc) not found.", cc=currency_code)
res["notifications"] = {"type": "error", "message": message}
continue
journal = self.env["account.journal"].search(
[
("type", "=", "bank"),
(
"bank_account_id.sanitized_acc_number",
"ilike",
acc_number,
),
]
)
journal_currency = (
journal.currency_id or journal.company_id.currency_id
)
if journal_currency != currency:
message = msg_hdr.format(_("Error"))
message += _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append(
{"type": "error", "message": message}
)
continue
root_new = deepcopy(root)
for j, el in enumerate(root_new[0].findall("ns:Stmt", ns), start=1):
if j != i:
el.getparent.remove(el)
data = base64.b64encode(etree.tostring(root_new))
if author == "oca":
# TODO: implement _process_camt053_oca() once OCA camt is
# released for 16.0
raise NotImplementedError
else:
self.env.company = journal.company_id
attachment = self.env["ir.attachment"].create(
{"name": self.name, "datas": data, "store_fname": self.name}
)
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"])
except UserError as e:
message = msg_hdr.format(_("Error"))
message += "".join(e.args)
res["notifications"].append({"type": "error", "message": message})
except Exception:
tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error"))
message += tb
res["notifications"].append({"type": "error", "message": message})
if author == "oca":
# TODO: implement _process_camt053_oca() once OCA camt is
# released for 16.0
return self._process_camt053_oca()
else:
return self._process_download_result(res)
def _process_camt053_oca(self):
"""
Disable this code while waiting on OCA CAMT parser for 16.0
"""
# pylint: disable=W0101
raise NotImplementedError
wiz_model = "account.statement.import"
wiz_vals = {
"statement_filename": self.name,
"statement_file": self.data,
}
result_action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
result_action["context"] = safe_eval(result_action["context"])
result = {
"statement_ids": [],
"notifications": [],
}
statement_ids = []
notifications = []
wiz = (
self.env[wiz_model].with_context(active_model="ebics.file").create(wiz_vals)
)
msg_hdr = _(
"{} : Import failed for EBICS File %(fn)s:\n",
fn=wiz.statement_filename,
)
try:
with self.env.cr.savepoint():
file_data = base64.b64decode(self.data)
wiz.import_single_file(file_data, result)
if not result["statement_ids"]:
message = msg_hdr.format(_("Warning"))
message += _(
"You have already imported this file, or this file "
"only contains already imported transactions."
)
notifications += [
{
"type": "warning",
"message": message,
}
]
else:
statement_ids.extend(result["statement_ids"])
notifications.extend(result["notifications"])
except UserError as e:
message = msg_hdr.format(_("Error"))
message += "".join(e.args)
notifications += [
{
"type": "error",
"message": message,
}
]
except Exception:
tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error"))
message += tb
notifications += [
{
"type": "error",
"message": message,
}
]
result_action["context"]["notifications"] = notifications
result_action["domain"] = [("id", "in", statement_ids)]
return self._process_result_action(result_action)
def _unlink_camt053(self):
"""
Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements.
"""
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(
_(
"The current version of the 'account_ebics' module "
"has no support to automatically process EBICS files "
"with format %s."
)
% self.format_id.name
)

View File

@ -0,0 +1,118 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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
def name_get(self):
res = []
for rec in self:
name = rec.ebics_version == "2" and rec.name or rec.btf_message
res.append((rec.id, name))
return res

View File

@ -0,0 +1,510 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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__)
# logging.basicConfig(
# level=logging.DEBUG,
# format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
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,
readonly=True,
states={"draft": [("readonly", False)]},
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"
)
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.",
)
# Currently only a singe signature class per user is supported
# Classes A and B are not yet supported.
signature_class = fields.Selection(
selection=[("E", "Single signature"), ("T", "Transport signature")],
required=True,
default="T",
readonly=True,
states={"draft": [("readonly", False)]},
help="Default signature class."
"This default can be overriden for specific "
"EBICS transactions (cf. File Formats).",
)
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_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="EBICS Public Bank Keys 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]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_o = fields.Char(
string="Organization Name [O]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_ou = fields.Char(
string="Organizational Unit Name [OU]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_c = fields.Char(
string="Country Name [C]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_st = fields.Char(
string="State Or Province Name [ST]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_l = fields.Char(
string="Locality Name [L]",
readonly=True,
states={"draft": [("readonly", False)]},
)
ebics_key_x509_dn_e = fields.Char(
string="Email Address",
readonly=True,
states={"draft": [("readonly", False)]},
)
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")
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.constrains("ebics_key_x509")
def _check_ebics_key_x509(self):
for cfg in self:
if cfg.ebics_version == "H005" and not cfg.ebics_key_x509:
raise UserError(_("X.509 certificates must be used with EBICS 3.0."))
@api.constrains("ebics_passphrase")
def _check_ebics_passphrase(self):
for rec in self:
if not rec.ebics_passphrase or len(rec.ebics_passphrase) < 8:
raise UserError(_("The passphrase must be at least 8 characters long"))
@api.onchange("ebics_version")
def _onchange_ebics_version(self):
if self.ebics_version == "H005":
self.ebics_key_x509 = True
@api.onchange("signature_class")
def _onchange_signature_class(self):
if self.signature_class == "T":
self.swift_3skey = 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):
return self.write({"state": "active_keys"})
def set_to_get_bank_keys(self):
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()
self.ebics_config_id._check_ebics_files()
if self.state != "draft":
raise UserError(
_("Set state to 'draft' before Bank Key (re)initialisation.")
)
if not self.ebics_passphrase:
raise UserError(_("Set a passphrase."))
if self.swift_3skey and not self.swift_3skey_certificate:
raise UserError(_("3SKey certificate missing."))
ebics_version = self.ebics_config_id.ebics_version
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,
)
except Exception as err:
exctype, value = exc_info()[:2]
error = _("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 = _("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(
_(
"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 = _("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(
_("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 = _("EBICS version mismatch.") + "\n"
err_msg += _("Versions supported by your bank:")
for k in supported_versions:
err_msg += "\n{}: {} ".format(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(
_(
"urlopen error:\n url '%(url)s' - %(val)s",
url=self.ebics_config_id.ebics_url,
val=str(value),
)
) from err
except EbicsFunctionalError as err:
e = exc_info()
error = _("EBICS Functional Error:")
error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) from err
except EbicsTechnicalError as err:
e = exc_info()
error = _("EBICS Technical Error:")
error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) 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]
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today().isoformat()
fn = "_".join([self.ebics_config_id.ebics_host, "ini_letter", fn_date]) + ".pdf"
full_tmp_fn = os.path.normpath(tmp_dir + "/" + fn)
user.create_ini_letter(
bankname=ebics_config_bank.name, path=full_tmp_fn, lang=lang
)
with open(full_tmp_fn, "rb") as f:
letter = f.read()
self.write(
{
"ebics_ini_letter": base64.encodebytes(letter),
"ebics_ini_letter_fn": fn,
}
)
return self.write({"state": "init"})
def ebics_init_2(self):
"""
Initialization of bank keys - Step 2:
Activation of the account by the bank.
"""
if self.state != "init":
raise UserError(_("Set state to 'Initialisation'."))
self.ensure_one()
return self.write({"state": "get_bank_keys"})
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()
self.ebics_config_id._check_ebics_files()
if self.state != "get_bank_keys":
raise UserError(_("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 = _("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 = _("EBICS Functional Error:")
error += "\n"
error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) from err
except Exception as err:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) from err
public_bank_keys = public_bank_keys.encode()
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today().isoformat()
fn = (
"_".join([self.ebics_config_id.ebics_host, "public_bank_keys", fn_date])
+ ".txt"
)
self.write(
{
"ebics_public_bank_keys": base64.encodebytes(public_bank_keys),
"ebics_public_bank_keys_fn": fn,
"state": "to_verify",
}
)
return True
def ebics_init_4(self):
"""
Initialization of bank keys - Step 2:
Confirm Verification of the public bank keys
and activate the bank keyu.
"""
self.ensure_one()
if self.state != "to_verify":
raise UserError(_("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()
return self.write({"state": "active_keys"})
def change_passphrase(self):
self.ensure_one()
ctx = dict(self._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": _("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",
}

View File

@ -0,0 +1,46 @@
# Copyright 2009-2020 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
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()

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="res.groups" id="group_ebics_manager">
<field name="name">EBICS Manager</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<data noupdate="1">
<record id="ebics_config_comp_rule" model="ir.rule">
<field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_userid_comp_rule" model="ir.rule">
<field name="name">EBICS UserID model company rule</field>
<field name="model_id" ref="model_ebics_userid" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,12 @@
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ebics_config_manager ebics_config manager model_ebics_config group_ebics_manager 1 1 1 1
3 access_ebics_config_user ebics_config user model_ebics_config account.group_account_invoice 1 0 0 0
4 access_ebics_userid_manager ebics_userid manager model_ebics_userid group_ebics_manager 1 1 1 1
5 access_ebics_userid_user ebics_userid user model_ebics_userid account.group_account_invoice 1 0 0 0
6 access_ebics_file_format_manager ebics_file_format manager model_ebics_file_format group_ebics_manager 1 1 1 1
7 access_ebics_file_format_user ebics_file_format user model_ebics_file_format account.group_account_invoice 1 0 0 0
8 access_ebics_file_manager ebics_file manager model_ebics_file group_ebics_manager 1 1 1 1
9 access_ebics_file_user ebics_file user model_ebics_file account.group_account_invoice 1 1 1 0
10 access_ebics_change_passphrase access_ebics_change_passphrase model_ebics_change_passphrase group_ebics_manager 1 1 1 0
11 access_ebics_xfer access_ebics_xfer model_ebics_xfer account.group_account_invoice 1 1 1 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,572 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.16: http://docutils.sourceforge.net/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://www.gnu.org/licenses/lpgl"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a>
<div class="section" id="ebics-banking-protocol">
<h1>EBICS banking protocol</h1>
<p>Implementation of the EBICS banking protocol.</p>
<p>This module facilitates the exchange of files with banks via the EBICS protocol.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="installation">
<h2>Installation</h2>
<p>The module depends upon</p>
<ul class="simple">
<li><a class="reference external" href="https://pypi.python.org/pypi/fintech">https://pypi.python.org/pypi/fintech</a></li>
<li><a class="reference external" href="https://pypi.python.org/pypi/cryptography">https://pypi.python.org/pypi/cryptography</a></li>
</ul>
<p>Remark:</p>
<p>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.</p>
<p>SWIFT 3SKey support requires fintech 6.4 or higher.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>We also recommend to consider the installation of the following modules:</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_oe</p>
<p>Required if you are running Odoo Enterprise</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_batch</p>
<p>This module adds a cron job for the automated import of EBICS files.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_batch_payment</p>
<p>Recommended if you are using the Odoo Enterprise account_batch_payment module</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_usability</p>
<p>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.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/account-financial-tools">https://github.com/OCA/account-financial-tools</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_payment_order</p>
<p>Recommended if you are using the OCA account_payment_order module.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a> and <a class="reference external" href="https://github.com/OCA/bank-payment">https://github.com/OCA/bank-payment</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_statement_import_fr_cfonb</p>
<p>Required to handle french CFONB files.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/l10n_france">https://github.com/OCA/l10n_france</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_statement_import_camt</p>
<p>Required to handle camt.052 and camt.054 files.</p>
<p>Cf. <a class="reference external" href="https://github.com/OCA/bank-statement-import">https://github.com/OCA/bank-statement-import</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_statement_import_helper</p>
<p>Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files)
and using import parsers based upon the OCA account_statement_import module.</p>
<p>The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/noviat-apps">https://github.com/Noviat/noviat-apps</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_bank_statement_import_helper</p>
<p>Required if you are processing bank statements with local bank account numbers
and using import parsers based upon the Odoo Enterprise account_bank_statement_import module.</p>
<p>The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/noviat-apps">https://github.com/Noviat/noviat-apps</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="fintech-license">
<h3>Fintech license</h3>
<p>If you have a valid Fintech.ebics license, you should add the following
licensing parameters to the odoo server configuration file:</p>
<ul class="simple">
<li>fintech_register_name</li>
</ul>
<p>The name of the licensee.</p>
<ul class="simple">
<li>fintech_register_keycode</li>
</ul>
<p>The keycode of the licensed version.</p>
<ul class="simple">
<li>fintech_register_users</li>
</ul>
<p>The licensed EBICS user ids. It must be a string or a list of user ids.</p>
<p>You should NOT specify this parameter if your license is subsciption
based (with monthly recurring billing).</p>
<div class="line-block">
<div class="line"><br /></div>
<div class="line">Example:</div>
<div class="line"><br /></div>
</div>
<pre class="literal-block">
; fintech
fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
fintech_register_users = USER1, USER2
</pre>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
</div>
<div class="section" id="configuration">
<h2>Configuration</h2>
<p>Go to <strong>Settings &gt; Users</strong></p>
<p>Add the users that are authorised to maintain the EBICS configuration to the 'EBICS Manager' Group.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Go to <strong>Accounting &gt; Configuration &gt; Miscellaneous &gt; EBICS &gt; EBICS File Formats</strong></p>
<p>Check if the EBICS File formats that you want to process in Odoo are defined.</p>
<p>Most commonly used formats for which support is available in Odoo should be there already.</p>
<p>Please open an issue on <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a> to report missing EBICS File Formats.</p>
<p>For File Formats of type 'Downloads' you can also specifiy a 'Download Process Method'.</p>
<p>This is the method that will be executed when hitting the 'Process' button on the downloaded file.</p>
<p>The following methods are currently available:</p>
<ul class="simple">
<li>cfonb120</li>
<li>camt.053</li>
<li>camt.052</li>
<li>camt.054</li>
</ul>
<p>All these methods require complimentary modules to be installed (cf. Installation section supra).</p>
<p>You'll get an error message when the required module is not installed on your Odoo instance.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Go to <strong>Accounting &gt; Configuration &gt; Miscellaneous &gt; EBICS &gt; EBICS Configuration</strong></p>
<p>Configure your EBICS configuration according to the contract with your bank.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
<div class="section" id="usage">
<h2>Usage</h2>
<p>Go to <strong>Accounting &gt; Bank and Cash &gt; EBICS Processing</strong></p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="ebics-return-codes">
<h3>EBICS Return Codes</h3>
<p>During the processing of your EBICS upload/download, your bank may return an Error Code, e.g.</p>
<p>EBICS Functional Error:
EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 90005)</p>
<p>A detailled explanation of the codes can be found on <a class="reference external" href="http://www.ebics.org">http://www.ebics.org</a>.
You can also find this information in the doc folder of this module (file EBICS_Annex1_ReturnCodes).</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h2>Known Issues / Roadmap</h2>
<ul class="simple">
<li>add support to import externally generated keys &amp; certificates (currently only 3SKey signature certificate)</li>
<li>For Odoo 16.0 the interaction with the OCA payment order and bank statement import modules (e.g. french CFONB) is not yet available.</li>
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_config_view_tree" model="ir.ui.view">
<field name="name">ebics.config.tree</field>
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<tree decoration-muted="state == 'draft'">
<field name="name" />
<field name="ebics_host" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
<record id="ebics_config_view_form" model="ir.ui.view">
<field name="name">ebics.config.form</field>
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<form string="EBICS Configuration">
<header>
<button
name="set_to_draft"
states="confirm"
string="Set to Draft"
type="object"
groups="account_ebics.group_ebics_manager"
help="Set to Draft in order to change the EBICS configuration parameters."
/>
<button
name="set_to_confirm"
states="draft"
string="Confirm"
type="object"
class="oe_highlight"
groups="account_ebics.group_ebics_manager"
help="The EBICS configuration must be confirmed before it can used for bank transactions."
/>
<field name="state" widget="statusbar" />
</header>
<field name="active" invisible="1" />
<widget
name="web_ribbon"
text="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<group name="main">
<group name="main-left">
<field name="name" colspan="2" />
<field name="ebics_host" />
<field name="ebics_url" />
<field name="ebics_partner" />
<field name="ebics_files" />
<field name="ebics_keys" />
</group>
<group name="main-right">
<field
name="journal_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_version" />
<field name="ebics_key_version" />
<field name="ebics_key_bitlength" />
<field
name="order_number"
attrs="{'invisible': [('ebics_version', '!=', 'H003')]}"
/>
<field name="company_ids" widget="many2many_tags" invisible="1" />
</group>
</group>
<notebook>
<page string="EBICS Users" groups="account_ebics.group_ebics_manager">
<field name="ebics_userid_ids" />
</page>
<page string="File Formats" groups="account_ebics.group_ebics_manager">
<field name="ebics_file_format_ids" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_config_action" model="ir.actions.act_window">
<field name="name">EBICS Configuration</field>
<field name="res_model">ebics.config</field>
<field name="view_mode">tree,form</field>
<field name="context">{'active_test': False}</field>
</record>
</odoo>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_file_format_view_tree" model="ir.ui.view">
<field name="name">ebics.file.format.tree</field>
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<tree>
<field name="ebics_version" />
<field name="type" />
<field name="order_type" />
<field name="signature_class" />
<field name="name" />
<field name="description" />
</tree>
</field>
</record>
<record id="ebics_file_format_view_form" model="ir.ui.view">
<field name="name">ebics.file.format.form</field>
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<form string="EBICS File Format">
<group name="main">
<group name="main-left">
<field name="ebics_version" />
<field name="type" />
<field name="suffix" />
<field
name="download_process_method"
attrs="{'invisible': [('type', '=', 'up')]}"
force_save="1"
/>
<field name="signature_class" />
</group>
<group name="main-right">
<field name="order_type" />
<field
name="name"
attrs="{'required': [('ebics_version', '=', '2')], 'invisible': [('ebics_version', '=', '3')]}"
/>
<field
name="btf_service"
attrs="{'required': [('ebics_version', '=', '3')], 'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_message"
attrs="{'required': [('ebics_version', '=', '3')], 'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_scope"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_option"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_container"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_version"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_variant"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
<field
name="btf_format"
attrs="{'invisible': [('ebics_version', '=', '2')]}"
/>
</group>
</group>
<group name="description">
<field name="description" />
</group>
</form>
</field>
</record>
<record id="ebics_file_format_action" model="ir.actions.act_window">
<field name="name">EBICS File Formats</field>
<field name="res_model">ebics.file.format</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@ -0,0 +1,256 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_file_view_search" model="ir.ui.view">
<field name="name">ebics.file.search</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<search string="Search EBICS Files">
<group col="10" colspan="4">
<field name="date_from" />
<field name="date_to" />
<field name="name" />
<field name="format_id" />
<field name="user_id" />
<field
name="company_ids"
widget="selection"
groups="base.group_multi_company"
/>
</group>
<newline />
<group expand="0" string="Group By">
<filter
string="File Format"
name="file_format"
context="{'group_by':'format_id'}"
/>
<filter string="State" name="state" context="{'group_by':'state'}" />
<filter string="User" name="user" context="{'group_by':'user_id'}" />
</group>
</search>
</field>
</record>
<!-- Download -->
<record id="ebics_file_view_tree_download" model="ir.ui.view">
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Download Date" />
<field name="name" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
<record id="ebics_file_view_form_download" model="ir.ui.view">
<field name="name">ebics.file.form</field>
<field name="model">ebics.file</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button
name="set_to_draft"
states="done"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="process"
class="oe_highlight"
states="draft"
string="Process"
type="object"
groups="account.group_account_invoice"
help="Process the EBICS File"
/>
<button
name="set_to_done"
states="draft"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" />
</header>
<group colspan="4" col="4">
<field name="date" string="Download Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1" />
</page>
<page
string="Bank Statements"
attrs="{'invisible':[('bank_statement_ids','=',[])]}"
>
<field name="bank_statement_ids" nolabel="1" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_file_view_form_result" model="ir.ui.view">
<field name="name">ebics.file.process.result</field>
<field name="model">ebics.file</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="Process EBICS File">
<separator colspan="4" string="Results :" />
<field name="note_process" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
name="action_open_bank_statements"
string="View Bank Statement(s)"
type="object"
class="oe_highlight"
invisible="not context.get('statement_ids')"
/>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>
</record>
<record id="ebics_file_action_download" model="ir.actions.act_window">
<field name="name">EBICS Download Files</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','down')]</field>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_download_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<record id="ebics_file_action_download_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<!-- Upload -->
<record id="ebics_file_view_tree_upload" model="ir.ui.view">
<field name="name">ebics.file.tree</field>
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree decoration-muted="state=='draft'" create="false">
<field name="date" string="Upload Date" />
<field name="name" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
<record id="ebics_file_view_form_upload" model="ir.ui.view">
<field name="name">ebics.file.form</field>
<field name="model">ebics.file</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button
name="set_to_draft"
states="done"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="set_to_done"
states="draft"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" />
</header>
<group colspan="4" col="4">
<field name="date" string="Upload Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1" />
</page>
</notebook>
</form>
</field>
</record>
<record id="ebics_file_action_upload" model="ir.actions.act_window">
<field name="name">EBICS Upload Files</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','up')]</field>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_upload_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
<record id="ebics_file_action_upload_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
</odoo>

View File

@ -0,0 +1,160 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_userid_view_tree" model="ir.ui.view">
<field name="name">ebics.userid.tree</field>
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<tree decoration-muted="state != 'active_keys'">
<field name="name" />
<field name="signature_class" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
<record id="ebics_userid_view_form" model="ir.ui.view">
<field name="name">ebics.userid.form</field>
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<form string="EBICS UserID">
<header>
<button
name="ebics_init_1"
states="draft"
string="EBICS Initialisation"
type="object"
class="oe_highlight"
help="Initialise EBICS Bank Keys"
/>
<button
name="ebics_init_2"
states="init"
string="Account activated"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the account has been activated by the bank."
/>
<button
name="ebics_init_3"
states="get_bank_keys"
string="Get Bank Keys"
type="object"
class="oe_highlight"
help="EBICS Initialisation - After the account has been activated the public bank keys must be downloaded and checked for consistency."
/>
<button
name="ebics_init_4"
states="to_verify"
string="Bank Keys Verified"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the public have been checked for consistency."
/>
<button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="oe_highlight"
attrs="{'invisible': [('ebics_keys_found', '=', False)]}"
/>
<button
name="set_to_draft"
states="active_keys"
string="Set to Draft"
type="object"
help="Set to Draft in order to reinitialize your bank connection."
/>
<button
name="set_to_get_bank_keys"
states="active_keys"
string="Renew Bank Keys"
type="object"
help="Use this button to update the EBICS certificates of your bank."
/>
<button
name="set_to_active_keys"
states="draft"
string="Force Active Keys"
type="object"
help="Use this button to bypass the EBICS initialization (e.g. in case you have manually transferred active EBICS keys from another system."
/>
<field name="state" widget="statusbar" />
</header>
<group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}">
<field name="ebics_keys_found" invisible="1" />
<field name="ebics_keys_fn" invisible="1" />
<field name="ebics_version" invisible="1" />
<group name="main-left">
<field name="name" />
<field
name="ebics_passphrase"
password="True"
attrs="{'required': [('state', '=', 'draft')]}"
/>
<field
name="swift_3skey"
attrs="{'invisible': [('signature_class', '=', 'T')]}"
/>
<field name="swift_3skey_certificate_fn" invisible="1" />
<field
name="swift_3skey_certificate"
filename="swift_3skey_certificate_fn"
attrs="{'invisible': [('swift_3skey', '=', False)], 'required': [('swift_3skey', '=', True)]}"
/>
<field name="active" />
</group>
<group name="main-right">
<field name="signature_class" />
<field
name="user_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_key_x509" />
</group>
</group>
<group
name="dn"
attrs="{'invisible': [('ebics_key_x509', '=', False)], 'readonly': [('state', '!=', 'draft')]}"
>
<div colspan="2" col="1">
<strong
>Distinguished Name attributes used to create self-signed X.509 certificates:</strong>
</div>
<group name="dn_l">
<field name="ebics_key_x509_dn_cn" />
<field name="ebics_key_x509_dn_o" />
<field name="ebics_key_x509_dn_l" />
<field name="ebics_key_x509_dn_c" />
</group>
<group name="dn_r">
<field name="ebics_key_x509_dn_e" />
<field name="ebics_key_x509_dn_ou" />
<field name="ebics_key_x509_dn_st" />
</group>
</group>
<group name="files">
<group
colspan="2"
name="ebics_ini_letter"
attrs="{'invisible': [('ebics_ini_letter', '=', False)]}"
>
<field name="ebics_ini_letter_fn" invisible="1" />
<field name="ebics_ini_letter" filename="ebics_ini_letter_fn" />
</group>
<group
colspan="2"
name="ebics_public_bank_keys"
attrs="{'invisible': [('ebics_public_bank_keys', '=', False)]}"
>
<field name="ebics_public_bank_keys_fn" invisible="1" />
<field name="ebics_public_bank_keys" filename="ebics_public_bank_keys_fn" />
</group>
</group>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" ?>
<odoo>
<menuitem
id="ebics_processing_menu"
name="EBICS Processing"
parent="account.menu_finance"
sequence="4"
/>
<menuitem
id="ebics_xfer_menu_download"
name="EBICS Download"
parent="ebics_processing_menu"
action="ebics_xfer_action_download"
sequence="10"
/>
<menuitem
id="ebics_xfer_menu_upload"
name="EBICS Upload"
parent="ebics_processing_menu"
action="ebics_xfer_action_upload"
sequence="20"
/>
<menuitem
id="ebics_file_menu"
name="EBICS Files"
parent="ebics_processing_menu"
sequence="30"
/>
<menuitem
id="ebics_file_menu_download"
name="Download"
parent="ebics_file_menu"
action="ebics_file_action_download"
sequence="10"
/>
<menuitem
id="ebics_file_menu_upload"
name="Upload"
parent="ebics_file_menu"
action="ebics_file_action_upload"
sequence="20"
/>
<menuitem
id="ebics_menu"
name="EBICS"
parent='account.menu_finance_configuration'
groups="account_ebics.group_ebics_manager"
sequence="100"
/>
<menuitem
id="ebics_config_menu"
name="EBICS Configuration"
parent="ebics_menu"
action="ebics_config_action"
groups="account_ebics.group_ebics_manager"
sequence="10"
/>
<menuitem
id="ebics_file_format_menu"
name="EBICS File Formats"
parent="ebics_menu"
action="ebics_file_format_action"
groups="account_ebics.group_ebics_manager"
sequence="20"
/>
</odoo>

View File

@ -0,0 +1,2 @@
from . import ebics_change_passphrase
from . import ebics_xfer

View File

@ -0,0 +1,68 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging
from odoo import _, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import EbicsKeyRing
fintech.cryptolib = "cryptography"
except ImportError:
_logger.warning("Failed to import fintech")
class EbicsChangePassphrase(models.TransientModel):
_name = "ebics.change.passphrase"
_description = "Change EBICS keys passphrase"
ebics_userid_id = fields.Many2one(
comodel_name="ebics.userid", string="EBICS UserID", readonly=True
)
old_pass = fields.Char(string="Old Passphrase", required=True)
new_pass = fields.Char(string="New Passphrase", required=True)
new_pass_check = fields.Char(string="New Passphrase (verification)", required=True)
note = fields.Text(string="Notes", readonly=True)
def change_passphrase(self):
self.ensure_one()
if self.old_pass != self.ebics_userid_id.ebics_passphrase:
raise UserError(_("Incorrect old passphrase."))
if self.new_pass != self.new_pass_check:
raise UserError(_("New passphrase verification error."))
if self.new_pass == self.ebics_userid_id.ebics_passphrase:
raise UserError(_("New passphrase equal to old passphrase."))
try:
keyring = EbicsKeyRing(
keys=self.ebics_userid_id.ebics_keys_fn,
passphrase=self.ebics_userid_id.ebics_passphrase,
)
keyring.change_passphrase(self.new_pass)
except ValueError as err:
raise UserError(str(err)) from err
self.ebics_userid.ebics_passphrase = self.new_pass
self.note = "The EBICS Passphrase has been changed."
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref(
"%s.ebics_change_passphrase_view_form_result" % module
)
return {
"name": _("EBICS Keys Change Passphrase"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.change.passphrase",
"view_id": result_view.id,
"target": "new",
"type": "ir.actions.act_window",
}
def button_close(self):
self.ensure_one()
return {"type": "ir.actions.act_window_close"}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_change_passphrase_view_form" model="ir.ui.view">
<field name="name">EBICS Keys Change Passphrase</field>
<field name="model">ebics.change.passphrase</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<group>
<field name="old_pass" password="True" />
<field name="new_pass" password="True" />
<field name="new_pass_check" password="True" />
</group>
<footer>
<button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="ebics_change_passphrase_view_form_result" model="ir.ui.view">
<field name="name">EBICS Keys Change Passphrase</field>
<field name="model">ebics.change.passphrase</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,646 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
"""
import logging
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
"""
import base64
import logging
import os
from sys import exc_info
from traceback import format_exception
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import (
BusinessTransactionFormat,
EbicsBank,
EbicsClient,
EbicsFunctionalError,
EbicsKeyRing,
EbicsTechnicalError,
EbicsUser,
EbicsVerificationError,
)
fintech.cryptolib = "cryptography"
except ImportError:
EbicsBank = object
_logger.warning("Failed to import fintech")
class EbicsBank(EbicsBank):
def _next_order_id(self, partnerid):
"""
EBICS protocol version H003 requires generation of the OrderID.
The OrderID must be a string between 'A000' and 'ZZZZ' and
unique for each partner id.
"""
return hasattr(self, "_order_number") and self._order_number or "A000"
class EbicsXfer(models.TransientModel):
_name = "ebics.xfer"
_description = "EBICS file transfer"
ebics_config_id = fields.Many2one(
comodel_name="ebics.config",
string="EBICS Configuration",
domain=[("state", "=", "confirm")],
default=lambda self: self._default_ebics_config_id(),
)
ebics_userid_id = fields.Many2one(
comodel_name="ebics.userid", string="EBICS UserID"
)
ebics_passphrase = fields.Char(string="EBICS Passphrase")
date_from = fields.Date()
date_to = fields.Date()
upload_data = fields.Binary(string="File to Upload")
upload_fname = fields.Char(string="Upload Filename", default="")
upload_fname_dummy = fields.Char(
related="upload_fname", string="Upload Filename", readonly=True
)
format_id = fields.Many2one(
comodel_name="ebics.file.format",
string="EBICS File Format",
help="Select EBICS File Format to upload/download."
"\nLeave blank to download all available files.",
)
allowed_format_ids = fields.Many2many(
related="ebics_config_id.ebics_file_format_ids",
string="Allowed EBICS File Formats",
)
order_type = fields.Char(
related="format_id.order_type",
string="Order Type",
)
test_mode = fields.Boolean(
help="Select this option to test if the syntax of "
"the upload file is correct."
"\nThis option is only available for "
"Order Type 'FUL'.",
)
note = fields.Text(string="EBICS file transfer Log", readonly=True)
@api.model
def _default_ebics_config_id(self):
cfg_mod = self.env["ebics.config"]
cfg = cfg_mod.search(
[
("company_ids", "in", self.env.user.company_ids.ids),
("state", "=", "confirm"),
]
)
if cfg and len(cfg) == 1:
return cfg
else:
return cfg_mod
@api.onchange("ebics_config_id")
def _onchange_ebics_config_id(self):
ebics_userids = self.ebics_config_id.ebics_userid_ids
if self._context.get("ebics_download"):
download_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "down"
)
if len(download_formats) == 1:
self.format_id = download_formats
if len(ebics_userids) == 1:
self.ebics_userid_id = ebics_userids
else:
transport_users = ebics_userids.filtered(
lambda r: r.signature_class == "T"
)
if len(transport_users) == 1:
self.ebics_userid_id = transport_users
else:
upload_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "up"
)
if len(upload_formats) == 1:
self.format_id = upload_formats
if len(ebics_userids) == 1:
self.ebics_userid_id = ebics_userids
@api.onchange("upload_data")
def _onchange_upload_data(self):
self.upload_fname_dummy = self.upload_fname
self.format_id = False
self._detect_upload_format()
if not self.format_id:
upload_formats = (
self.format_id
or self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "up"
)
)
if len(upload_formats) > 1:
upload_formats = upload_formats.filtered(
lambda r: self.upload_fname.endswith(r.suffix)
)
if len(upload_formats) == 1:
self.format_id = upload_formats
@api.onchange("format_id")
def _onchange_format_id(self):
self.order_type = self.format_id.order_type
def ebics_upload(self):
self.ensure_one()
ctx = self._context.copy()
ebics_file = self._ebics_upload()
if ebics_file:
ctx["ebics_file_id"] = ebics_file.id
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
return {
"name": _("EBICS file transfer result"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def ebics_download(self):
self.ensure_one()
self.ebics_config_id._check_ebics_files()
ctx = self.env.context.copy()
self.note = ""
err_cnt = 0
client = self._setup_client()
if not client:
err_cnt += 1
self.note += (
_("EBICS client setup failed for connection '%s'")
% self.ebics_config_id.name
)
else:
download_formats = (
self.format_id
or self.ebics_config_id.ebics_file_format_ids.filtered(
lambda r: r.type == "down"
)
)
ebics_files = self.env["ebics.file"]
date_from = self.date_from and self.date_from.isoformat() or None
date_to = self.date_to and self.date_to.isoformat() or None
for df in download_formats:
try:
success = False
if df.order_type == "BTD":
btf = BusinessTransactionFormat(
df.btf_service,
df.btf_message,
scope=df.btf_scope or None,
option=df.btf_option or None,
container=df.btf_container or None,
version=df.btf_version or None,
variant=df.btf_variant or None,
format=df.btf_format or None,
)
data = client.BTD(btf, start=date_from, end=date_to)
elif df.order_type == "FDL":
data = client.FDL(df.name, date_from, date_to)
else:
params = None
if date_from and date_to:
params = {
"DateRange": {
"Start": date_from,
"End": date_to,
}
}
data = client.download(df.order_type, params=params)
ebics_files += self._handle_download_data(data, df)
success = True
except EbicsFunctionalError:
err_cnt += 1
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Functional Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsTechnicalError:
err_cnt += 1
e = exc_info()
self.note += "\n"
self.note += _(
"EBICS Technical Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsVerificationError:
err_cnt += 1
self.note += "\n"
self.note += _(
"EBICS Verification Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += _("The EBICS response could not be verified.")
except UserError as e:
self.note += "\n"
self.note += _(
"Warning during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
self.note += "\n"
self.note += e.name
except Exception:
err_cnt += 1
self.note += "\n"
self.note += _(
"Unknown Error during download of "
"File Format %(name)s (%(order_type)s):",
name=df.name,
order_type=df.order_type,
)
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
else:
# mark received data so that it is not included in further
# downloads
trans_id = client.last_trans_id
client.confirm_download(trans_id=trans_id, success=success)
ctx["ebics_file_ids"] = ebics_files.ids
if ebics_files:
self.note += "\n"
for f in ebics_files:
self.note += (
_("EBICS File '%s' is available for further processing.")
% f.name
)
self.note += "\n"
ctx["err_cnt"] = err_cnt
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
return {
"name": _("EBICS file transfer result"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
def button_close(self):
self.ensure_one()
return {"type": "ir.actions.act_window_close"}
def view_ebics_file(self):
self.ensure_one()
module = __name__.split("addons.")[1].split(".")[0]
act = self.env["ir.actions.act_window"]._for_xml_id(
"{}.ebics_file_action_download".format(module)
)
act["domain"] = [("id", "in", self._context["ebics_file_ids"])]
return act
def _ebics_upload(self):
self.ensure_one()
ebics_file = self.env["ebics.file"]
self.note = ""
client = self._setup_client()
if client:
upload_data = base64.decodebytes(self.upload_data)
ef_format = self.format_id
OrderID = False
try:
order_type = self.order_type
if order_type == "BTU":
btf = BusinessTransactionFormat(
ef_format.btf_service,
ef_format.btf_message,
scope=ef_format.btf_scope or None,
option=ef_format.btf_option or None,
container=ef_format.btf_container or None,
version=ef_format.btf_version or None,
variant=ef_format.btf_variant or None,
format=ef_format.btf_format or None,
)
kwargs = {}
if self.test_mode:
kwargs["TEST"] = "TRUE"
OrderID = client.BTU(btf, upload_data, **kwargs)
elif order_type == "FUL":
kwargs = {}
bank = self.ebics_config_id.journal_ids[0].bank_id
cc = bank.country.code
if cc:
kwargs["country"] = cc
if self.test_mode:
kwargs["TEST"] = "TRUE"
OrderID = client.FUL(ef_format.name, upload_data, **kwargs)
else:
OrderID = client.upload(order_type, upload_data)
if OrderID:
self.note += "\n"
self.note += (
_("EBICS File has been uploaded (OrderID %s).") % OrderID
)
ef_note = _("EBICS OrderID: %s") % OrderID
if self.env.context.get("origin"):
ef_note += "\n" + _("Origin: %s") % self._context["origin"]
suffix = self.format_id.suffix
fn = self.upload_fname
if suffix and not fn.endswith(suffix):
fn = ".".join([fn, suffix])
ef_vals = {
"name": self.upload_fname,
"data": self.upload_data,
"date": fields.Datetime.now(),
"format_id": self.format_id.id,
"state": "done",
"user_id": self._uid,
"ebics_userid_id": self.ebics_userid_id.id,
"note": ef_note,
"company_ids": [
self.env.context.get("force_company", self.env.company.id)
],
}
self._update_ef_vals(ef_vals)
ebics_file = self.env["ebics.file"].create(ef_vals)
except EbicsFunctionalError:
e = exc_info()
self.note += "\n"
self.note += _("EBICS Functional Error:")
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsTechnicalError:
e = exc_info()
self.note += "\n"
self.note += _("EBICS Technical Error:")
self.note += "\n"
self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsVerificationError:
self.note += "\n"
self.note += _("EBICS Verification Error:")
self.note += "\n"
self.note += _("The EBICS response could not be verified.")
except Exception:
self.note += "\n"
self.note += _("Unknown Error")
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
if self.ebics_config_id.ebics_version == "H003":
OrderID = self.ebics_config_id._get_order_number()
self.ebics_config_id.sudo()._update_order_number(OrderID)
return ebics_file
def _setup_client(self):
self.ebics_config_id._check_ebics_keys()
passphrase = self._get_passphrase()
keyring = EbicsKeyRing(
keys=self.ebics_userid_id.ebics_keys_fn, passphrase=passphrase
)
bank = EbicsBank(
keyring=keyring,
hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url,
)
if self.ebics_config_id.ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number()
user = EbicsUser(
keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner,
userid=self.ebics_userid_id.name,
)
signature_class = (
self.format_id.signature_class or self.ebics_userid_id.signature_class
)
if signature_class == "T":
user.manual_approval = True
try:
client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception:
self.note += "\n"
self.note += _("Unknown Error")
tb = "".join(format_exception(*exc_info()))
self.note += "\n%s" % tb
client = False
return client
def _get_passphrase(self):
passphrase = self.ebics_userid_id.ebics_passphrase
if passphrase:
return passphrase
module = __name__.split("addons.")[1].split(".")[0]
passphrase_view = self.env.ref("%s.ebics_xfer_view_form_passphrase" % module)
return {
"name": _("EBICS file transfer"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": passphrase_view.id,
"target": "new",
"context": self._context,
"type": "ir.actions.act_window",
}
def _file_format_methods(self):
"""
Extend this dictionary in order to add support
for extra file formats.
"""
res = {
"camt.xxx.cfonb120.stm": self._handle_cfonb120,
"camt.xxx.cfonb120.stm.rfi": self._handle_cfonb120,
"camt.052.001.02.stm": self._handle_camt052,
"camt.053.001.02.stm": self._handle_camt053,
}
return res
def _update_ef_vals(self, ef_vals):
"""
Adapt this method to customize the EBICS File values.
"""
if self.format_id and self.format_id.type == "up":
fn = ef_vals["name"]
dups = self._check_duplicate_ebics_file(fn, self.format_id)
if dups:
n = 1
fn = "_".join([fn, str(n)])
while self._check_duplicate_ebics_file(fn, self.format_id):
n += 1
fn = "_".join([fn, str(n)])
ef_vals["name"] = fn
def _handle_download_data(self, data, file_format):
ebics_files = self.env["ebics.file"]
if isinstance(data, dict):
for doc in data:
ebics_files += self._create_ebics_file(
data[doc], file_format, docname=doc
)
else:
ebics_files += self._create_ebics_file(data, file_format)
return ebics_files
def _create_ebics_file(self, data, file_format, docname=None):
"""
Write the data as received over the EBICS connection
to a temporary file so that is is available for
analysis (e.g. in case formats are received that cannot
be handled in the current version of this module).
TODO: add code to clean-up /tmp on a regular basis.
After saving the data received we call the method to perform
file format specific processing.
"""
ebics_files_root = self.ebics_config_id.ebics_files
tmp_dir = os.path.normpath(ebics_files_root + "/tmp")
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner]
if docname:
fn_parts.append(docname)
else:
fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date.isoformat())
base_fn = "_".join(fn_parts)
n = 1
full_tmp_fn = os.path.normpath(tmp_dir + "/" + base_fn)
while os.path.exists(full_tmp_fn):
n += 1
tmp_fn = base_fn + "_" + str(n).rjust(3, "0")
full_tmp_fn = os.path.normpath(tmp_dir + "/" + tmp_fn)
with open(full_tmp_fn, "wb") as f:
f.write(data)
ff_methods = self._file_format_methods()
if file_format.name in ff_methods:
data = ff_methods[file_format.name](data)
fn = base_fn
suffix = file_format.suffix
if suffix and not fn.endswith(suffix):
fn = ".".join([fn, suffix])
dups = self._check_duplicate_ebics_file(fn, file_format)
if dups:
raise UserError(
_(
"EBICS File with name '%s' has already been downloaded."
"\nPlease check this file and rename in case there is "
"no risk on duplicate transactions."
)
% fn
)
data = base64.encodebytes(data)
ef_vals = {
"name": fn,
"data": data,
"date": fields.Datetime.now(),
"date_from": self.date_from,
"date_to": self.date_to,
"format_id": file_format.id,
"user_id": self._uid,
"ebics_userid_id": self.ebics_userid_id.id,
"company_ids": self.ebics_config_id.company_ids.ids,
}
self._update_ef_vals(ef_vals)
ebics_file = self.env["ebics.file"].create(ef_vals)
return ebics_file
def _check_duplicate_ebics_file(self, fn, file_format):
dups = self.env["ebics.file"].search(
[("name", "=", fn), ("format_id", "=", file_format.id)]
)
return dups
def _detect_upload_format(self):
"""
Use this method in order to automatically detect and set the
EBICS upload file format.
"""
def _update_order_number(self, OrderID):
o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1):
if c == "9":
o_list[-i] = "A"
break
if c == "Z":
continue
else:
o_list[-i] = chr(ord(c) + 1)
break
next_nr = "".join(o_list)
if next_nr == "ZZZZ":
next_nr = "A000"
self.ebics_config_id.order_number = next_nr
def _insert_line_terminator(self, data_in, line_len):
data_in = data_in.replace(b"\n", b"").replace(b"\r", b"")
data_out = b""
max_len = len(data_in)
i = 0
while i + line_len <= max_len:
data_out += data_in[i : i + line_len] + b"\n"
i += line_len
return data_out
def _handle_cfonb120(self, data_in):
return self._insert_line_terminator(data_in, 120)
def _handle_cfonb240(self, data_in):
return self._insert_line_terminator(data_in, 240)
def _handle_camt052(self, data_in):
"""
Use this method if you need to fix camt files received
from your bank before passing them to the
Odoo Community CAMT parser.
Remark: Odoo Enterprise doesn't support camt.052.
"""
return data_in
def _handle_camt053(self, data_in):
"""
Use this method if you need to fix camt files received
from your bank before passing them to the
Odoo Enterprise or Community CAMT parser.
"""
return data_in

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_xfer_view_form_download" model="ir.ui.view">
<field name="name">EBICS File Download</field>
<field name="model">ebics.xfer</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File Download">
<group>
<separator string="Select your bank :" colspan="2" />
<field
name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id)]"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field name="date_from" />
<field name="date_to" />
<field
name="format_id"
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field name="allowed_format_ids" invisible="1" />
</group>
<footer>
<button
name="ebics_download"
string="Download Files"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_view_form_upload" model="ir.ui.view">
<field name="name">EBICS File Upload</field>
<field name="model">ebics.xfer</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="EBICS File Upload">
<group>
<separator string="Select your bank :" colspan="2" />
<field
name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id)]"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<separator string="Select your file :" colspan="2" />
<field name="upload_data" filename="upload_fname" required="1" />
<field name="upload_fname" invisible="1" />
<field name="upload_fname_dummy" />
<field
name="format_id"
required="1"
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field
name="test_mode"
attrs="{'invisible': [('order_type', 'not in', ('FUL', 'BTU'))]}"
/>
<field name="allowed_format_ids" invisible="1" />
</group>
<footer>
<button
name="ebics_upload"
string="Upload File"
type="object"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_view_form_result" model="ir.ui.view">
<field name="name">EBICS File Transfer</field>
<field name="model">ebics.xfer</field>
<field name="priority">2</field>
<field name="arch" type="xml">
<form string="EBICS File Transfer">
<separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer>
<button
name="view_ebics_file"
type="object"
string="View EBICS File(s)"
class="oe_highlight"
invisible="not context.get('ebics_file_ids')"
/>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>
</record>
<record id="ebics_xfer_action_download" model="ir.actions.act_window">
<field name="name">EBICS File Transfer</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.xfer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_download': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_download" />
</record>
<record id="ebics_xfer_action_upload" model="ir.actions.act_window">
<field name="name">EBICS File Transfer</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.xfer</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_upload': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_upload" />
</record>
</odoo>

View File

@ -0,0 +1,50 @@
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-3
============================================
Module to enable batch import of EBICS files
============================================
This module adds a cron job for the automated import of EBICS files.
|
A Log is created during the import in order to document import errors.
If errors have been detected, the Batch Import Log state is set to 'error'.
When all EBICS Files have been imported correctly, the Batch Import Log state is set to 'done'.
|
The user can reprocess the imported EBICS files in status 'draft' via the Log object 'REPROCESS' button until all errors have been cleared.
As an alternative, the user can force the Batch Import Log state to 'done'
(e.g. when the errors have been circumvented via manual encoding or the reprocessing of a single EBICS file).
|
Configuration
=============
Adapt the 'EBICS Batch Import' ir.cron job created during the module installation.
The cron job calls the following python method:
|
.. code-block:: python
_batch_import()
The EBICS download will be performed on all confirmed EBICS connections.
You can limit the automated operation to a subset of your EBICS connections via the ebics_config_ids parameter, e.g.
|
.. code-block:: python
_batch_import(ebics_config_ids=[1,3])

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,20 @@
# Copyright 2009-2023 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "EBICS Files batch import",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Accounting & Finance",
"summary": "EBICS Files automated import and processing",
"depends": ["account_ebics"],
"data": [
"security/ir.model.access.csv",
"data/ir_cron_data.xml",
"views/ebics_batch_log_views.xml",
"views/menu.xml",
],
"installable": True,
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record id="ir_cron_ebics_batch_import" model="ir.cron">
<field name="name">EBICS Batch Import</field>
<field name="model_id" ref="model_ebics_batch_log" />
<field name="state">code</field>
<field name="code">model._batch_import()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active" eval="False" />
<field name="doall" eval="False" />
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import ebics_batch_log

View File

@ -0,0 +1,190 @@
# Copyright 2009-2023 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from sys import exc_info
from traceback import format_exception
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class EbicsBatchLog(models.Model):
_name = "ebics.batch.log"
_description = "Object to store EBICS Batch Import Logs"
_order = "create_date desc"
date_from = fields.Date()
date_to = fields.Date()
ebics_config_ids = fields.Many2many(
comodel_name="ebics.config", string="EBICS Configurations"
)
log_ids = fields.One2many(
comodel_name="ebics.batch.log.item",
inverse_name="log_id",
string="Batch Import Log Items",
readonly=True,
)
file_ids = fields.Many2many(
comodel_name="ebics.file",
string="Batch Import EBICS Files",
readonly=True,
)
file_count = fields.Integer(
string="EBICS Files Count", compute="_compute_ebics_files_fields", readonly=True
)
has_draft_files = fields.Boolean(compute="_compute_ebics_files_fields")
state = fields.Selection(
selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")],
required=True,
readonly=True,
default="draft",
)
@api.depends("file_ids")
def _compute_ebics_files_fields(self):
for rec in self:
rec.has_draft_files = "draft" in rec.file_ids.mapped("state")
rec.file_count = len(rec.file_ids)
def unlink(self):
for log in self:
if log.state != "draft":
raise UserError(_("Only log objects in state 'draft' can be deleted !"))
return super().unlink()
def button_draft(self):
self.state = "draft"
def button_done(self):
self.state = "done"
def reprocess(self):
import_dict = {"errors": []}
self._ebics_process(import_dict)
self._finalise_processing(import_dict)
def view_ebics_files(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"account_ebics.ebics_file_action_download"
)
action["domain"] = [("id", "in", self.file_ids.ids)]
return action
def _batch_import(self, ebics_config_ids=None, date_from=None, date_to=None):
"""
Call this method from a cron job to automate the EBICS import.
"""
log_model = self.env["ebics.batch.log"]
import_dict = {"errors": []}
configs = self.env["ebics.config"].browse(ebics_config_ids) or self.env[
"ebics.config"
].search(
[
("company_ids", "in", self.env.user.company_ids.ids),
("state", "=", "confirm"),
]
)
log = log_model.create(
{
"ebics_config_ids": [(6, 0, configs.ids)],
"date_from": date_from,
"date_to": date_to,
}
)
ebics_file_ids = []
for config in configs:
err_msg = (
_("Error while processing EBICS connection '%s' :\n") % config.name
)
if config.state == "draft":
import_dict["errors"].append(
err_msg
+ _(
"Please set state to 'Confirm' and "
"Reprocess this EBICS Import Log."
)
)
continue
try:
with self.env.cr.savepoint():
ebics_file_ids += self._ebics_import(
config, date_from, date_to, import_dict
)
except UserError as e:
import_dict["errors"].append(err_msg + " ".join(e.args))
except Exception:
tb = "".join(format_exception(*exc_info()))
import_dict["errors"].append(err_msg + tb)
log.file_ids = [(6, 0, ebics_file_ids)]
try:
with self.env.cr.savepoint():
log._ebics_process(import_dict)
except UserError as e:
import_dict["errors"].append(err_msg + " ".join(e.args))
except Exception:
tb = "".join(format_exception(*exc_info()))
import_dict["errors"].append(err_msg + tb)
log._finalise_processing(import_dict)
def _finalise_processing(self, import_dict):
log_item_model = self.env["ebics.batch.log.item"]
state = self.has_draft_files and "draft" or "done"
note = ""
error_count = 0
if import_dict["errors"]:
state = "error"
note = "\n\n".join(import_dict["errors"])
error_count = len(import_dict["errors"])
log_item_model.create(
{
"log_id": self.id,
"state": state,
"note": note,
"error_count": error_count,
}
)
self.state = state
def _ebics_import(self, config, date_from, date_to, import_dict):
xfer_wiz = (
self.env["ebics.xfer"]
.with_context(ebics_download=True)
.create(
{
"ebics_config_id": config.id,
"date_from": date_from,
"date_to": date_to,
}
)
)
xfer_wiz._onchange_ebics_config_id()
res = xfer_wiz.ebics_download()
file_ids = res["context"].get("ebics_file_ids")
if res["context"]["err_cnt"]:
import_dict["errors"].append(xfer_wiz.note)
return file_ids
def _ebics_process(self, import_dict):
to_process = self.file_ids.filtered(lambda r: r.state == "draft")
for ebics_file in to_process:
ebics_file.process()
class EbicsBatchLogItem(models.Model):
_name = "ebics.batch.log.item"
_description = "Object to store EBICS Batch Import Log Items"
_order = "create_date desc"
log_id = fields.Many2one(
comodel_name="ebics.batch.log",
string="Batch Object",
ondelete="cascade",
readonly=True,
)
state = fields.Selection(
selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")],
required=True,
readonly=True,
)
note = fields.Text(string="Batch Import Log", readonly=True)
error_count = fields.Integer(string="Number of Errors", required=True, default=0)

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ebics_batch_log,ebics.batch.log,model_ebics_batch_log,account.group_account_invoice,1,1,1,1
access_ebics_batch_log_item,ebics.batch.log.item,model_ebics_batch_log_item,account.group_account_invoice,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ebics_batch_log ebics.batch.log model_ebics_batch_log account.group_account_invoice 1 1 1 1
3 access_ebics_batch_log_item ebics.batch.log.item model_ebics_batch_log_item account.group_account_invoice 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,118 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_batch_log_view_search" model="ir.ui.view">
<field name="name">ebics.batch.log.search</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<search string="Search EBICS Batch Import Log Files">
<group col="10" colspan="4">
<field name="create_date" />
<field name="state" />
</group>
<newline />
<group expand="0" string="Group By">
<filter name="group_by_state" string="State" context="{'group_by':'state'}" />
</group>
</search>
</field>
</record>
<record id="ebics_batch_log_view_tree" model="ir.ui.view">
<field name="name">ebics.batch.log.tree</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<tree create="false">
<field name="create_date" />
<field name="file_count" />
<field name="state" />
</tree>
</field>
</record>
<record id="ebics_batch_log_view_form" model="ir.ui.view">
<field name="name">ebics.batch.log.form</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<form string="EBICS Batch Import Log" create="false">
<header>
<button
name="button_draft"
states="done,error"
string="Set to Draft"
type="object"
/>
<button
name="reprocess"
string="Reprocess"
help="Reprocess 'draft' EBICS Files"
type="object"
class="oe_highlight"
attrs="{'invisible': ['|', ('state', '=', 'done'),('has_draft_files', '=', False)]}"
/>
<button
name="button_done"
states="draft,error"
string="Mark Done"
type="object"
/>
<field
name="state"
widget="statusbar"
statusbar_visible="draft,done"
statusbar_colors="{'error':'red'}"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="view_ebics_files"
type="object"
class="oe_stat_button"
icon="fa-pencil-square-o"
attrs="{'invisible': [('file_count', '=', 0)]}"
>
<field name="file_count" widget="statinfo" string="EBICS Files" />
</button>
</div>
<group colspan="4" col="4">
<field name="create_date" />
<field name="ebics_config_ids" widget="many2many_tags" />
<field name="has_draft_files" invisible="1" />
</group>
<notebook colspan="4">
<page string="Batch Import Logs">
<field name="log_ids" nolabel="1">
<tree>
<field name="create_date" />
<field name="state" />
<field name="error_count" />
</tree>
<form string="Batch Import Log">
<group colspan="4" col="6">
<field name="create_date" />
<field name="error_count" />
</group>
<group attrs="{'invisible':[('note', '=', False)]}">
<separator colspan="4" />
<field name="note" nolabel="1" colspan="4" height="360" />
</group>
</form>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="ebics_batch_log_action" model="ir.actions.act_window">
<field name="name">EBICS Batch Import Logs</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.batch.log</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="ebics_batch_log_view_tree" />
<field name="search_view_id" ref="ebics_batch_log_view_search" />
</record>
</odoo>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<odoo>
<menuitem
id="ebics_batch_log_menu"
name="EBICS Batch Import Logs"
parent="account_ebics.ebics_processing_menu"
action="ebics_batch_log_action"
sequence="100"
/>
</odoo>

View File

@ -0,0 +1,24 @@
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: https://www.gnu.org/licenses/lpgl
:alt: License: AGPL-3
==============================
Upload Batch Payment via EBICS
==============================
This module allows to upload a Batch Payment to the bank via the EBICS protocol.
Installation
============
This module depends upon the following modules:
- account_ebics (cf. https://github.com/Noviat/account_ebics)
- account_ebics_oe (cf. https://github.com/Noviat/account_ebics)
- account_batch_payment (Odoo Enterprise)
Usage
=====
Create your Batch Payment and generate the bank file.
Upload the generated file via the 'EBICS Upload' button on the batch payment.

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,14 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
"name": "Upload Batch Payment via EBICS",
"version": "16.0.1.0.0",
"license": "LGPL-3",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Accounting & Finance",
"depends": ["account_ebics", "account_batch_payment"],
"data": ["views/account_batch_payment_views.xml"],
"installable": True,
}

View File

@ -0,0 +1 @@
from . import account_batch_payment

View File

@ -0,0 +1,53 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
from odoo import _, models
from odoo.exceptions import UserError
class AccountBatchPayment(models.Model):
_inherit = "account.batch.payment"
def ebics_upload(self):
self.ensure_one()
ctx = self.env.context.copy()
origin = _("Batch Payment") + ": " + self.name
ebics_config = self.env["ebics.config"].search(
[
("journal_ids", "=", self.journal_id.id),
("state", "=", "confirm"),
]
)
if not ebics_config:
raise UserError(
_("No active EBICS configuration available " "for the selected bank.")
)
if len(ebics_config) == 1:
ctx["default_ebics_config_id"] = ebics_config.id
ctx.update(
{
"default_upload_data": self.export_file,
"default_upload_fname": self.export_filename,
"origin": origin,
"force_comany": self.journal_id.company_id.id,
}
)
ebics_xfer = self.env["ebics.xfer"].with_context(**ctx).create({})
ebics_xfer._onchange_ebics_config_id()
ebics_xfer._onchange_upload_data()
ebics_xfer._onchange_format_id()
view = self.env.ref("account_ebics.ebics_xfer_view_form_upload")
act = {
"name": _("EBICS Upload"),
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": view.id,
"res_id": ebics_xfer.id,
"type": "ir.actions.act_window",
"target": "new",
"context": ctx,
}
return act

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_batch_payment_form" model="ir.ui.view">
<field name="name">account.batch.payment.form</field>
<field name="model">account.batch.payment</field>
<field name="inherit_id" ref="account_batch_payment.view_batch_payment_form" />
<field name="arch" type="xml">
<button name="validate_batch_button" position="after">
<button
name="ebics_upload"
type="object"
attrs="{'invisible': ['|', ('file_generation_enabled', '=', False), ('state', '!=', 'sent')]}"
string="EBICS Upload"
/>
</button>
</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: https://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3
==========================================================
Deploy account_ebics module with OCA Bank Statement Import
==========================================================
This module makes it possible to use OCA account_statement_import
in combination with 'account_ebics'.
This module will be installed automatically when following modules are activated
on your odoo database :
- account_ebics
- account_statement_import

View File

@ -0,0 +1 @@
from . import wizards

View File

@ -0,0 +1,20 @@
# Copyright 2020-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
"name": "account_ebics with OCA Bank Statement Imoort",
"summary": "Use OCA Bank Statement Import with account_ebics",
"version": "16.0.1.0.0",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Hidden",
"license": "LGPL-3",
"depends": [
"account_ebics",
"account_statement_import",
],
# installable False unit OCA statement import becomes
# available for 16.0
"installable": False,
"auto_install": True,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1 @@
from . import account_statement_import

View File

@ -0,0 +1,60 @@
# Copyright 2009-2020 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging
from odoo import _, models
_logger = logging.getLogger(__name__)
class AccountStatementImport(models.TransientModel):
_inherit = "account.statement.import"
def _check_parsed_data(self, stmts_vals):
"""Basic and structural verifications"""
if self.env.context.get("active_model") == "ebics.file":
message = False
if len(stmts_vals) == 0:
message = _("This file doesn't contain any statement.")
if not message:
no_st_line = True
for vals in stmts_vals:
if vals["transactions"] and len(vals["transactions"]) > 0:
no_st_line = False
break
if no_st_line:
message = _("This file doesn't contain any transaction.")
if message:
log_msg = (
_("Error detected while processing and EBICS File")
+ ":\n"
+ message
)
_logger.warn(log_msg)
return
return super()._check_parsed_data(stmts_vals)
def _create_bank_statements(self, stmts_vals, result):
"""
Return error message to ebics.file when handling empty camt.
Remarks/TODO:
We could add more info to the message (e.g. date, balance, ...)
and write this to the ebics.file, note field.
We could also create empty bank statement (in state done) to clearly
show days without transactions via the bank statement list view.
"""
if self.env.context.get("active_model") == "ebics.file":
transactions = False
for st_vals in stmts_vals:
if st_vals.get("transactions"):
transactions = True
break
if not transactions:
message = _("This file doesn't contain any transaction.")
st_line_ids = []
notifications = {"type": "warning", "message": message, "details": ""}
return st_line_ids, [notifications]
return super()._create_bank_statements(stmts_vals, result)

View File

@ -0,0 +1,16 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: https://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3
==============================================
Deploy account_ebics module on Odoo Enterprise
==============================================
This module makes it possible to deploy the 'account_ebics'
module on Odoo Enterprise.
This module will be installed automatically when following modules are activated
on your odoo database :
- account_ebics
- account_accountant

View File

View File

@ -0,0 +1,19 @@
# Copyright 2020-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
"name": "account_ebics on Odoo Enterprise",
"summary": "Deploy account_ebics module on Odoo Enterprise",
"version": "16.0.1.0.0",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Hidden",
"license": "LGPL-3",
"depends": [
"account_ebics",
"account_accountant",
],
"data": ["views/account_ebics_menu.xml"],
"installable": True,
"auto_install": True,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_ebics.ebics_processing_menu" model="ir.ui.menu">
<field name="parent_id" eval="ref('account_accountant.menu_accounting')" />
</record>
</odoo>

View File

@ -0,0 +1,28 @@
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: https://www.gnu.org/licenses/lpgl
:alt: License: AGPL-3
==============================
Upload Payment Order via EBICS
==============================
This module allows to upload a Payment Order to the bank via the EBICS protocol.
Installation
============
This module depends upon the following modules (cf. apps.odoo.com):
- account_ebics
- account_payment_order
Usage
=====
Create your Payment Order and generate the bank file.
Upload the generated file via the 'EBICS Upload' button on the payment order.
Known issues / Roadmap
======================
* Add support for multiple EBICS connections.

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,18 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
"name": "Upload Payment Order via EBICS",
"version": "16.0.1.0.0",
"license": "LGPL-3",
"author": "Noviat",
"website": "https://www.noviat.com",
"category": "Accounting & Finance",
"depends": ["account_ebics", "account_payment_order"],
"data": [
"views/account_payment_order_views.xml",
],
# installable False unit OCA payment order becomes
# available for 16.0
"installable": False,
}

View File

@ -0,0 +1 @@
from . import account_payment_order

View File

@ -0,0 +1,74 @@
# Copyright 2009-2023 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
from odoo import _, models
from odoo.exceptions import UserError
class AccountPaymentOrder(models.Model):
_inherit = "account.payment.order"
def ebics_upload(self):
self.ensure_one()
attach = self.env["ir.attachment"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id)]
)
if not attach:
raise UserError(
_(
"This payment order doesn't contains attachements."
"\nPlease generate first the Payment Order file first."
)
)
elif len(attach) > 1:
raise UserError(
_(
"This payment order contains multiple attachments."
"\nPlease remove the obsolete attachments or upload "
"the payment order file via the "
"EBICS Processing > EBICS Upload menu"
)
)
else:
origin = _("Payment Order") + ": " + self.name
ebics_config = self.env["ebics.config"].search(
[
("journal_ids", "=", self.journal_id.id),
("state", "=", "confirm"),
]
)
if not ebics_config:
raise UserError(
_(
"No active EBICS configuration available "
"for the selected bank."
)
)
ctx = self.env.context.copy()
if len(ebics_config) == 1:
ctx["default_ebics_config_id"] = ebics_config.id
ctx.update(
{
"default_upload_data": attach.datas,
"default_upload_fname": attach.name,
"origin": origin,
"force_comany": self.company_id.id,
}
)
ebics_xfer = self.env["ebics.xfer"].with_context(**ctx).create({})
ebics_xfer._onchange_ebics_config_id()
ebics_xfer._onchange_upload_data()
ebics_xfer._onchange_format_id()
view = self.env.ref("account_ebics.ebics_xfer_view_form_upload")
act = {
"name": _("EBICS Upload"),
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": view.id,
"res_id": ebics_xfer.id,
"type": "ir.actions.act_window",
"target": "new",
"context": ctx,
}
return act

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_payment_order_form" model="ir.ui.view">
<field name="name">account.payment.order.form</field>
<field name="model">account.payment.order</field>
<field name="inherit_id" ref="account_payment_order.account_payment_order_form" />
<field name="arch" type="xml">
<button name="open2generated" position="after">
<button
name="ebics_upload"
type="object"
states="generated"
string="EBICS Upload"
class="oe_highlight"
/>
</button>
</field>
</record>
</odoo>

View File

@ -0,0 +1,2 @@
# addons listed in this file are ignored by
# setuptools-odoo-make-default (one addon per line)

2
setup/README Normal file
View File

@ -0,0 +1,2 @@
To learn more about this directory, please visit
https://pypi.python.org/pypi/setuptools-odoo

View File

@ -0,0 +1 @@
../../../../account_ebics

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_batch

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_batch_payment

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_oe

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)