From 663f58874359b1182a38a1f244690fb876cfc4eb Mon Sep 17 00:00:00 2001 From: Luc De Meyer Date: Sun, 5 Jul 2020 08:24:57 +0200 Subject: [PATCH] 13 multi bank account support (#15) * [13.0][IMP]add support for multiple bank accounts and multiple EBICS UserIDs over a single EBICS connection * ebics refactoring fixes * ebics refactoring fixes --- account_ebics/README.rst | 6 + account_ebics/__init__.py | 2 +- account_ebics/__manifest__.py | 15 +- .../migrations/13.0.1.1/noupdate_changes.xml | 18 + .../migrations/13.0.1.1/post-migration.py | 73 ++++ .../migrations/13.0.1.1/pre-migration.py | 9 + account_ebics/models/__init__.py | 1 + .../models/account_bank_statement.py | 2 +- account_ebics/models/ebics_config.py | 364 ++---------------- account_ebics/models/ebics_file.py | 62 +-- account_ebics/models/ebics_userid.py | 358 +++++++++++++++++ account_ebics/security/ebics_security.xml | 11 +- account_ebics/security/ir.model.access.csv | 8 +- account_ebics/views/ebics_config.xml | 127 ------ account_ebics/views/ebics_config_views.xml | 74 ++++ ...format.xml => ebics_file_format_views.xml} | 7 - .../{ebics_file.xml => ebics_file_views.xml} | 30 +- account_ebics/views/ebics_userid_views.xml | 82 ++++ account_ebics/views/menu.xml | 58 +++ account_ebics/views/menuitem.xml | 15 - account_ebics/{wizard => wizards}/__init__.py | 0 .../account_bank_statement_import.py | 8 +- .../ebics_change_passphrase.py | 0 .../ebics_change_passphrase.xml | 0 .../{wizard => wizards}/ebics_xfer.py | 34 +- .../{wizard => wizards}/ebics_xfer.xml | 5 +- 26 files changed, 812 insertions(+), 557 deletions(-) create mode 100644 account_ebics/migrations/13.0.1.1/noupdate_changes.xml create mode 100644 account_ebics/migrations/13.0.1.1/post-migration.py create mode 100644 account_ebics/migrations/13.0.1.1/pre-migration.py create mode 100644 account_ebics/models/ebics_userid.py delete mode 100644 account_ebics/views/ebics_config.xml create mode 100644 account_ebics/views/ebics_config_views.xml rename account_ebics/views/{ebics_file_format.xml => ebics_file_format_views.xml} (84%) rename account_ebics/views/{ebics_file.xml => ebics_file_views.xml} (88%) create mode 100644 account_ebics/views/ebics_userid_views.xml create mode 100644 account_ebics/views/menu.xml delete mode 100644 account_ebics/views/menuitem.xml rename account_ebics/{wizard => wizards}/__init__.py (100%) rename account_ebics/{wizard => wizards}/account_bank_statement_import.py (90%) rename account_ebics/{wizard => wizards}/ebics_change_passphrase.py (100%) rename account_ebics/{wizard => wizards}/ebics_change_passphrase.xml (100%) rename account_ebics/{wizard => wizards}/ebics_xfer.py (94%) rename account_ebics/{wizard => wizards}/ebics_xfer.xml (92%) diff --git a/account_ebics/README.rst b/account_ebics/README.rst index fbc746a..cbb0493 100644 --- a/account_ebics/README.rst +++ b/account_ebics/README.rst @@ -67,3 +67,9 @@ 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 3SKEY signed transactions +- add support for EBICS 3.0 diff --git a/account_ebics/__init__.py b/account_ebics/__init__.py index 9b42961..aee8895 100644 --- a/account_ebics/__init__.py +++ b/account_ebics/__init__.py @@ -1,2 +1,2 @@ from . import models -from . import wizard +from . import wizards diff --git a/account_ebics/__manifest__.py b/account_ebics/__manifest__.py index 4db5efa..5a69a27 100644 --- a/account_ebics/__manifest__.py +++ b/account_ebics/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'EBICS banking protocol', - 'version': '13.0.1.0.3', + 'version': '13.0.1.1.0', 'license': 'LGPL-3', 'author': 'Noviat', 'category': 'Accounting & Finance', @@ -12,12 +12,13 @@ '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', + '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, diff --git a/account_ebics/migrations/13.0.1.1/noupdate_changes.xml b/account_ebics/migrations/13.0.1.1/noupdate_changes.xml new file mode 100644 index 0000000..ae3cbe1 --- /dev/null +++ b/account_ebics/migrations/13.0.1.1/noupdate_changes.xml @@ -0,0 +1,18 @@ + + + + + EBICS Configuration model company rule + + + ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)] + + + + EBICS File model company rule + + + ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)] + + + diff --git a/account_ebics/migrations/13.0.1.1/post-migration.py b/account_ebics/migrations/13.0.1.1/post-migration.py new file mode 100644 index 0000000..9e9937e --- /dev/null +++ b/account_ebics/migrations/13.0.1.1/post-migration.py @@ -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'] + 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' + ) diff --git a/account_ebics/migrations/13.0.1.1/pre-migration.py b/account_ebics/migrations/13.0.1.1/pre-migration.py new file mode 100644 index 0000000..53b714f --- /dev/null +++ b/account_ebics/migrations/13.0.1.1/pre-migration.py @@ -0,0 +1,9 @@ +# Copyright 2009-2020 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(cr, version): + if not version: + return + + cr.execute("DELETE FROM ebics_xfer;") diff --git a/account_ebics/models/__init__.py b/account_ebics/models/__init__.py index 01f4bfe..0a211c1 100644 --- a/account_ebics/models/__init__.py +++ b/account_ebics/models/__init__.py @@ -3,3 +3,4 @@ from . import account_bank_statement from . import ebics_config from . import ebics_file from . import ebics_file_format +from . import ebics_userid diff --git a/account_ebics/models/account_bank_statement.py b/account_ebics/models/account_bank_statement.py index 898a36a..dde134d 100644 --- a/account_ebics/models/account_bank_statement.py +++ b/account_ebics/models/account_bank_statement.py @@ -1,4 +1,4 @@ -# Copyright 2009-2018 Noviat. +# Copyright 2009-2020 Noviat. # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). from odoo import fields, models diff --git a/account_ebics/models/ebics_config.py b/account_ebics/models/ebics_config.py index 16b53cf..051b9df 100644 --- a/account_ebics/models/ebics_config.py +++ b/account_ebics/models/ebics_config.py @@ -1,22 +1,18 @@ # Copyright 2009-2020 Noviat. # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). -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 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 + from fintech.ebics import EbicsBank fintech.cryptolib = 'cryptography' except ImportError: EbicsBank = object @@ -38,27 +34,20 @@ 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) - company_partner_id = fields.Many2one( - comodel_name='res.partner', - related='company_id.partner_id', - string='Account Holder', - readonly=True, store=False) - bank_id = fields.Many2one( - comodel_name='res.partner.bank', + name = fields.Char( + string='Name', readonly=True, states={'draft': [('readonly', False)]}, - string='Bank Account', - domain="[('partner_id','=', company_partner_id)]", + 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, @@ -88,8 +77,9 @@ class EbicsConfig(models.Model): "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, + 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. " @@ -98,35 +88,18 @@ class EbicsConfig(models.Model): "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, + string='EBICS Keys Root', 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') + help="Root Directory for storing the EBICS Keys.") ebics_key_version = fields.Selection( selection=[('A005', 'A005 (RSASSA-PKCS1-v1_5)'), ('A006', 'A006 (RSASSA-PSS)')], @@ -140,51 +113,6 @@ class EbicsConfig(models.Model): 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', @@ -193,10 +121,7 @@ class EbicsConfig(models.Model): ) state = fields.Selection( [('draft', 'Draft'), - ('init', 'Initialisation'), - ('get_bank_keys', 'Get Keys from Bank'), - ('to_verify', 'Verification'), - ('active', 'Active')], + ('confirm', 'Confirmed')], string='State', default='draft', required=True, readonly=True) @@ -207,10 +132,11 @@ class EbicsConfig(models.Model): "[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) + 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): @@ -218,22 +144,7 @@ class EbicsConfig(models.Model): @api.model def _default_ebics_keys(self): - return '/'.join(['/etc/odoo/ebics_keys', - self._cr.dbname, - 'mykeys']) - - @api.depends('ebics_keys') - def _compute_ebics_keys_found(self): - for cfg in self: - cfg.ebics_keys_found = ( - cfg.ebics_keys and os.path.isfile(cfg.ebics_keys)) - - @api.constrains('ebics_passphrase') - def _check_ebics_passphrase(self): - for cfg in self: - if not cfg.ebics_passphrase or len(cfg.ebics_passphrase) < 8: - raise UserError(_( - "The passphrase must be at least 8 characters long")) + return '/'.join(['/etc/odoo/ebics_keys', self._cr.dbname]) @api.constrains('order_number') def _check_order_number(self): @@ -252,223 +163,22 @@ class EbicsConfig(models.Model): "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 congirations.")) + "You cannot remove active EBICS configurations.")) return super(EbicsConfig, self).unlink() def set_to_draft(self): return self.write({'state': 'draft'}) - def set_to_get_bank_keys(self): - return self.write({'state': 'get_bank_keys'}) - - def set_to_active(self): - return self.write({'state': 'active'}) - - 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.")) - - if not self.ebics_passphrase: - raise UserError( - _("Set a passphrase.")) - - try: - 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) - except Exception: - 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): - try: - user.create_keys( - keyversion=self.ebics_key_version, - bitlength=self.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.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. - cc = self.bank_id.bank_id.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_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_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'}) - - 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._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().isoformat() - 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 - - 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'}) - - 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 set_to_confirm(self): + return self.write({'state': 'confirm'}) def _get_order_number(self): return self.order_number @@ -490,18 +200,12 @@ class EbicsConfig(models.Model): 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) - if os.path.isdir(self.ebics_keys): + dirname = self.ebics_keys or '' + if not os.path.exists(dirname): raise UserError(_( - "Configuration Error.\n" - "The 'EBICS Keys' parameter should be a full path " - "(directory + filename) not a directory name.")) + "EBICS Keys Root Directory %s is not available." + "\nPlease contact your system administrator.") + % dirname) def _check_ebics_files(self): dirname = self.ebics_files or '' diff --git a/account_ebics/models/ebics_file.py b/account_ebics/models/ebics_file.py index 622c709..1a8e810 100644 --- a/account_ebics/models/ebics_file.py +++ b/account_ebics/models/ebics_file.py @@ -1,9 +1,10 @@ # Copyright 2009-2020 Noviat. # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). +import base64 import logging -from odoo import api, fields, models, _ +from odoo import _, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -13,6 +14,10 @@ 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) @@ -46,25 +51,17 @@ class EbicsFile(models.Model): 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_id = fields.Many2one( + company_ids = fields.Many2many( 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 + string='Companies', + help="Companies sharing this EBICS file.") def unlink(self): ff_methods = self._file_format_methods() @@ -168,11 +165,25 @@ class EbicsFile(models.Model): import_module = 'account_bank_statement_import_fr_cfonb' self._check_import_module(import_module) wiz_model = 'account.bank.statement.import' - wiz_vals = { - 'attachment_ids': [(0, 0, {'name': self.name, - 'datas': self.data, - 'store_fname': self.name})]} - wiz = self.env[wiz_model].create(wiz_vals) + data_file = base64.b64decode(self.data) + lines = data_file.split(b'\n') + att_vals = [] + st_lines = b'' + for line in lines: + rec_type = line[0:2] + acc_number = line[21:32] + st_lines += line + b'\n' + if rec_type == b'07': + fn = '_'.join([acc_number.decode(), self.name]) + att_vals.append({ + 'name': fn, + 'store_fname': fn, + 'datas': base64.b64encode(st_lines) + }) + st_lines = b'' + wiz_vals = {'attachment_ids': [(0, 0, x) for x in att_vals]} + 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() notifications = [] statement_ids = [] @@ -224,10 +235,11 @@ class EbicsFile(models.Model): 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, - } + '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() diff --git a/account_ebics/models/ebics_userid.py b/account_ebics/models/ebics_userid.py new file mode 100644 index 0000000..c5773fd --- /dev/null +++ b/account_ebics/models/ebics_userid.py @@ -0,0 +1,358 @@ +# 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 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: + _logger.warning('Failed to import fintech') + + +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) + # 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 os.path.isfile( + 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")) + + def set_to_draft(self): + return self.write({'state': 'draft'}) + + 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._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.")) + + try: + 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.name) + except Exception: + 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): + try: + user.create_keys( + keyversion=self.ebics_key_version, + bitlength=self.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.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. + cc = self.bank_id.bank_id.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_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_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'}) + + 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._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.name) + 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().isoformat() + 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 + + 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_keys'}) + + 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', + } diff --git a/account_ebics/security/ebics_security.xml b/account_ebics/security/ebics_security.xml index e518901..99fb018 100644 --- a/account_ebics/security/ebics_security.xml +++ b/account_ebics/security/ebics_security.xml @@ -12,14 +12,21 @@ EBICS Configuration model company rule - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)] + + + + EBICS UserID model company rule + + + ['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)] EBICS File model company rule - ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + ['|', ('company_ids', '=', False), ('company_isd', 'in', user.company_ids.ids)] diff --git a/account_ebics/security/ir.model.access.csv b/account_ebics/security/ir.model.access.csv index d398706..3d71939 100644 --- a/account_ebics/security/ir.model.access.csv +++ b/account_ebics/security/ir.model.access.csv @@ -1,7 +1,9 @@ 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_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_file_format_manager,ebics_file_format manager,model_ebics_file_format,account_ebics.group_ebics_manager,1,1,1,1 +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,account_ebics.group_ebics_manager,1,1,1,1 +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 diff --git a/account_ebics/views/ebics_config.xml b/account_ebics/views/ebics_config.xml deleted file mode 100644 index a2e4c24..0000000 --- a/account_ebics/views/ebics_config.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - ebics.config.tree - ebics.config - - - - - - - - - - - - - - ebics.config.form - ebics.config - -
-
-
- - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - Distinguished Name attributes used to create self-signed X.509 certificates: - - - - - - - - - - - - - -
- - - -
-
-
-
- - - EBICS Configuration - ebics.config - tree,form - - - - -
diff --git a/account_ebics/views/ebics_config_views.xml b/account_ebics/views/ebics_config_views.xml new file mode 100644 index 0000000..f76c619 --- /dev/null +++ b/account_ebics/views/ebics_config_views.xml @@ -0,0 +1,74 @@ + + + + + ebics.config.tree + ebics.config + + + + + + + + + + + + ebics.config.form + ebics.config + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + EBICS Configuration + ebics.config + tree,form + {'active_test': False} + + +
diff --git a/account_ebics/views/ebics_file_format.xml b/account_ebics/views/ebics_file_format_views.xml similarity index 84% rename from account_ebics/views/ebics_file_format.xml rename to account_ebics/views/ebics_file_format_views.xml index fa31d6f..7e901ac 100644 --- a/account_ebics/views/ebics_file_format.xml +++ b/account_ebics/views/ebics_file_format_views.xml @@ -39,11 +39,4 @@ tree,form - - diff --git a/account_ebics/views/ebics_file.xml b/account_ebics/views/ebics_file_views.xml similarity index 88% rename from account_ebics/views/ebics_file.xml rename to account_ebics/views/ebics_file_views.xml index badf32c..36f4d7d 100644 --- a/account_ebics/views/ebics_file.xml +++ b/account_ebics/views/ebics_file_views.xml @@ -1,11 +1,6 @@ - - ebics.file.search ebics.file @@ -17,14 +12,13 @@ - + - @@ -44,7 +38,7 @@ - + @@ -75,7 +69,8 @@ - + + @@ -131,12 +126,6 @@ - - @@ -149,7 +138,7 @@ - + @@ -171,7 +160,8 @@ - + + @@ -206,10 +196,4 @@ - - diff --git a/account_ebics/views/ebics_userid_views.xml b/account_ebics/views/ebics_userid_views.xml new file mode 100644 index 0000000..268133e --- /dev/null +++ b/account_ebics/views/ebics_userid_views.xml @@ -0,0 +1,82 @@ + + + + + ebics.userid.tree + ebics.userid + + + + + + + + + + + + ebics.userid.form + ebics.userid + +
+
+
+ + + + + + + + + + + + + + + + + Distinguished Name attributes used to create self-signed X.509 certificates: + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
diff --git a/account_ebics/views/menu.xml b/account_ebics/views/menu.xml new file mode 100644 index 0000000..a2a2c6a --- /dev/null +++ b/account_ebics/views/menu.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/account_ebics/views/menuitem.xml b/account_ebics/views/menuitem.xml deleted file mode 100644 index 7bfca0e..0000000 --- a/account_ebics/views/menuitem.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/account_ebics/wizard/__init__.py b/account_ebics/wizards/__init__.py similarity index 100% rename from account_ebics/wizard/__init__.py rename to account_ebics/wizards/__init__.py diff --git a/account_ebics/wizard/account_bank_statement_import.py b/account_ebics/wizards/account_bank_statement_import.py similarity index 90% rename from account_ebics/wizard/account_bank_statement_import.py rename to account_ebics/wizards/account_bank_statement_import.py index 507e678..3e1d331 100644 --- a/account_ebics/wizard/account_bank_statement_import.py +++ b/account_ebics/wizards/account_bank_statement_import.py @@ -1,4 +1,4 @@ -# Copyright 2009-2019 Noviat. +# Copyright 2009-2020 Noviat. # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). import logging @@ -11,12 +11,12 @@ _logger = logging.getLogger(__name__) class AccountBankStatementImport(models.TransientModel): _inherit = 'account.bank.statement.import' - def _check_parsed_data(self, stmts_vals): + 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.') + message = _("This file doesn't contain any statement.") if not message: no_st_line = True for vals in stmts_vals: @@ -31,7 +31,7 @@ class AccountBankStatementImport(models.TransientModel): ) + ':\n' + message _logger.warn(log_msg) return - super()._check_parsed_data(stmts_vals) + super()._check_parsed_data(stmts_vals, account_number) def _create_bank_statements(self, stmts_vals): """ diff --git a/account_ebics/wizard/ebics_change_passphrase.py b/account_ebics/wizards/ebics_change_passphrase.py similarity index 100% rename from account_ebics/wizard/ebics_change_passphrase.py rename to account_ebics/wizards/ebics_change_passphrase.py diff --git a/account_ebics/wizard/ebics_change_passphrase.xml b/account_ebics/wizards/ebics_change_passphrase.xml similarity index 100% rename from account_ebics/wizard/ebics_change_passphrase.xml rename to account_ebics/wizards/ebics_change_passphrase.xml diff --git a/account_ebics/wizard/ebics_xfer.py b/account_ebics/wizards/ebics_xfer.py similarity index 94% rename from account_ebics/wizard/ebics_xfer.py rename to account_ebics/wizards/ebics_xfer.py index 991bad2..902c978 100644 --- a/account_ebics/wizard/ebics_xfer.py +++ b/account_ebics/wizards/ebics_xfer.py @@ -47,8 +47,12 @@ class EbicsXfer(models.TransientModel): ebics_config_id = fields.Many2one( comodel_name='ebics.config', string='EBICS Configuration', - domain=[('state', '=', 'active')], + domain=[('state', '=', 'confirm')], default=lambda self: self._default_ebics_config_id()) + ebics_userid_id = fields.Many2one( + comodel_name='ebics.userid', + string='EBICS UserID', + required=True) ebics_passphrase = fields.Char( string='EBICS Passphrase') date_from = fields.Date() @@ -81,8 +85,8 @@ class EbicsXfer(models.TransientModel): 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')]) + [('company_ids', 'in', self.env.user.company_ids.ids), + ('state', '=', 'confirm')]) if cfg and len(cfg) == 1: return cfg else: @@ -94,7 +98,8 @@ class EbicsXfer(models.TransientModel): @api.onchange('ebics_config_id') def _onchange_ebics_config_id(self): - domain = {} + ebics_userids = self.ebics_config_id.ebics_userid_ids + domain = {'ebics_userid_id': [('id', 'in', ebics_userids.ids)]} if self._context.get('ebics_download'): download_formats = self.ebics_config_id.ebics_file_format_ids\ .filtered(lambda r: r.type == 'down') @@ -102,6 +107,13 @@ class EbicsXfer(models.TransientModel): self.format_id = download_formats domain['format_id'] = [('type', '=', 'down'), ('id', 'in', download_formats.ids)] + 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') @@ -109,6 +121,8 @@ class EbicsXfer(models.TransientModel): self.format_id = upload_formats domain['format_id'] = [('type', '=', 'up'), ('id', 'in', upload_formats.ids)] + if len(ebics_userids) == 1: + self.ebics_userid_id = ebics_userids return {'domain': domain} @api.onchange('upload_data') @@ -298,7 +312,9 @@ class EbicsXfer(models.TransientModel): '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.user.company_id.id], } self._update_ef_vals(ef_vals) ebics_file = self.env['ebics.file'].create(ef_vals) @@ -441,7 +457,8 @@ class EbicsXfer(models.TransientModel): 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] + fn_parts = [self.ebics_config_id.ebics_host, + self.ebics_config_id.ebics_partner] if docname: fn_parts.append(docname) else: @@ -479,6 +496,8 @@ class EbicsXfer(models.TransientModel): 'date_to': self.date_from, '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) @@ -487,10 +506,7 @@ class EbicsXfer(models.TransientModel): 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)]) + ('format_id', '=', file_format.id)]) return dups def _detect_upload_format(self): diff --git a/account_ebics/wizard/ebics_xfer.xml b/account_ebics/wizards/ebics_xfer.xml similarity index 92% rename from account_ebics/wizard/ebics_xfer.xml rename to account_ebics/wizards/ebics_xfer.xml index 337ca01..594e55e 100644 --- a/account_ebics/wizard/ebics_xfer.xml +++ b/account_ebics/wizards/ebics_xfer.xml @@ -10,6 +10,7 @@ + @@ -32,6 +33,7 @@ + @@ -86,7 +88,4 @@ - - -