Merge pull request #30 from Noviat/14.0-mig-account_ebics

14.0 mig account ebics
This commit is contained in:
Luc De Meyer 2021-04-11 20:39:24 +02:00 committed by GitHub
commit c746eaf34c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 3864 additions and 0 deletions

191
account_ebics/README.rst Normal file
View File

@ -0,0 +1,191 @@
.. 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.
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
|
- account_ebics_batch_payment
Recommended if you are using the Odoo Enterprise account_batch_payment module
|
- account_ebics_payment_order
Recommended if you are using the OCA account_payment_order module.
Cf. 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 for EBICS 3.0
- add support to import externally generated keys & certificates (currently only 3SKey signature certificate)

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-2021 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
'name': 'EBICS banking protocol',
'version': '14.0.1.0.0',
'license': 'LGPL-3',
'author': 'Noviat',
'website': '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,154 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Download formats -->
<record id="ebics_ff_C52" model="ebics.file.format">
<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="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="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="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="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="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="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="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="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>
<!-- Upload formats -->
<record id="ebics_ff_LCR" model="ebics.file.format">
<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="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="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="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="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="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="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="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>
</data>
</odoo>

Binary file not shown.

View File

@ -0,0 +1,18 @@
<?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,73 @@
# Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openupgradelib import openupgrade # pylint: disable=W7936
import os
@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.id])],
'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,41 @@
# 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(
"""
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(
"""
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,70 @@
# 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(
"""
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)

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,11 @@
# Copyright 2009-2020 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,197 @@
# Copyright 2009-2020 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging
import re
import os
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(
string='Name',
readonly=True, states={'draft': [('readonly', False)]},
required=True)
journal_ids = fields.Many2many(
comodel_name='account.journal',
readonly=True, states={'draft': [('readonly', False)]},
string='Bank Accounts',
domain="[('type', '=', 'bank')]",
required=True)
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)')],
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')],
string='State',
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(
string='Active', default=True)
company_ids = fields.Many2many(
comodel_name='res.company',
string='Companies',
required=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('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}"))
@api.onchange('journal_ids')
def _onchange_journal_ids(self):
self.company_ids = self.journal_ids.mapped('company_id')
def unlink(self):
for ebics_config in self:
if ebics_config.state == 'active':
raise UserError(_(
"You cannot remove active EBICS configurations."))
return super(EbicsConfig, self).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':
continue
else:
o_list[-i] = chr(ord(c) + 1)
break
next = ''.join(o_list)
if next == 'ZZZZ':
next = 'A000'
self.order_number = next
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,426 @@
# Copyright 2009-2021 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import base64
import logging
from odoo import _, fields, models
from odoo.exceptions import UserError
_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')],
string='State',
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')
company_ids = fields.Many2many(
comodel_name='res.company',
string='Companies',
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(EbicsFile, self).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()
ctx = dict(
self.env.context,
allowed_company_ids=self.env.user.company_ids.ids)
self = self.with_context(ctx)
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)
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 = eval(action.get('domain') or '[]')
domain += [('id', 'in', self._context.get('statement_ids'))]
action.update({'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 '%s' format is not installed "
"on your system. "
"\nPlease install module '%s'")
% (self.format_id.name, module))
return False
return True
def _process_result_action(self, res):
notifications = []
st_line_ids = []
statement_ids = []
if res.get('context'):
notifications = res['context'].get('notifications', [])
st_line_ids = res['context'].get('statement_line_ids', [])
if notifications:
for notif in notifications:
parts = []
for k in ['type', 'message', 'details']:
if notif.get(k):
msg = '%s: %s' % (k, notif[k])
parts.append(msg)
self.note_process += '\n'.join(parts)
self.note_process += '\n'
self.note_process += '\n'
if st_line_ids:
self.flush()
self.env.cr.execute(
"""
SELECT DISTINCT
absl.statement_id,
abs.name, abs.date, abs.company_id,
rc.name AS company_name
FROM account_bank_statement_line absl
INNER JOIN account_bank_statement abs
ON abs.id = absl.statement_id
INNER JOIN res_company rc
ON rc.id = abs.company_id
WHERE absl.id IN %s
ORDER BY date, company_id
""",
(tuple(st_line_ids),)
)
sts_data = self.env.cr.dictfetchall()
else:
sts_data = []
st_cnt = len(sts_data)
if st_cnt:
self.note_process += _(
"%s bank statements have been imported: "
) % st_cnt
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'])
statement_ids = [x['statement_id'] for x in sts_data]
if statement_ids:
self.sudo().bank_statement_ids = [(6, 0, statement_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',
}
@staticmethod
def _process_cfonb120(self):
"""
We do not support the standard _journal_creation_wizard since a single
cfonb120 file may contain statements from different legal entities.
"""
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 = {
'type': 'ir.actions.client',
'tag': 'bank_statement_reconciliation_view',
'context': {'statement_line_ids': [],
'company_ids': self.env.user.company_ids.ids,
'notifications': []},
}
wiz_ctx = dict(self.env.context, active_model='ebics.file')
for i, wiz_vals in enumerate(wiz_vals_list, start=1):
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button()
ctx = res.get('context')
if (res.get('res_model')
== 'account.bank.statement.import.journal.creation'):
message = _(
"Error detected while importing statement number %s.\n"
) % i
message += _("No financial journal found.")
details = _(
'Bank account number: %s'
) % ctx.get('default_bank_acc_number')
result['context']['notifications'].extend([{
'type': 'warning',
'message': message,
'details': details,
}])
continue
result['context']['statement_line_ids'].extend(
ctx['statement_line_ids'])
result['context']['notifications'].extend(
ctx['notifications'])
return self._process_result_action(result)
@staticmethod
def _unlink_cfonb120(self):
"""
Placeholder for cfonb120 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt052(self):
import_module = 'account_statement_import_camt'
self._check_import_module(import_module)
return self._process_camt053(self)
@staticmethod
def _unlink_camt052(self):
"""
Placeholder for camt052 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt054(self):
import_module = 'account_statement_import_camt'
self._check_import_module(import_module)
return self._process_camt053(self)
@staticmethod
def _unlink_camt054(self):
"""
Placeholder for camt054 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt053(self):
modules = [
('oca', 'account_statement_import_camt'),
('oe', 'account_bank_statement_import_camt'),
]
found = False
for src, mod in modules:
if self._check_import_module(mod, raise_if_not_found=False):
found = True
break
if not found:
raise UserError(_(
"The module to process the '%s' format is not installed "
"on your system. "
"\nPlease install one of the following modules: \n%s."
) % (self.format_id.name, ', '.join([x[1] for x in modules]))
)
if src == 'oca':
self._process_camt053_oca()
else:
self._process_camt053_oe()
def _process_camt053_oca(self):
wiz_model = 'account.statement.import'
wiz_vals = {
'statement_filename': self.name,
'statement_file': self.data,
}
result = {
'type': 'ir.actions.client',
'tag': 'bank_statement_reconciliation_view',
'context': {'statement_line_ids': [],
'company_ids': self.env.user.company_ids.ids,
'notifications': []},
}
wiz_ctx = dict(self.env.context, active_model='ebics.file')
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button()
ctx = res.get('context')
if (res.get('res_model')
== 'account.bank.statement.import.journal.creation'):
message = _(
"Error detected while importing statement %s.\n"
) % self.name
message += _("No financial journal found.")
details = _(
'Bank account number: %s'
) % ctx.get('default_bank_acc_number')
result['context']['notifications'].extend([{
'type': 'warning',
'message': message,
'details': details,
}])
result['context']['statement_line_ids'].extend(
ctx['statement_line_ids'])
result['context']['notifications'].extend(
ctx['notifications'])
return self._process_result_action(result)
def _process_camt053_oe(self):
wiz_model = 'account.bank.statement.import'
wiz_vals = {
'attachment_ids': [(0, 0, {'name': self.name,
'datas': self.data,
'store_fname': self.name})]}
ctx = dict(self.env.context, active_model='ebics.file')
wiz = self.env[wiz_model].with_context(ctx).create(wiz_vals)
res = wiz.import_file()
if res.get('res_model') \
== 'account.bank.statement.import.journal.creation':
if res.get('context'):
bank_account = res['context'].get('default_bank_acc_number')
raise UserError(_(
"No financial journal found for Company Bank Account %s"
) % bank_account)
return self._process_result_action(res)
@staticmethod
def _unlink_camt053(self):
"""
Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_pain002(self):
"""
Placeholder for processing pain.002 files.
TODO:
add import logic based upon OCA 'account_payment_return_import'
"""
pass
@staticmethod
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,62 @@
# Copyright 2009-2020 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'
name = fields.Char(
string='Request Type',
required=True,
help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
"Specify camt.052, camt.053, camt.054 for camt "
"Order Types such as C53, Z53, C54, Z54.\n"
"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(
string='Order Type',
required=True,
help="E.g. C53 (check your EBICS contract).\n"
"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')],
string='Signature Class',
help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments."
"\nLeave this field empty to use the default "
"defined for your EBICS UserID.")
description = fields.Char()
suffix = fields.Char(
required=True,
help="Specify the filename suffix for this File Format."
"\nE.g. c53.xml")
@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

View File

@ -0,0 +1,466 @@
# Copyright 2009-2020 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 EbicsKeyRing, EbicsBank, EbicsUser,\
EbicsClient, EbicsFunctionalError, EbicsTechnicalError
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')
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')],
string='Signature Class',
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')],
string='State',
default='draft',
required=True, readonly=True)
active = fields.Boolean(
string='Active', 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 + '_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_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('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):
"""
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:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value)
raise UserError(error)
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.decodestring(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:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value)
raise UserError(error)
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)
client = EbicsClient(
bank, user, version=ebics_version)
# 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 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%s: %s " % (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:
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 '%s' - %s")
% (self.ebics_config_id.ebics_url, str(value)))
except EbicsFunctionalError:
e = exc_info()
error = _("EBICS Functional Error:")
error += '\n'
error += '%s (code: %s)' % (e[1].message, e[1].code)
raise UserError(error)
except EbicsTechnicalError:
e = exc_info()
error = _("EBICS Technical Error:")
error += '\n'
error += '%s (code: %s)' % (e[1].message, e[1].code)
raise UserError(error)
# 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:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value)
raise UserError(error)
try:
public_bank_keys = client.HPB()
except EbicsFunctionalError:
e = exc_info()
error = _("EBICS Functional Error:")
error += '\n'
error += '%s (code: %s)' % (e[1].message, e[1].code)
raise UserError(error)
except Exception:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value)
raise UserError(error)
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.encodestring(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,45 @@
# 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,34 @@
<?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,513 @@
<?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.12: http://docutils.sourceforge.net/" />
<title></title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7614 2013-02-21 15:55:51Z 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 }
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 {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.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;
}
.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 } */
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.</p>
<p>SWIFT 3SKey support requires Fintech 6.4 or higher.
|</p>
<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>
</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>
</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/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_bank_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_fr">https://github.com/OCA/l10n_fr</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_bank_statement_import_camt_oca</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_bank_statement_import_helper</p>
<p>Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files).</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 for EBICS 3.0</li>
<li>add support to import externally generated keys &amp; certificates (currently only 3SKey signature certificate)</li>
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,74 @@
<?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 string="EBICS Configuration" 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', '=', 'H004')]}"/>
</group>
<field name="company_ids" invisible="1"/>
</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,50 @@
<?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 string="EBICS File Formats">
<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="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"/>
</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,199 @@
<?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 string="EBICS Files" 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 string="EBICS Files" 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,89 @@
<?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 string="EBICS UserID" 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 groups="account_ebics.group_ebics_manager">
<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"/>
<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 col="4" name="dn" attrs="{'invisible': [('ebics_key_x509', '=', False)], 'readonly': [('state', '!=', 'draft')]}">
<group colspan="4" col="1">
<strong>Distinguished Name attributes used to create self-signed X.509 certificates:</strong>
</group>
<group name="dn_l" colspan="2">
<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" colspan="2">
<field name="ebics_key_x509_dn_e"/>
<field name="ebics_key_x509_dn_ou"/>
<field name="ebics_key_x509_dn_st"/>
</group>
</group>
<group colspan="4" 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="4" 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>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,58 @@
<?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,75 @@
# Copyright 2009-2020 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 e:
raise UserError(str(e))
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,39 @@
<?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,567 @@
# Copyright 2009-2021 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 EbicsKeyRing, EbicsBank, EbicsUser, EbicsClient,\
EbicsFunctionalError, EbicsTechnicalError, 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',
help="For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.")
test_mode = fields.Boolean(
string='Test Mode',
help="Select this option to test if the syntax of "
"the upload file is correct."
"\nThis option is only available for "
"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._context.copy()
self.note = ''
client = self._setup_client()
if client:
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 == '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:
e = exc_info()
self.note += '\n'
self.note += _(
"EBICS Functional Error during download of File Format %s (%s):"
) % (df.name, df.order_type)
self.note += '\n'
self.note += '%s (code: %s)' % (e[1].message, e[1].code)
except EbicsTechnicalError:
e = exc_info()
self.note += '\n'
self.note += _(
"EBICS Technical Error during download of File Format %s (%s):"
) % (df.name, df.order_type)
self.note += '\n'
self.note += '%s (code: %s)' % (e[1].message, e[1].code)
except EbicsVerificationError:
self.note += '\n'
self.note += _(
"EBICS Verification Error during download of "
"File Format %s (%s):"
) % (df.name, df.order_type)
self.note += '\n'
self.note += _("The EBICS response could not be verified.")
except UserError as e:
self.note += '\n'
self.note += _(
"Warning during download of File Format %s (%s):"
) % (df.name, df.order_type)
self.note += '\n'
self.note += e.name
except Exception:
self.note += '\n'
self.note += _(
"Unknown Error during download of File Format %s (%s):"
) % (df.name, 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'
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.decodestring(self.upload_data)
ef_format = self.format_id
OrderID = False
try:
order_type = self.order_type
if order_type == 'FUL':
kwargs = {}
bank = self.ebics_config_id.journal_ids[0].bank_id
cc = bank.country.code
if cc:
kwargs['country'] = cc
if self.test_mode:
kwargs['TEST'] = 'TRUE'
OrderID = client.FUL(ef_format.name, upload_data, **kwargs)
else:
OrderID = client.upload(order_type, upload_data)
if OrderID:
self.note += '\n'
self.note += _(
"EBICS File has been uploaded (OrderID %s)."
) % OrderID
ef_note = _("EBICS OrderID: %s") % OrderID
if self.env.context.get('origin'):
ef_note += '\n' + _(
"Origin: %s") % self._context['origin']
suffix = self.format_id.suffix
fn = self.upload_fname
if not fn.endswith(suffix):
fn = '.'.join([fn, suffix])
ef_vals = {
'name': self.upload_fname,
'data': self.upload_data,
'date': fields.Datetime.now(),
'format_id': self.format_id.id,
'state': 'done',
'user_id': self._uid,
'ebics_userid_id': self.ebics_userid_id.id,
'note': ef_note,
"company_ids": [
self.env.context.get("force_company", self.env.company.id)
],
}
self._update_ef_vals(ef_vals)
ebics_file = self.env['ebics.file'].create(ef_vals)
except EbicsFunctionalError:
e = exc_info()
self.note += '\n'
self.note += _("EBICS Functional Error:")
self.note += '\n'
self.note += '%s (code: %s)' % (e[1].message, e[1].code)
except EbicsTechnicalError:
e = exc_info()
self.note += '\n'
self.note += _("EBICS Technical Error:")
self.note += '\n'
self.note += '%s (code: %s)' % (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' and OrderID:
self.ebics_config_id._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.052.001.02.stm': self._handle_camt052,
'camt.053.001.02.stm': self._handle_camt053,
}
return res
def _update_ef_vals(self, ef_vals):
"""
Adapt this method to customize the EBICS File values.
"""
if self.format_id and self.format_id.type == 'up':
fn = ef_vals['name']
dups = self._check_duplicate_ebics_file(
fn, self.format_id)
if dups:
n = 1
fn = '_'.join([fn, str(n)])
while self._check_duplicate_ebics_file(fn, self.format_id):
n += 1
fn = '_'.join([fn, str(n)])
ef_vals['name'] = fn
def _handle_download_data(self, data, file_format):
ebics_files = self.env['ebics.file']
if isinstance(data, dict):
for doc in data:
ebics_files += self._create_ebics_file(
data[doc], file_format, docname=doc)
else:
ebics_files += self._create_ebics_file(data, file_format)
return ebics_files
def _create_ebics_file(self, data, file_format, docname=None):
"""
Write the data as received over the EBICS connection
to a temporary file so that is is available for
analysis (e.g. in case formats are received that cannot
be handled in the current version of this module).
TODO: add code to clean-up /tmp on a regular basis.
After saving the data received we call the method to perform
file format specific processing.
"""
ebics_files_root = self.ebics_config_id.ebics_files
tmp_dir = os.path.normpath(ebics_files_root + '/tmp')
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_parts = [self.ebics_config_id.ebics_host,
self.ebics_config_id.ebics_partner]
if docname:
fn_parts.append(docname)
else:
fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date.isoformat())
base_fn = '_'.join(fn_parts)
n = 1
full_tmp_fn = os.path.normpath(tmp_dir + '/' + base_fn)
while os.path.exists(full_tmp_fn):
n += 1
tmp_fn = base_fn + '_' + str(n).rjust(3, '0')
full_tmp_fn = os.path.normpath(tmp_dir + '/' + tmp_fn)
with open(full_tmp_fn, 'wb') as f:
f.write(data)
ff_methods = self._file_format_methods()
if file_format.name in ff_methods:
data = ff_methods[file_format.name](data)
fn = '.'.join([base_fn, file_format.suffix])
dups = self._check_duplicate_ebics_file(fn, file_format)
if dups:
raise UserError(_(
"EBICS File with name '%s' has already been downloaded."
"\nPlease check this file and rename in case there is "
"no risk on duplicate transactions.")
% fn)
data = base64.encodebytes(data)
ef_vals = {
'name': fn,
'data': data,
'date': fields.Datetime.now(),
'date_from': self.date_from,
'date_to': self.date_to,
'format_id': file_format.id,
'user_id': self._uid,
'ebics_userid_id': self.ebics_userid_id.id,
'company_ids': self.ebics_config_id.company_ids.ids,
}
self._update_ef_vals(ef_vals)
ebics_file = self.env['ebics.file'].create(ef_vals)
return ebics_file
def _check_duplicate_ebics_file(self, fn, file_format):
dups = self.env['ebics.file'].search(
[('name', '=', fn),
('format_id', '=', file_format.id)])
return dups
def _detect_upload_format(self):
"""
Use this method in order to automatically detect and set the
EBICS upload file format.
"""
pass
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,102 @@
<?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', '!=', 'FUL')]}"/>
<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,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,17 @@
# Copyright 2020 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': '14.0.1.0.0',
'author': 'Noviat',
'category': 'Hidden',
'license': 'LGPL-3',
'depends': [
'account_ebics',
'account_statement_import',
],
'installable': True,
'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,62 @@
# 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
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,20 @@
# Copyright 2020 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': '14.0.1.0.0',
'author': 'Noviat',
'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,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 Odoo Enterprise Bank Statement Import
======================================================================
This module makes it possible to use Odoo Enterprise account_bank_statement_import
in combination with 'account_ebics'.
This module will be installed automatically when following modules are activated
on your odoo database :
- account_ebics_oe
- account_bank_statement_import

View File

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

View File

@ -0,0 +1,17 @@
# Copyright 2020 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
'name': 'account_ebics with Odoo Enterprise Bank Statement Import',
'summary': "Use Odoo Enterprise Bank Statement Import with account_ebics",
'version': '14.0.1.0.0',
'author': 'Noviat',
'category': 'Hidden',
'license': 'LGPL-3',
'depends': [
'account_ebics_oe',
'account_bank_statement_import',
],
'installable': True,
'auto_install': True,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

@ -0,0 +1,62 @@
# 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 AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
def _check_parsed_data(self, stmts_vals, account_number):
""" 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
super()._check_parsed_data(stmts_vals, account_number)
def _create_bank_statements(self, stmts_vals):
"""
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)