[RFR] Refactor out custom IBAN wizardry

This commit is contained in:
Stefan Rijnhart
2014-01-21 18:03:17 +01:00
parent 5f8eed7f11
commit 7c19d3fdb0
18 changed files with 530 additions and 468 deletions

View File

@@ -33,7 +33,6 @@
'category': 'Banking addons', 'category': 'Banking addons',
'depends': [ 'depends': [
'account_voucher', 'account_voucher',
'account_iban_preserve_domestic',
], ],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
@@ -47,14 +46,9 @@
'js': [ 'js': [
'static/src/js/account_banking.js', 'static/src/js/account_banking.js',
], ],
'external_dependencies': {
'python' : ['BeautifulSoup'],
},
'description': ''' 'description': '''
Module to do banking. Module to do banking.
Note: This module is depending on BeautifulSoup.
This modules tries to combine all current banking import and export This modules tries to combine all current banking import and export
schemes. Rationale for this is that it is quite common to have foreign schemes. Rationale for this is that it is quite common to have foreign
bank account numbers next to national bank account numbers. The current bank account numbers next to national bank account numbers. The current

View File

@@ -70,10 +70,6 @@ from openerp.addons.decimal_precision import decimal_precision as dp
from openerp.addons.account_banking import sepa from openerp.addons.account_banking import sepa
from openerp.addons.account_banking.wizard.banktools import get_or_create_bank from openerp.addons.account_banking.wizard.banktools import get_or_create_bank
def warning(title, message):
'''Convenience routine'''
return {'warning': {'title': title, 'message': message}}
class account_banking_account_settings(orm.Model): class account_banking_account_settings(orm.Model):
'''Default Journal for Bank Account''' '''Default Journal for Bank Account'''
@@ -562,413 +558,6 @@ class account_bank_statement_line(orm.Model):
account_bank_statement_line() account_bank_statement_line()
class res_partner_bank(orm.Model):
'''
This is a hack to circumvent the very limited but widely used base_iban
dependency. The usage of __mro__ requires inside information of
inheritence. This code is tested and works - it bypasses base_iban
altogether. Be sure to use 'super' for inherited classes from here though.
Extended functionality:
1. BBAN and IBAN are considered equal
2. Online databases are checked when available
3. Banks are created on the fly when using IBAN
4. Storage is uppercase, not lowercase
5. Presentation is formal IBAN
6. BBAN's are generated from IBAN when possible
7. In the absence of online databanks, BBAN's are checked on format
using IBAN specs.
'''
_inherit = 'res.partner.bank'
def __init__(self, *args, **kwargs):
'''
Locate founder (first non inherited class) in inheritance tree.
Defaults to super()
Algorithm should prevent moving unknown classes between
base.res_partner_bank and this module's res_partner_bank.
'''
self._founder = super(res_partner_bank, self)
self._founder.__init__(*args, **kwargs)
mro = self.__class__.__mro__
for i in range(len(mro)):
if mro[i].__module__.startswith('openerp.addons.base.'):
self._founder = mro[i]
break
def init(self, cr):
'''
Update existing iban accounts to comply to new regime
'''
partner_bank_obj = self.pool.get('res.partner.bank')
bank_ids = partner_bank_obj.search(
cr, SUPERUSER_ID, [('state', '=', 'iban')], limit=0)
for bank in partner_bank_obj.read(cr, SUPERUSER_ID, bank_ids):
write_vals = {}
if bank['state'] == 'iban':
iban_acc = sepa.IBAN(bank['acc_number'])
if iban_acc.valid:
write_vals['acc_number_domestic'] = iban_acc.localized_BBAN
write_vals['acc_number'] = str(iban_acc)
elif bank['acc_number'] != bank['acc_number'].upper():
write_vals['acc_number'] = bank['acc_number'].upper()
if write_vals:
partner_bank_obj.write(
cr, SUPERUSER_ID, bank['id'], write_vals)
@staticmethod
def _correct_IBAN(acc_number):
'''
Routine to correct IBAN values and deduce localized values when valid.
Note: No check on validity IBAN/Country
'''
iban = sepa.IBAN(acc_number)
return (str(iban), iban.localized_BBAN)
def create(self, cr, uid, vals, context=None):
'''
Create dual function IBAN account for SEPA countries
'''
if vals.get('state') == 'iban':
iban = (vals.get('acc_number')
or vals.get('acc_number_domestic', False))
vals['acc_number'], vals['acc_number_domestic'] = (
self._correct_IBAN(iban))
return self._founder.create(self, cr, uid, vals, context)
def write(self, cr, uid, ids, vals, context=None):
'''
Create dual function IBAN account for SEPA countries
Update the domestic account number when the IBAN is
written, or clear the domestic number on regular account numbers.
'''
if ids and isinstance(ids, (int, long)):
ids = [ids]
for account in self.read(
cr, uid, ids, ['state', 'acc_number']):
if 'state' in vals or 'acc_number' in vals:
account.update(vals)
if account['state'] == 'iban':
vals['acc_number'], vals['acc_number_domestic'] = (
self._correct_IBAN(account['acc_number']))
else:
vals['acc_number_domestic'] = False
self._founder.write(self, cr, uid, account['id'], vals, context)
return True
def search(self, cr, uid, args, *rest, **kwargs):
'''
Overwrite search, as both acc_number and iban now can be filled, so
the original base_iban 'search and search again fuzzy' tactic now can
result in doubled findings. Also there is now enough info to search
for local accounts when a valid IBAN was supplied.
Chosen strategy: create complex filter to find all results in just
one search
'''
def is_term(arg):
'''Flag an arg as term or otherwise'''
return isinstance(arg, (list, tuple)) and len(arg) == 3
def extended_filter_term(term):
'''
Extend the search criteria in term when appropriate.
'''
extra_term = None
if term[0].lower() == 'acc_number' and term[1] in ('=', '=='):
iban = sepa.IBAN(term[2])
if iban.valid:
# Some countries can't convert to BBAN
try:
bban = iban.localized_BBAN
# Prevent empty search filters
if bban:
extra_term = ('acc_number_domestic', term[1], bban)
except:
pass
if extra_term:
return ['|', term, extra_term]
return [term]
def extended_search_expression(args):
'''
Extend the search expression in args when appropriate.
The expression itself is in reverse polish notation, so recursion
is not needed.
'''
if not args:
return []
all = []
if is_term(args[0]) and len(args) > 1:
# Classic filter, implicit '&'
all += ['&']
for arg in args:
if is_term(arg):
all += extended_filter_term(arg)
else:
all += arg
return all
# Extend search filter
newargs = extended_search_expression(args)
# Original search
results = super(res_partner_bank, self).search(
cr, uid, newargs, *rest, **kwargs)
return results
def read(
self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
'''
Convert IBAN electronic format to IBAN display format
SR 2012-02-19: do we really need this? Fields are converted upon write already.
'''
if fields and 'state' not in fields:
fields.append('state')
records = self._founder.read(self, cr, uid, ids, fields, context, load)
is_list = True
if not isinstance(records, list):
records = [records,]
is_list = False
for record in records:
if 'acc_number' in record and record['state'] == 'iban':
record['acc_number'] = unicode(sepa.IBAN(record['acc_number']))
if is_list:
return records
return records[0]
def check_iban(self, cr, uid, ids, context=None):
'''
Check IBAN number
'''
for bank_acc in self.browse(cr, uid, ids, context=context):
if bank_acc.state == 'iban' and bank_acc.acc_number:
iban = sepa.IBAN(bank_acc.acc_number)
if not iban.valid:
return False
return True
def get_bban_from_iban(self, cr, uid, ids, context=None):
'''
Return the local bank account number aka BBAN from the IBAN.
'''
res = {}
for record in self.browse(cr, uid, ids, context):
if not record.state == 'iban':
res[record.id] = False
else:
iban_acc = sepa.IBAN(record.acc_number)
try:
res[record.id] = iban_acc.localized_BBAN
except NotImplementedError:
res[record.id] = False
return res
def onchange_acc_number(
self, cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None):
if state == 'iban':
return self.onchange_iban(
cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None
)
else:
return self.onchange_domestic(
cr, uid, ids, acc_number,
partner_id, country_id, context=None
)
def onchange_domestic(
self, cr, uid, ids, acc_number,
partner_id, country_id, context=None):
'''
Trigger to find IBAN. When found:
1. Reformat BBAN
2. Autocomplete bank
TODO: prevent unnecessary assignment of country_ids and
browsing of the country
'''
if not acc_number:
return {}
values = {}
country_obj = self.pool.get('res.country')
country_ids = []
country = False
# Pre fill country based on available data. This is just a default
# which can be overridden by the user.
# 1. Use provided country_id (manually filled)
if country_id:
country = country_obj.browse(cr, uid, country_id, context=context)
country_ids = [country_id]
# 2. Use country_id of found bank accounts
# This can be usefull when there is no country set in the partners
# addresses, but there was a country set in the address for the bank
# account itself before this method was triggered.
elif ids and len(ids) == 1:
partner_bank_obj = self.pool.get('res.partner.bank')
partner_bank_id = partner_bank_obj.browse(cr, uid, ids[0], context=context)
if partner_bank_id.country_id:
country = partner_bank_id.country_id
country_ids = [country.id]
# 3. Use country_id of default address of partner
# The country_id of a bank account is a one time default on creation.
# It originates in the same address we are about to check, but
# modifications on that address afterwards are not transfered to the
# bank account, hence the additional check.
elif partner_id:
partner_obj = self.pool.get('res.partner')
country = partner_obj.browse(cr, uid, partner_id, context=context).country
country_ids = country and [country.id] or []
# 4. Without any of the above, take the country from the company of
# the handling user
if not country_ids:
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
# Try user companies partner (user no longer has address in 6.1)
if (user.company_id and
user.company_id.partner_id and
user.company_id.partner_id.country
):
country_ids = [user.company_id.partner_id.country.id]
else:
if (user.company_id and user.company_id.partner_id and
user.company_id.partner_id.country):
country_ids = [user.company_id.partner_id.country.id]
else:
# Ok, tried everything, give up and leave it to the user
return warning(_('Insufficient data'),
_('Insufficient data to select online '
'conversion database')
)
result = {'value': values}
# Complete data with online database when available
if country_ids:
country = country_obj.browse(
cr, uid, country_ids[0], context=context)
values['country_id'] = country_ids[0]
if country and country.code in sepa.IBAN.countries:
try:
info = sepa.online.account_info(country.code, acc_number)
if info:
iban_acc = sepa.IBAN(info.iban)
if iban_acc.valid:
values['acc_number_domestic'] = iban_acc.localized_BBAN
values['acc_number'] = unicode(iban_acc)
values['state'] = 'iban'
bank_id, country_id = get_or_create_bank(
self.pool, cr, uid,
info.bic or iban_acc.BIC_searchkey,
name = info.bank
)
if country_id:
values['country_id'] = country_id
values['bank'] = bank_id or False
if info.bic:
values['bank_bic'] = info.bic
else:
info = None
if info is None:
result.update(warning(
_('Invalid data'),
_('The account number appears to be invalid for %s')
% country.name
))
except NotImplementedError:
if country.code in sepa.IBAN.countries:
acc_number_fmt = sepa.BBAN(acc_number, country.code)
if acc_number_fmt.valid:
values['acc_number_domestic'] = str(acc_number_fmt)
else:
result.update(warning(
_('Invalid format'),
_('The account number has the wrong format for %s')
% country.name
))
return result
def onchange_iban(
self, cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None):
'''
Trigger to verify IBAN. When valid:
1. Extract BBAN as local account
2. Auto complete bank
'''
if not acc_number:
return {}
iban_acc = sepa.IBAN(acc_number)
if iban_acc.valid:
bank_id, country_id = get_or_create_bank(
self.pool, cr, uid, iban_acc.BIC_searchkey,
code=iban_acc.BIC_searchkey
)
return {
'value': dict(
acc_number_domestic = iban_acc.localized_BBAN,
acc_number = unicode(iban_acc),
country = country_id or False,
bank = bank_id or False,
)
}
return warning(_('Invalid IBAN account number!'),
_("The IBAN number doesn't seem to be correct")
)
res_partner_bank()
class res_bank(orm.Model):
'''
Add a on_change trigger to automagically fill bank details from the
online SWIFT database. Allow hand filled names to overrule SWIFT names.
'''
_inherit = 'res.bank'
def onchange_bic(self, cr, uid, ids, bic, name, context=None):
'''
Trigger to auto complete other fields.
'''
if not bic:
return {}
info, address = sepa.online.bank_info(bic)
if not info:
return {}
if address and address.country_id:
country_id = self.pool.get('res.country').search(
cr, uid, [('code','=',address.country_id)]
)
country_id = country_id and country_id[0] or False
else:
country_id = False
return {
'value': dict(
# Only the first eight positions of BIC are used for bank
# transfers, so ditch the rest.
bic = info.bic[:8],
street = address.street,
street2 =
address.has_key('street2') and address.street2 or False,
zip = address.zip,
city = address.city,
country = country_id,
name = name and name or info.name,
)
}
res_bank()
class invoice(orm.Model): class invoice(orm.Model):
''' '''
Create other reference types as well. Create other reference types as well.

View File

@@ -286,35 +286,6 @@
</field> </field>
</record> </record>
<record id="view_partner_bank_account_banking_form_2" model="ir.ui.view">
<field name="name">res.partner.bank.form.banking-2</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_form"/>
<field name="priority" eval="24"/>
<field name="arch" type="xml">
<data>
<field name="acc_number" position="attributes">
<attribute name="on_change">onchange_acc_number(acc_number, acc_number_domestic, state, partner_id, country_id)</attribute>
</field>
<field name="acc_number_domestic" position="attributes">
<attribute name="on_change">onchange_domestic(acc_number_domestic, partner_id, country_id)</attribute>
</field>
</data>
</field>
</record>
<!-- Set trigger on BIC in res_bank form -->
<record id="view_res_bank_account_banking_form_1" model="ir.ui.view">
<field name="name">res.bank.form.banking-1</field>
<field name="model">res.bank</field>
<field name="inherit_id" ref="base.view_res_bank_form"/>
<field name="arch" type="xml">
<field name="bic" position="replace">
<field name="bic" on_change="onchange_bic(bic, name)"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="view_bank_statement_line_tree"> <record model="ir.ui.view" id="view_bank_statement_line_tree">
<field name="name">Bank statement line tree view</field> <field name="name">Bank statement line tree view</field>
<field name="model">account.bank.statement.line</field> <field name="model">account.bank.statement.line</field>

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright 2011 - 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm
class ResBank(orm.Model):
_inherit = 'res.bank'
def online_bank_info(self, cr, uid, bic, context=None):
"""
API hook for legacy online lookup of BICs,
to be removed in OpenERP 8.0.
"""
return False, False

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 - 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm
class ResBank(orm.Model):
_inherit = 'res.bank'
def online_account_info(
self, cr, uid, country_code, acc_number, context=None):
"""
API hook for legacy online lookup of account info,
to be removed in OpenERP 8.0.
"""
return False

View File

@@ -19,6 +19,5 @@
# #
############################################################################## ##############################################################################
import iban import iban
import online
IBAN = iban.IBAN IBAN = iban.IBAN
BBAN = iban.BBAN BBAN = iban.BBAN

View File

@@ -63,12 +63,6 @@ def get_bank_accounts(pool, cr, uid, account_number, log, fail=False):
bank_account_ids = partner_bank_obj.search(cr, uid, [ bank_account_ids = partner_bank_obj.search(cr, uid, [
('acc_number', '=', account_number) ('acc_number', '=', account_number)
]) ])
if not bank_account_ids:
# SR 2012-02-19 does the search() override in res_partner_bank
# provides this result on the previous query?
bank_account_ids = partner_bank_obj.search(cr, uid, [
('acc_number_domestic', '=', account_number)
])
if not bank_account_ids: if not bank_account_ids:
if not fail: if not fail:
log.append( log.append(
@@ -237,7 +231,7 @@ def get_or_create_bank(pool, cr, uid, bic, online=False, code=None,
bank_id = False bank_id = False
if online: if online:
info, address = sepa.online.bank_info(bic) info, address = bank_obj.online_bank_info(cr, uid, bic, context=context)
if info: if info:
bank_id = bank_obj.create(cr, uid, dict( bank_id = bank_obj.create(cr, uid, dict(
code = info.code, code = info.code,
@@ -301,7 +295,6 @@ def create_bank_account(pool, cr, uid, partner_id,
owner_name = holder_name, owner_name = holder_name,
country_id = country_id, country_id = country_id,
) )
bankcode = None
# Are we dealing with IBAN? # Are we dealing with IBAN?
iban = sepa.IBAN(account_number) iban = sepa.IBAN(account_number)
@@ -309,23 +302,20 @@ def create_bank_account(pool, cr, uid, partner_id,
# Take as much info as possible from IBAN # Take as much info as possible from IBAN
values.state = 'iban' values.state = 'iban'
values.acc_number = str(iban) values.acc_number = str(iban)
values.acc_number_domestic = iban.BBAN
bankcode = iban.bankcode + iban.countrycode
else: else:
# No, try to convert to IBAN # No, try to convert to IBAN
values.state = 'bank' values.state = 'bank'
values.acc_number = values.acc_number_domestic = account_number values.acc_number = account_number
if country_id: if country_id:
country_code = pool.get('res.country').read( country_code = pool.get('res.country').read(
cr, uid, country_id, ['code'], context=context)['code'] cr, uid, country_id, ['code'], context=context)['code']
if country_code in sepa.IBAN.countries: if country_code in sepa.IBAN.countries:
account_info = sepa.online.account_info( account_info = pool['res.partner.bank'].online_account_info(
country_code, values.acc_number) cr, uid, country_code, values.acc_number, context=context)
if account_info: if account_info:
values.acc_number = iban = account_info.iban values.acc_number = iban = account_info.iban
values.state = 'iban' values.state = 'iban'
bankcode = account_info.code
bic = account_info.bic bic = account_info.bic
if bic: if bic:

View File

@@ -0,0 +1,4 @@
from . import model
from . import online
from . import urlagent

View File

@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013 Therp BV (<http://therp.nl>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Banking Addons - Iban lookup (legacy)',
'version': '0.1',
'license': 'AGPL-3',
'author': 'Therp BV',
'website': 'https://launchpad.net/banking-addons',
'category': 'Banking addons',
'depends': [
'account_banking',
'account_iban_preserve_domestic',
],
'external_dependencies': {
'python' : ['BeautifulSoup'],
},
'description': '''
This addons contains the legacy infrastructure for autocompletion of IBANs
and BBANs.
The autocompletion was implemented for Dutch IBANs, but as it turns out
the online database that it consults does not get updated. As a result,
the autocompletion will come up with outdated IBANs and BICs.
This module is deprecated and will be dropped in OpenERP 8.0.
''',
'auto_install': False,
'installable': True,
}

View File

@@ -0,0 +1,2 @@
from . import res_bank
from . import res_partner_bank

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2014 Therp BV (<http://therp.nl>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm
from openerp.addons.account_banking_iban_lookup import online
class ResBank(orm.Model):
_inherit = 'res.bank'
def online_bank_info(self, cr, uid, bic, context=None):
"""
Overwrite existing API hook from account_banking
"""
return online.bank_info(bic)
def onchange_bic(
self, cr, uid, ids, bic, name, context=None):
if not bic:
return {}
info, address = online.bank_info(bic)
if not info:
return {}
if address and address.country_id:
country_id = self.pool.get('res.country').search(
cr, uid, [('code','=',address.country_id)]
)
country_id = country_id and country_id[0] or False
else:
country_id = False
return {
'value': dict(
# Only the first eight positions of BIC are used for bank
# transfers, so ditch the rest.
bic = info.bic[:8],
street = address.street,
street2 =
address.has_key('street2') and address.street2 or False,
zip = address.zip,
city = address.city,
country = country_id,
name = name and name or info.name,
)
}

View File

@@ -0,0 +1,288 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# (C) 2011 - 2013 Therp BV (<http://therp.nl>).
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.osv import orm
from openerp.tools.translate import _
from openerp.addons.account_banking_iban_lookup import online
from openerp.addons.account_banking import sepa
from openerp.addons.account_banking.wizard.banktools import get_or_create_bank
def warning(title, message):
'''Convenience routine'''
return {'warning': {'title': title, 'message': message}}
class res_partner_bank(orm.Model):
'''
This is a hack to circumvent the very limited but widely used base_iban
dependency. The usage of __mro__ requires inside information of
inheritence. This code is tested and works - it bypasses base_iban
altogether. Be sure to use 'super' for inherited classes from here though.
Extended functionality:
1. BBAN and IBAN are considered equal
3. Banks are created on the fly when using IBAN
4. Storage is uppercase, not lowercase
5. Presentation is formal IBAN
6. BBAN's are generated from IBAN when possible
'''
_inherit = 'res.partner.bank'
def __init__(self, *args, **kwargs):
'''
Locate founder (first non inherited class) in inheritance tree.
Defaults to super()
Algorithm should prevent moving unknown classes between
base.res_partner_bank and this module's res_partner_bank.
'''
self._founder = super(res_partner_bank, self)
self._founder.__init__(*args, **kwargs)
mro = self.__class__.__mro__
for i in range(len(mro)):
if mro[i].__module__.startswith('openerp.addons.base.'):
self._founder = mro[i]
break
def init(self, cr):
'''
Update existing iban accounts to comply to new regime
'''
partner_bank_obj = self.pool.get('res.partner.bank')
bank_ids = partner_bank_obj.search(
cr, SUPERUSER_ID, [('state', '=', 'iban')], limit=0)
for bank in partner_bank_obj.read(cr, SUPERUSER_ID, bank_ids):
write_vals = {}
if bank['state'] == 'iban':
iban_acc = sepa.IBAN(bank['acc_number'])
if iban_acc.valid:
write_vals['acc_number_domestic'] = iban_acc.localized_BBAN
write_vals['acc_number'] = str(iban_acc)
elif bank['acc_number'] != bank['acc_number'].upper():
write_vals['acc_number'] = bank['acc_number'].upper()
if write_vals:
partner_bank_obj.write(
cr, SUPERUSER_ID, bank['id'], write_vals)
@staticmethod
def _correct_IBAN(acc_number):
'''
Routine to correct IBAN values and deduce localized values when valid.
Note: No check on validity IBAN/Country
'''
iban = sepa.IBAN(acc_number)
return (str(iban), iban.localized_BBAN)
def create(self, cr, uid, vals, context=None):
'''
Create dual function IBAN account for SEPA countries
'''
if vals.get('state') == 'iban':
iban = (vals.get('acc_number')
or vals.get('acc_number_domestic', False))
vals['acc_number'], vals['acc_number_domestic'] = (
self._correct_IBAN(iban))
return self._founder.create(self, cr, uid, vals, context)
def write(self, cr, uid, ids, vals, context=None):
'''
Create dual function IBAN account for SEPA countries
Update the domestic account number when the IBAN is
written, or clear the domestic number on regular account numbers.
'''
if ids and isinstance(ids, (int, long)):
ids = [ids]
for account in self.read(
cr, uid, ids, ['state', 'acc_number']):
if 'state' in vals or 'acc_number' in vals:
account.update(vals)
if account['state'] == 'iban':
vals['acc_number'], vals['acc_number_domestic'] = (
self._correct_IBAN(account['acc_number']))
else:
vals['acc_number_domestic'] = False
self._founder.write(self, cr, uid, account['id'], vals, context)
return True
def onchange_acc_number(
self, cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None):
if state == 'iban':
return self.onchange_iban(
cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None
)
else:
return self.onchange_domestic(
cr, uid, ids, acc_number,
partner_id, country_id, context=None
)
def onchange_domestic(
self, cr, uid, ids, acc_number,
partner_id, country_id, context=None):
'''
Trigger to find IBAN. When found:
1. Reformat BBAN
2. Autocomplete bank
TODO: prevent unnecessary assignment of country_ids and
browsing of the country
'''
if not acc_number:
return {}
values = {}
country_obj = self.pool.get('res.country')
country_ids = []
country = False
# Pre fill country based on available data. This is just a default
# which can be overridden by the user.
# 1. Use provided country_id (manually filled)
if country_id:
country = country_obj.browse(cr, uid, country_id, context=context)
country_ids = [country_id]
# 2. Use country_id of found bank accounts
# This can be usefull when there is no country set in the partners
# addresses, but there was a country set in the address for the bank
# account itself before this method was triggered.
elif ids and len(ids) == 1:
partner_bank_obj = self.pool.get('res.partner.bank')
partner_bank_id = partner_bank_obj.browse(cr, uid, ids[0], context=context)
if partner_bank_id.country_id:
country = partner_bank_id.country_id
country_ids = [country.id]
# 3. Use country_id of default address of partner
# The country_id of a bank account is a one time default on creation.
# It originates in the same address we are about to check, but
# modifications on that address afterwards are not transfered to the
# bank account, hence the additional check.
elif partner_id:
partner_obj = self.pool.get('res.partner')
country = partner_obj.browse(cr, uid, partner_id, context=context).country
country_ids = country and [country.id] or []
# 4. Without any of the above, take the country from the company of
# the handling user
if not country_ids:
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
# Try user companies partner (user no longer has address in 6.1)
if (user.company_id and
user.company_id.partner_id and
user.company_id.partner_id.country
):
country_ids = [user.company_id.partner_id.country.id]
else:
if (user.company_id and user.company_id.partner_id and
user.company_id.partner_id.country):
country_ids = [user.company_id.partner_id.country.id]
else:
# Ok, tried everything, give up and leave it to the user
return warning(_('Insufficient data'),
_('Insufficient data to select online '
'conversion database')
)
result = {'value': values}
# Complete data with online database when available
if country_ids:
country = country_obj.browse(
cr, uid, country_ids[0], context=context)
values['country_id'] = country_ids[0]
if country and country.code in sepa.IBAN.countries:
try:
info = online.account_info(country.code, acc_number)
if info:
iban_acc = sepa.IBAN(info.iban)
if iban_acc.valid:
values['acc_number_domestic'] = iban_acc.localized_BBAN
values['acc_number'] = unicode(iban_acc)
values['state'] = 'iban'
bank_id, country_id = get_or_create_bank(
self.pool, cr, uid,
info.bic or iban_acc.BIC_searchkey,
name = info.bank
)
if country_id:
values['country_id'] = country_id
values['bank'] = bank_id or False
if info.bic:
values['bank_bic'] = info.bic
else:
info = None
if info is None:
result.update(warning(
_('Invalid data'),
_('The account number appears to be invalid for %s')
% country.name
))
except NotImplementedError:
if country.code in sepa.IBAN.countries:
acc_number_fmt = sepa.BBAN(acc_number, country.code)
if acc_number_fmt.valid:
values['acc_number_domestic'] = str(acc_number_fmt)
else:
result.update(warning(
_('Invalid format'),
_('The account number has the wrong format for %s')
% country.name
))
return result
def onchange_iban(
self, cr, uid, ids, acc_number, acc_number_domestic,
state, partner_id, country_id, context=None):
'''
Trigger to verify IBAN. When valid:
1. Extract BBAN as local account
2. Auto complete bank
'''
if not acc_number:
return {}
iban_acc = sepa.IBAN(acc_number)
if iban_acc.valid:
bank_id, country_id = get_or_create_bank(
self.pool, cr, uid, iban_acc.BIC_searchkey,
code=iban_acc.BIC_searchkey
)
return {
'value': dict(
acc_number_domestic = iban_acc.localized_BBAN,
acc_number = unicode(iban_acc),
country = country_id or False,
bank = bank_id or False,
)
}
return warning(_('Invalid IBAN account number!'),
_("The IBAN number doesn't seem to be correct")
)
def online_account_info(
self, cr, uid, country_code, acc_number, context=None):
"""
Overwrite API hook from account_banking
"""
return online.account_info(country_code, acc_number)

View File

@@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*- # -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>). # Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
@@ -26,7 +26,7 @@ import re
import urllib, urllib2 import urllib, urllib2
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from openerp.addons.account_banking.sepa import postalcode from openerp.addons.account_banking.sepa import postalcode
from openerp.addons.account_banking.sepa.urlagent import URLAgent, SoupForm from openerp.addons.account_banking_iban_lookup.urlagent import URLAgent, SoupForm
from openerp.addons.account_banking.sepa.iban import IBAN from openerp.addons.account_banking.sepa.iban import IBAN
from openerp.addons.account_banking.struct import struct from openerp.addons.account_banking.struct import struct

View File

@@ -25,7 +25,6 @@ forms and to parse the results back in. It is heavily based on BeautifulSoup.
''' '''
import urllib import urllib
from BeautifulSoup import BeautifulSoup
__all__ = ['urlsplit', 'urljoin', 'pathbase', 'urlbase', 'SoupForm', __all__ = ['urlsplit', 'urljoin', 'pathbase', 'urlbase', 'SoupForm',
'URLAgent' 'URLAgent'

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_res_bank_account_banking_form_1" model="ir.ui.view">
<field name="name">Add BIC lookup to bank form</field>
<field name="model">res.bank</field>
<field name="inherit_id" ref="base.view_res_bank_form"/>
<field name="arch" type="xml">
<field name="bic" position="replace">
<field name="bic" on_change="onchange_bic(bic, name)"/>
</field>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_partner_bank_account_banking_form_2" model="ir.ui.view">
<field name="name">Add autocompletion methods to partner bank form</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_form"/>
<field name="priority" eval="24"/>
<field name="arch" type="xml">
<data>
<field name="acc_number" position="attributes">
<attribute name="on_change">onchange_acc_number(acc_number, acc_number_domestic, state, partner_id, country_id)</attribute>
</field>
<field name="acc_number_domestic" position="attributes">
<attribute name="on_change">onchange_domestic(acc_number_domestic, partner_id, country_id)</attribute>
</field>
</data>
</field>
</record>
</data>
</openerp>

View File

@@ -24,7 +24,10 @@
'author': 'EduSense BV', 'author': 'EduSense BV',
'website': 'http://www.edusense.nl', 'website': 'http://www.edusense.nl',
'category': 'Account Banking', 'category': 'Account Banking',
'depends': ['account_banking_payment'], 'depends': [
'account_banking_payment',
'account_iban_preserve_domestic',
],
'data': [ 'data': [
'account_banking_nl_clieop.xml', 'account_banking_nl_clieop.xml',
'wizard/export_clieop_view.xml', 'wizard/export_clieop_view.xml',

View File

@@ -116,15 +116,25 @@ class banking_import_transaction(orm.Model):
''' '''
# TODO: Not sure what side effects are created when payments are done # TODO: Not sure what side effects are created when payments are done
# for credited customer invoices, which will be matched later on too. # for credited customer invoices, which will be matched later on too.
def bank_match(account, partner_bank):
"""
Compare account number with or without presence
of the domestic number field
"""
return (
account == partner_bank.acc_number
or (hasattr(partner_bank, 'acc_number_domestic')
and account == partner_bank.acc_number_domestic)
)
digits = dp.get_precision('Account')(cr)[1] digits = dp.get_precision('Account')(cr)[1]
candidates = [ candidates = [
x for x in payment_lines x for x in payment_lines
if x.communication == trans.reference if x.communication == trans.reference
and round(x.amount, digits) == -round( and round(x.amount, digits) == -round(
trans.statement_line_id.amount, digits) trans.statement_line_id.amount, digits)
and trans.remote_account in (x.bank_id.acc_number, and bank_match(trans.remote_account, x.bank_id)]
x.bank_id.acc_number_domestic)
]
if len(candidates) == 1: if len(candidates) == 1:
candidate = candidates[0] candidate = candidates[0]
# Check cache to prevent multiple matching of a single payment # Check cache to prevent multiple matching of a single payment