add ebics module

This commit is contained in:
Luc De Meyer 2018-08-14 17:04:39 +02:00
parent db508b8b4a
commit e0b619c740
26 changed files with 2270 additions and 0 deletions

69
account_ebics/README.rst Normal file
View File

@ -0,0 +1,69 @@
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-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.
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.
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 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).

View File

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

View File

@ -0,0 +1,24 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'EBICS banking protocol',
'version': '11.0.1.4.0',
'license': 'AGPL-3',
'author': 'Noviat',
'category': 'Accounting & Finance',
'depends': ['account'],
'data': [
'security/ebics_security.xml',
'security/ir.model.access.csv',
'data/ebics_file_format.xml',
'views/menuitem.xml',
'views/ebics_config.xml',
'views/ebics_file.xml',
'views/ebics_file_format.xml',
'wizard/ebics_change_passphrase.xml',
'wizard/ebics_xfer.xml',
],
'installable': True,
'application': True,
}

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--
File format tested with the following banks:
- GLS Gemeinschaftsbank (Germany)
-->
<record id="ebics_ff_camt_053_001_02_stm" model="ebics.file.format">
<field name="name">camt.053.001.02.stm</field>
<field name="type">down</field>
<field name="order_type">C53</field>
<field name="description">Bank Statement in Format camt.053</field>
<field name="suffix">c53.xml</field>
</record>
<!--
File format tested with the following banks:
- GLS Gemeinschaftsbank (Germany)
-->
<record id="ebics_ff_pain_001_001_03_sct" model="ebics.file.format">
<field name="name">pain.001.001.03.sct</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_pain_008_001_02_sdd" 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_pain_008_001_02_sbb" 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>
<!--
File format tested with the following banks:
- Credit Suisse (Switzerland)
-->
<record id="ebics_ff_pain_001" model="ebics.file.format">
<field name="name">pain.001</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>
<!--
File format tested with the following banks:
- Credit Suisse (Switzerland)
-->
<record id="ebics_ff_pain_008" model="ebics.file.format">
<field name="name">pain.008</field>
<field name="type">up</field>
<field name="order_type">XE3</field>
<field name="description">Direct Debit Order in Format pain.008.001.02</field>
<field name="suffix">xml</field>
</record>
<!--
File format tested with the following banks:
- CIC (France)
-->
<record id="ebics_ff_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="description">Bank Statement in Format cfonb120</field>
<field name="suffix">cfonb120.dat</field>
</record>
<!--
File format tested with the following banks:
- CIC (France)
-->
<record id="ebics_ff_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,5 @@
from . import fintech_ebics_register
from . import account_bank_statement
from . import ebics_config
from . import ebics_file
from . import ebics_file_format

View File

@ -0,0 +1,11 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
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,494 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
"""
import logging
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
"""
import base64
import logging
import re
import os
from sys import exc_info
from urllib.error import URLError
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
import fintech
from fintech.ebics import EbicsKeyRing, EbicsBank, EbicsUser,\
EbicsClient, EbicsFunctionalError, EbicsTechnicalError
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 EbicsConfig(models.Model):
"""
EBICS configuration is stored in a separate object in order to
allow extra security policies on this object.
Remark:
This Configuration model implements a simple model of the relationship
between users and authorizations and may need to be adapted
in next versions of this module to cope with higher complexity .
"""
_name = 'ebics.config'
_description = 'EBICS Configuration'
_order = 'name'
name = fields.Char(string='Name', required=True)
bank_id = fields.Many2one(
comodel_name='res.partner.bank',
readonly=True, states={'draft': [('readonly', False)]},
string='Bank Account', 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_user = 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.")
# 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_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', required=True,
readonly=True, states={'draft': [('readonly', False)]},
default=lambda self: self._default_ebics_keys(),
help="File holding the EBICS Keys."
"\nSpecify the full path (directory + filename).")
ebics_keys_found = fields.Boolean(
compute='_compute_ebics_keys_found')
ebics_passphrase = fields.Char(
string='EBICS Passphrase')
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_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)
# 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)]},
)
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'),
('init', 'Initialisation'),
('get_bank_keys', 'Get Keys from Bank'),
('to_verify', 'Verification'),
('active', 'Active')],
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_id = fields.Many2one(
'res.company', string='Company',
default=lambda self: self.env.user.company_id,
required=True)
@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,
'mykeys'])
@api.multi
def _compute_ebics_keys_found(self):
for cfg in self:
if cfg.ebics_keys:
dirname = os.path.dirname(self.ebics_keys)
self.ebics_keys_found = os.path.exists(dirname)
@api.multi
@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.multi
def unlink(self):
for ebics_config in self:
if ebics_config.state == 'active':
raise UserError(_(
"You cannot remove active EBICS congirations."))
return super(EbicsConfig, self).unlink()
@api.multi
def set_to_draft(self):
return self.write({'state': 'draft'})
@api.multi
def set_to_active(self):
return self.write({'state': 'active'})
@api.multi
def ebics_init_1(self):
"""
Initialization of bank keys - Step 1:
Create new keys and certificates for this user
"""
self.ensure_one()
self._check_ebics_files()
if self.state != 'draft':
raise UserError(
_("Set state to 'draft' before Bank Key (re)initialisation."))
try:
keyring = EbicsKeyRing(
keys=self.ebics_keys,
passphrase=self.ebics_passphrase or None)
bank = EbicsBank(
keyring=keyring, hostid=self.ebics_host, url=self.ebics_url)
user = EbicsUser(
keyring=keyring, partnerid=self.ebics_partner,
userid=self.ebics_user)
except:
exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value)
raise UserError(error)
self._check_ebics_keys()
if not os.path.isfile(self.ebics_keys):
user.create_keys(
keyversion=self.ebics_key_version,
bitlength=self.ebics_key_bitlength)
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=self.ebics_version)
# Send the public electronic signature key to the bank.
try:
if self.ebics_version == 'H003':
bank._order_number = self._get_order_number()
OrderID = client.INI()
_logger.info(
'%s, EBICS INI command, OrderID=%s', self._name, OrderID)
if self.ebics_version == 'H003':
self._update_order_number(OrderID)
except URLError:
exctype, value = exc_info()[:2]
raise UserError(_(
"urlopen error:\n url '%s' - %s")
% (self.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 self.ebics_version == 'H003':
bank._order_number = self._get_order_number()
OrderID = client.HIA()
_logger.info('%s, EBICS HIA command, OrderID=%s', self._name, OrderID)
if self.ebics_version == 'H003':
self._update_order_number(OrderID)
# Create an INI-letter which must be printed and sent to the bank.
lang = self.env.user.lang[:2]
cc = self.bank_id.bank_id.country.code
if cc in ['FR', 'DE']:
lang = cc
tmp_dir = os.path.normpath(self.ebics_files + '/tmp')
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today()
fn = '_'.join([self.ebics_host, 'ini_letter', fn_date]) + '.pdf'
full_tmp_fn = os.path.normpath(tmp_dir + '/' + fn)
user.create_ini_letter(
bankname=self.bank_id.bank_id.name,
path=full_tmp_fn,
lang=lang)
with open(full_tmp_fn, 'rb') as f:
letter = f.read()
self.write({
'ebics_ini_letter': base64.encodestring(letter),
'ebics_ini_letter_fn': fn,
})
return self.write({'state': 'init'})
@api.multi
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'})
@api.multi
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._check_ebics_files()
if self.state != 'get_bank_keys':
raise UserError(
_("Set state to 'Get Keys from Bank'."))
keyring = EbicsKeyRing(
keys=self.ebics_keys, passphrase=self.ebics_passphrase)
bank = EbicsBank(
keyring=keyring, hostid=self.ebics_host, url=self.ebics_url)
user = EbicsUser(
keyring=keyring, partnerid=self.ebics_partner,
userid=self.ebics_user)
client = EbicsClient(
bank, user, version=self.ebics_version)
public_bank_keys = client.HPB()
public_bank_keys = public_bank_keys.encode()
tmp_dir = os.path.normpath(self.ebics_files + '/tmp')
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today()
fn = '_'.join([self.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
@api.multi
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, passphrase=self.ebics_passphrase)
bank = EbicsBank(
keyring=keyring, hostid=self.ebics_host, url=self.ebics_url)
bank.activate_keys()
return self.write({'state': 'active'})
@api.multi
def change_passphrase(self):
self.ensure_one()
ctx = dict(self._context, default_ebics_config_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',
}
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):
if self.ebics_keys:
dirname = os.path.dirname(self.ebics_keys)
if not os.path.exists(dirname):
raise UserError(_(
"EBICS Keys 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,262 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, 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'
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)
note = fields.Text(string='Notes')
note_process = fields.Text(string='Notes')
company_id = fields.Many2one(
comodel_name='res.company',
string='Company',
default=lambda self: self._default_company_id())
_sql_constraints = [
('name_company_uniq', 'unique (name, company_id, format_id)',
'This File has already been imported !')
]
@api.model
def _default_company_id(self):
"""
Adapt this method in case your bank provides transactions
of multiple legal entities in a single EBICS File.
"""
return self.env.user.company_id
@api.multi
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.name
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()
@api.multi
def set_to_draft(self):
return self.write({'state': 'draft'})
@api.multi
def set_to_done(self):
return self.write({'state': 'done'})
@api.multi
def process(self):
self.ensure_one()
self.note_process = ''
ff_methods = self._file_format_methods()
ff = self.format_id.name
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()
@api.multi
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
@api.multi
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 = {
'camt.xxx.cfonb120.stm':
{'process': self._process_cfonb120,
'unlink': self._unlink_cfonb120},
'camt.053.001.02.stm':
{'process': self._process_camt053,
'unlink': self._unlink_camt053},
}
return res
def _check_import_module(self, module):
mod = self.env['ir.module.module'].search(
[('name', '=', module),
('state', '=', 'installed')])
if not mod:
raise UserError(_(
"The module to process the '%s' format is not installed "
"on your system. "
"\nPlease install module '%s'")
% (self.format_id.name, module))
def _process_result_action(self, ctx):
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):
"""
TODO:
adapt OCA import logic to find correct journal on the basis
of both account number and currency.
Prompt for journal in case the journal is not found.
"""
import_module = 'account_bank_statement_import_fr_cfonb'
self._check_import_module(import_module)
wiz_model = 'account.bank.statement.import'
wiz_vals = {'data_file': self.data}
wiz = self.env[wiz_model].create(wiz_vals)
res = wiz.import_file()
notifications = []
statement_ids = []
if res.get('context'):
notifications = res['context'].get('notifications', [])
statement_ids = res['context'].get('statement_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'
self.note_process += _(
"Number of Bank Statements: %s"
) % len(statement_ids)
if statement_ids:
self.bank_statement_ids = [(6, 0, statement_ids)]
ctx = dict(self._context, statement_ids=statement_ids)
return self._process_result_action(ctx)
@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_camt053(self):
import_module = 'account_bank_statement_import_camt'
self._check_import_module(import_module)
wiz_model = 'account.bank.statement.import'
wiz_vals = {
'data_file': self.data,
'filename': self.name,
}
wiz = self.env[wiz_model].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)
notifications = []
statement_ids = []
if res.get('context'):
notifications = res['context'].get('notifications', [])
statement_ids = res['context'].get('statement_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'
self.note_process += _(
"Number of Bank Statements: %s"
) % len(statement_ids)
if statement_ids:
self.bank_statement_ids = [(6, 0, statement_ids)]
ctx = dict(self._context, statement_ids=statement_ids)
return self._process_result_action(ctx)
@staticmethod
def _unlink_camt053(self):
"""
Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
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,79 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class EbicsFileFormat(models.Model):
_name = 'ebics.file.format'
_description = 'EBICS File Formats'
_order = 'type,name'
name = fields.Selection(
selection=lambda self: self._selection_name(),
string='Request Type', required=True)
type = fields.Selection(
selection=[('down', 'Download'),
('up', 'Upload')],
required=True)
order_type = fields.Selection(
selection=lambda self: self._selection_order_type(),
string='Order Type',
help="For most banks is France you should use the "
"format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.")
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 bank connection.")
description = fields.Char()
suffix = fields.Char(
required=True,
help="Specify the filename suffix for this File Format."
"\nE.g. camt.053.xml")
@api.model
def _selection_order_type(self):
up = self._supported_upload_order_types()
down = self._supported_download_order_types()
selection = [(x, x) for x in up + down]
return selection
def _supported_upload_order_types(self):
return ['FUL', 'CCT', 'CDD', 'CDB', 'XE2', 'XE3']
def _supported_download_order_types(self):
return ['FDL', 'C53']
@api.model
def _selection_name(self):
"""
List of supported EBICS Request Types.
Extend this method via a custom module when testing
a new Request Type and make a PR for the
account_ebics module when this new Request Type
is working correctly.
This PR should include at least updates to
- 'data/ebics_file_format.xml'
- 'models/ebics_file_format.py'
An overview of the EBICS Request Types can be found in
the doc folder of this module (EBICS_Annex2).
"""
request_types = [
'camt.053.001.02.stm',
'pain.001.001.03.sct',
'pain.008.001.02.sdd',
'pain.008.001.02.sbb',
'camt.xxx.cfonb120.stm',
'pain.001.001.02.sct',
'camt.053',
'pain.001',
'pain.008',
]
selection = [(x, x) for x in request_types]
return selection

View File

@ -0,0 +1,40 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
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.cryptolib = 'cryptography'
fintech.register(
fintech_register_name,
fintech_register_keycode,
fintech_register_users)
except RuntimeError as e:
if e.message == "'register' can be called only once":
pass
else:
_logger.error(e.message)
fintech.register()
except:
msg = "fintech.register error"
tb = ''.join(format_exception(*exc_info()))
msg += '\n%s' % tb
_logger.error(msg)
fintech.register()

View File

@ -0,0 +1,27 @@
<?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_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</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_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,7 @@
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,account_ebics.group_ebics_manager,1,1,1,1
access_ebics_config_user,ebics_config user,model_ebics_config,account.group_account_user,1,0,0,0
access_ebics_file_format_manager,ebics_file_format manager,model_ebics_file_format,account_ebics.group_ebics_manager,1,1,1,1
access_ebics_file_format_user,ebics_file_format user,model_ebics_file_format,account.group_account_user,1,0,0,0
access_ebics_file_manager,ebics_file manager,model_ebics_file,account_ebics.group_ebics_manager,1,1,1,1
access_ebics_file_user,ebics_file user,model_ebics_file,account.group_account_user,1,0,0,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 account_ebics.group_ebics_manager 1 1 1 1
3 access_ebics_config_user ebics_config user model_ebics_config account.group_account_user 1 0 0 0
4 access_ebics_file_format_manager ebics_file_format manager model_ebics_file_format account_ebics.group_ebics_manager 1 1 1 1
5 access_ebics_file_format_user ebics_file_format user model_ebics_file_format account.group_account_user 1 0 0 0
6 access_ebics_file_manager ebics_file manager model_ebics_file account_ebics.group_ebics_manager 1 1 1 1
7 access_ebics_file_user ebics_file user model_ebics_file account.group_account_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,122 @@
<?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" colors="blue:state!='active'">
<field name="name"/>
<field name="bank_id"/>
<field name="ebics_host"/>
<field name="ebics_user"/>
<field name="state"/>
<field name="company_id" groups="base.group_multi_company"/>
</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="active" string="Set to Draft" type="object"
groups="account.group_account_manager"
help="Set to Draft in order to reinitialize your bank connection."/>
<button name="set_to_active" states="draft" string="Force Active" type="object"
groups="account.group_account_manager"
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 col="4">
<field name="name" colspan="2"/>
<field name="bank_id" domain="[('company_id', '=', company_id)]"/>
<field name="ebics_host"/>
<field name="ebics_url"/>
<field name="ebics_partner"/>
<field name="ebics_user"/>
<field name="ebics_version"/>
<field name="signature_class"/>
<field name="ebics_files"/>
<field name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"/>
<field name="active"/>
<field name="company_id" widget='selection' groups="base.group_multi_company"/>
</group>
<notebook>
<page string="Keys" groups="account_ebics.group_ebics_manager">
<label string=""/>
<header>
<button name="ebics_init_1" states="draft" string="EBICS Initialisation" type="object" class="oe_highlight"
groups="account.group_account_manager"
help="Initialise EBICS Bank Keys"/>
<button name="ebics_init_2" states="init" string="Account activated" type="object" class="oe_highlight"
groups="account.group_account_manager"
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"
groups="account.group_account_manager"
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"
groups="account.group_account_manager"
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)]}"
groups="account.group_account_manager"/>
</header>
<group col="4" name="ebics_key">
<field name="ebics_keys"/>
<field name="ebics_keys_found" invisible="1"/>
<field name="ebics_passphrase" password="True" attrs="{'invisible': [('ebics_keys_found', '!=', False)]}"/>
<newline/>
<field name="ebics_key_version"/>
<field name="ebics_key_bitlength"/>
<field name="ebics_key_x509"/>
</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>
<group col="4" name="dn" attrs="{'invisible': [('ebics_key_x509', '=', False)]}">
<label string="Distinguished Name attributes used to create self-signed X.509 certificates:" colspan="4"/>
<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>
</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_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="ebics_config_menu"
name="EBICS Configuration"
parent="ebics_menu"
action="ebics_config_action"
groups="account_ebics.group_ebics_manager"
sequence="10"/>
</odoo>

View File

@ -0,0 +1,216 @@
<?xml version="1.0" ?>
<odoo>
<menuitem id="ebics_file_menu"
name="EBICS Files"
parent="ebics_processing_menu"
sequence="30"/>
<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_id" widget="selection" groups="base.group_multi_company"/>
</group>
<newline/>
<group expand="0" string="Group By">
<filter string="File Format" context="{'group_by':'format_id'}"/>
<filter string="State" context="{'group_by':'state'}"/>
<filter string="User" context="{'group_by':'user_id'}"/>
<filter string="Company" domain="[]" groups="base.group_multi_company" context="{'group_by':'company_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" colors="blue: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_id" 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_manager"
help="Process the EBICS File"/>
<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="company_id" 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_type">form</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>
<menuitem id="ebics_file_menu_download"
name="Download"
parent="ebics_file_menu"
action="ebics_file_action_download"
sequence="31"/>
<!-- 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" colors="blue: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_id" 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="company_id" 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_type">form</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>
<menuitem id="ebics_file_menu_upload"
name="Upload"
parent="ebics_file_menu"
action="ebics_file_action_upload"
sequence="31"/>
</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 col="4">
<field name="type"/>
<field name="order_type"/>
<field name="name"/>
<field name="signature_class"/>
<field name="suffix"/>
<newline/>
<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_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<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,15 @@
<?xml version="1.0" ?>
<odoo>
<menuitem id="ebics_processing_menu"
name="EBICS Processing"
parent="account.menu_finance"
sequence="4"/>
<menuitem id="ebics_menu"
name="EBICS"
parent='account.menu_finance_configuration'
groups="account_ebics.group_ebics_manager"
sequence="100"/>
</odoo>

View File

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

View File

@ -0,0 +1,77 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, 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_config_id = fields.Many2one(
comodel_name='ebics.config',
string='EBICS Configuration',
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)
@api.multi
def change_passphrase(self):
self.ensure_one()
if self.old_pass != self.ebics_config_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_config_id.ebics_passphrase:
raise UserError(_(
"New passphrase equal to old passphrase."))
try:
keyring = EbicsKeyRing(
keys=self.ebics_config_id.ebics_keys,
passphrase=self.ebics_config_id.ebics_passphrase)
keyring.change_passphrase(self.new_pass)
except ValueError as e:
raise UserError(str(e))
self.ebics_config_id.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',
}
@api.multi
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,541 @@
# Copyright 2009-2018 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
"""
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', '=', 'active')],
required=True,
default=lambda self: self._default_ebics_config_id())
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.")
order_type = fields.Selection(
selection=lambda self: self._selection_order_type(),
string='Order Type',
help="For most banks is 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_id', '=', self.env.user.company_id.id),
('state', '=', 'active')])
if cfg and len(cfg) == 1:
return cfg
else:
return cfg_mod
@api.model
def _selection_order_type(self):
return self.env['ebics.file.format']._selection_order_type()
@api.onchange('ebics_config_id')
def _onchange_ebics_config_id(self):
domain = {}
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
domain['format_id'] = [('type', '=', 'down'),
('id', 'in', download_formats.ids)]
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
domain['format_id'] = [('type', '=', 'up'),
('id', 'in', upload_formats.ids)]
return {'domain': domain}
@api.onchange('upload_data')
def _onchange_upload_data(self):
self.upload_fname_dummy = self.upload_fname
self._detect_upload_format()
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:
self.format_id = upload_formats
@api.onchange('format_id')
def _onchange_format_id(self):
self.order_type = self.format_id.order_type
@api.multi
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',
}
@api.multi
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']
for df in download_formats:
success = False
order_type = df.order_type or 'FDL'
params = {}
if order_type == 'FDL':
params['filetype'] = df.name
if order_type in ['FDL', 'C53']:
params.update({
'start': self.date_from or None,
'end': self.date_to or None,
})
kwargs = {k: v for k, v in params.items() if v}
try:
method = getattr(client, order_type)
data = method(**kwargs)
ebics_files += self._handle_download_data(data, df)
success = True
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 UserError as e:
self.note += '\n'
self.note += _("Warning:")
self.note += '\n'
self.note += e.message
except:
self.note += '\n'
self.note += _("Unknown Error")
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',
}
@api.multi
def button_close(self):
self.ensure_one()
return {'type': 'ir.actions.act_window_close'}
@api.multi
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(
module, 'ebics_file_action_download')
act['domain'] = [('id', 'in', self._context['ebics_file_ids'])]
return act
def _ebics_upload(self):
self.ensure_one()
ebics_file = self.env['ebics.file']
self.note = ''
client = self._setup_client()
if client:
upload_data = base64.decodestring(self.upload_data)
ef_format = self.format_id
OrderID = False
try:
order_type = ef_format.order_type or 'FUL'
method = hasattr(client, order_type) \
and getattr(client, order_type)
if order_type == 'FUL':
kwargs = {}
# bank = self.ebics_config_id.bank_id.bank v8.0
bank = self.ebics_config_id.bank_id.bank_id
if bank.country:
kwargs['country'] = bank.country.code
if self.test_mode:
kwargs['TEST'] = 'TRUE'
OrderID = method(ef_format.name, upload_data, **kwargs)
elif order_type in ['CCT', 'CDD', 'CDB']:
OrderID = method(upload_data)
elif order_type in ['XE2', 'XE3']:
OrderID = client.upload(order_type, upload_data)
else:
# TODO: investigate if it makes sense to support
# a generic upload for a non-predefined order_type
pass
if OrderID:
self.note += '\n'
self.note += _(
"EBICS File has been uploaded (OrderID %s)."
) % OrderID
ef_note = _("EBICS OrderID: %s") % OrderID
if self._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,
'note': ef_note,
}
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:
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
@api.multi
def _setup_client(self):
self.ebics_config_id._check_ebics_keys()
passphrase = self._get_passphrase()
keyring = EbicsKeyRing(
keys=self.ebics_config_id.ebics_keys,
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_config_id.ebics_user)
signature_class = self.format_id.signature_class \
or self.ebics_config_id.signature_class
if signature_class == 'T':
user.manual_approval = True
try:
client = EbicsClient(
bank, user, version=self.ebics_config_id.ebics_version)
except:
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_config_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.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]
if docname:
fn_parts.append(docname)
else:
fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date)
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.encodestring(data)
ef_vals = {
'name': fn,
'data': data,
'date': fields.Datetime.now(),
'date_from': self.date_from,
'date_to': self.date_from,
'format_id': file_format.id,
'user_id': self._uid,
}
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),
'|',
('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False)])
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 = ''.join(o_list)
if next == 'ZZZZ':
next = 'A000'
self.ebics_config_id.order_number = next
def _insert_line_terminator(self, data_in, line_len):
data_out = b''
max = len(data_in)
i = 0
while i + line_len <= max:
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_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,94 @@
<?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" options="{'no_create': True, 'no_open': True}"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="format_id"/>
</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" 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"/>
<field name="order_type"/>
<field name="test_mode" attrs="{'invisible': [('order_type', '!=', 'FUL')]}"/>
</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_type">form</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_type">form</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>
<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"/>
</odoo>