Files
bank-payment/account_banking/account_banking.py
2013-04-07 22:27:20 +02:00

1099 lines
45 KiB
Python

# -*- 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/>.
#
##############################################################################
'''
This module shows resemblance to both account_bankimport/bankimport.py,
account/account_bank_statement.py and account_payment(_export). All hail to
the makers. account_bankimport is only referenced for their ideas and the
framework of the filters, which they in their turn seem to have derived
from account_coda.
Modifications are extensive:
1. In relation to account/account_bank_statement.py:
account.bank.statement is effectively stripped from its account.period
association, while account.bank.statement.line is extended with the same
association, thereby reflecting real world usage of bank.statement as a
list of bank transactions and bank.statement.line as a bank transaction.
2. In relation to account/account_bankimport:
All filter objects and extensions to res.company are removed. Instead a
flexible auto-loading and auto-browsing plugin structure is created,
whereby business logic and encoding logic are strictly separated.
Both parsers and business logic are rewritten from scratch.
The association of account.journal with res.company is replaced by an
association of account.journal with res.partner.bank, thereby allowing
multiple bank accounts per company and one journal per bank account.
The imported bank statement file does not result in a single 'bank
statement', but in a list of bank statements by definition of whatever the
bank sees as a statement. Every imported bank statement contains at least
one bank transaction, which is a modded account.bank.statement.line.
3. In relation to account_payment:
An additional state was inserted between 'open' and 'done', to reflect a
exported bank orders file which was not reported back through statements.
The import of statements matches the payments and reconciles them when
needed, flagging them 'done'. When no export wizards are found, the
default behavior is to flag the orders as 'sent', not as 'done'.
Rejected payments from the bank receive on import the status 'rejected'.
'''
import time
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp import netsvc, SUPERUSER_ID
from openerp.addons.decimal_precision import decimal_precision as dp
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 account_banking_account_settings(orm.Model):
'''Default Journal for Bank Account'''
_name = 'account.banking.account.settings'
_description = __doc__
_columns = {
'company_id': fields.many2one('res.company', 'Company', select=True,
required=True),
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
select=True, required=True),
'journal_id': fields.many2one('account.journal', 'Journal',
required=True),
'partner_id': fields.related(
'company_id', 'partner_id',
type='many2one', relation='res.partner',
string='Partner'),
'default_credit_account_id': fields.many2one(
'account.account', 'Default credit account', select=True,
help=('The account to use when an unexpected payment was signaled. '
'This can happen when a direct debit payment is cancelled '
'by a customer, or when no matching payment can be found. '
' Mind that you can correct movements before confirming them.'
),
required=True
),
'default_debit_account_id': fields.many2one(
'account.account', 'Default debit account',
select=True, required=True,
help=('The account to use when an unexpected payment is received. '
'This can be needed when a customer pays in advance or when '
'no matching invoice can be found. Mind that you can correct '
'movements before confirming them.'
),
),
'costs_account_id': fields.many2one(
'account.account', 'Bank Costs Account', select=True,
help=('The account to use when the bank invoices its own costs. '
'Leave it blank to disable automatic invoice generation '
'on bank costs.'
),
),
'invoice_journal_id': fields.many2one(
'account.journal', 'Costs Journal',
help=('This is the journal used to create invoices for bank costs.'
),
),
'bank_partner_id': fields.many2one(
'res.partner', 'Bank Partner',
help=('The partner to use for bank costs. Banks are not partners '
'by default. You will most likely have to create one.'
),
),
}
def _default_company(self, cr, uid, context=None):
"""
Return the user's company or the first company found
in the database
"""
user = self.pool.get('res.users').read(
cr, uid, uid, ['company_id'], context=context)
if user['company_id']:
return user['company_id'][0]
return self.pool.get('res.company').search(
cr, uid, [('parent_id', '=', False)])[0]
def _default_partner_id(self, cr, uid, context=None, company_id=False):
if not company_id:
company_id = self._default_company(cr, uid, context=context)
return self.pool.get('res.company').read(
cr, uid, company_id, ['partner_id'],
context=context)['partner_id'][0]
def _default_journal(self, cr, uid, context=None, company_id=False):
if not company_id:
company_id = self._default_company(cr, uid, context=context)
journal_ids = self.pool.get('account.journal').search(
cr, uid, [('type', '=', 'bank'), ('company_id', '=', company_id)])
return journal_ids and journal_ids[0] or False
def _default_partner_bank_id(
self, cr, uid, context=None, company_id=False):
if not company_id:
company_id = self._default_company(cr, uid, context=context)
partner_id = self.pool.get('res.company').read(
cr, uid, company_id, ['partner_id'], context=context)['partner_id'][0]
bank_ids = self.pool.get('res.partner.bank').search(
cr, uid, [('partner_id', '=', partner_id)], context=context)
return bank_ids and bank_ids[0] or False
def _default_debit_account_id(
self, cr, uid, context=None, company_id=False):
localcontext = context and context.copy() or {}
localcontext['force_company'] = (
company_id or self._default_company(cr, uid, context=context))
account_def = self.pool.get('ir.property').get(
cr, uid, 'property_account_receivable',
'res.partner', context=localcontext)
return account_def and account_def.id or False
def _default_credit_account_id(self, cr, uid, context=None, company_id=False):
localcontext = context and context.copy() or {}
localcontext['force_company'] = (
company_id or self._default_company(cr, uid, context=context))
account_def = self.pool.get('ir.property').get(
cr, uid, 'property_account_payable',
'res.partner', context=localcontext)
return account_def and account_def.id or False
def find(self, cr, uid, journal_id, partner_bank_id=False, context=None):
domain = [('journal_id','=',journal_id)]
if partner_bank_id:
domain.append(('partner_bank_id','=',partner_bank_id))
return self.search(cr, uid, domain, context=context)
def onchange_partner_bank_id(
self, cr, uid, ids, partner_bank_id, context=None):
values = {}
if partner_bank_id:
bank = self.pool.get('res.partner.bank').read(
cr, uid, partner_bank_id, ['journal_id'], context=context)
if bank['journal_id']:
values['journal_id'] = bank['journal_id'][0]
return {'value': values}
def onchange_company_id (
self, cr, uid, ids, company_id=False, context=None):
if not company_id:
return {}
result = {
'partner_id': self._default_partner_id(
cr, uid, company_id=company_id, context=context),
'journal_id': self._default_journal(
cr, uid, company_id=company_id, context=context),
'default_debit_account_id': self._default_debit_account_id(
cr, uid, company_id=company_id, context=context),
'default_credit_account_id': self._default_credit_account_id(
cr, uid, company_id=company_id, context=context),
}
return {'value': result}
_defaults = {
'company_id': _default_company,
'partner_id': _default_partner_id,
'journal_id': _default_journal,
'default_debit_account_id': _default_debit_account_id,
'default_credit_account_id': _default_credit_account_id,
'partner_bank_id': _default_partner_bank_id,
}
account_banking_account_settings()
class account_banking_imported_file(orm.Model):
'''Imported Bank Statements File'''
_name = 'account.banking.imported.file'
_description = __doc__
_rec_name = 'date'
_columns = {
'company_id': fields.many2one('res.company', 'Company',
select=True, readonly=True
),
'date': fields.datetime('Import Date', readonly=True, select=True,
states={'draft': [('readonly', False)]}
),
'format': fields.char('File Format', size=20, readonly=True,
states={'draft': [('readonly', False)]}
),
'file': fields.binary('Raw Data', readonly=True,
states={'draft': [('readonly', False)]}
),
'log': fields.text('Import Log', readonly=True,
states={'draft': [('readonly', False)]}
),
'user_id': fields.many2one('res.users', 'Responsible User',
readonly=True, select=True,
states={'draft': [('readonly', False)]}
),
'state': fields.selection(
[('unfinished', 'Unfinished'),
('error', 'Error'),
('review', 'Review'),
('ready', 'Finished'),
], 'State', select=True, readonly=True
),
'statement_ids': fields.one2many('account.bank.statement',
'banking_id', 'Statements',
readonly=False,
),
}
_defaults = {
'date': fields.date.context_today,
'user_id': lambda self, cursor, uid, context: uid,
}
account_banking_imported_file()
class account_bank_statement(orm.Model):
'''
Extensions from account_bank_statement:
1. Removed period_id (transformed to optional boolean) - as it is no
longer needed.
NB! because of #1. changes required to account_voucher!
2. Extended 'button_confirm' trigger to cope with the period per
statement_line situation.
3. Added optional relation with imported statements file
4. Ordering is based on auto generated id.
'''
_inherit = 'account.bank.statement'
_order = 'id'
_columns = {
'period_id': fields.many2one('account.period', 'Period',
required=False, readonly=True),
'banking_id': fields.many2one('account.banking.imported.file',
'Imported File', readonly=True,
),
}
_defaults = {
'period_id': False,
}
def _check_company_id(self, cr, uid, ids, context=None):
"""
Adapt this constraint method from the account module to reflect the
move of period_id to the statement line
"""
for statement in self.browse(cr, uid, ids, context=context):
for line in statement.line_ids:
if (line.period_id and
statement.company_id.id != line.period_id.company_id.id):
return False
return super(account_bank_statement, self)._check_company_id(
cr, uid, ids, context=context)
# Redefine the constraint, or it still refer to the original method
_constraints = [
(_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
]
def _get_period(self, cursor, uid, date, context=None):
'''
Find matching period for date, not meant for _defaults.
'''
period_obj = self.pool.get('account.period')
periods = period_obj.find(cursor, uid, dt=date, context=context)
return periods and periods[0] or False
def create_move_from_st_line(self, cr, uid, st_line_id,
company_currency_id, st_line_number,
context=None):
# This is largely a copy of the original code in account
# Modifications are marked with AB
# Modifications by account_voucher are merged below.
# As there is no valid inheritance mechanism for large actions, this
# is the only option to add functionality to existing actions.
# WARNING: when the original code changes, this trigger has to be
# updated in sync.
if context is None:
context = {}
res_currency_obj = self.pool.get('res.currency')
account_move_obj = self.pool.get('account.move')
account_move_line_obj = self.pool.get('account.move.line')
account_bank_statement_line_obj = self.pool.get(
'account.bank.statement.line')
st_line = account_bank_statement_line_obj.browse(
cr, uid, st_line_id, context=context)
period_id = self._get_period(
cr, uid, st_line.date, context=context) # AB
# Start account voucher
# Post the voucher and update links between statement and moves
if st_line.voucher_id:
voucher_pool = self.pool.get('account.voucher')
wf_service = netsvc.LocalService("workflow")
voucher_pool.write(
cr, uid, [st_line.voucher_id.id], {
'number': st_line_number,
'date': st_line.date,
'period_id': period_id, # AB
}, context=context)
if st_line.voucher_id.state == 'cancel':
voucher_pool.action_cancel_draft(
cr, uid, [st_line.voucher_id.id], context=context)
wf_service.trg_validate(
uid, 'account.voucher', st_line.voucher_id.id, 'proforma_voucher', cr)
v = voucher_pool.browse(
cr, uid, st_line.voucher_id.id, context=context)
account_bank_statement_line_obj.write(cr, uid, [st_line_id], {
'move_ids': [(4, v.move_id.id, False)]
})
account_move_line_obj.write(
cr, uid, [x.id for x in v.move_ids],
{'statement_id': st_line.statement_id.id}, context=context)
# End of account_voucher
st_line.refresh()
# AB: The voucher journal isn't automatically posted, so post it (if needed)
if not st_line.voucher_id.journal_id.entry_posted:
account_move_obj.post(cr, uid, [st_line.voucher_id.move_id.id], context={})
return True
st = st_line.statement_id
context.update({'date': st_line.date})
ctxt = context.copy() # AB
ctxt['company_id'] = st_line.company_id.id # AB
move_id = account_move_obj.create(cr, uid, {
'journal_id': st.journal_id.id,
'period_id': period_id, # AB
'date': st_line.date,
'name': st_line_number,
}, context=context)
account_bank_statement_line_obj.write(cr, uid, [st_line.id], {
'move_ids': [(4, move_id, False)]
})
torec = []
if st_line.amount >= 0:
account_id = st.journal_id.default_credit_account_id.id
else:
account_id = st.journal_id.default_debit_account_id.id
acc_cur = ((st_line.amount <= 0 and
st.journal_id.default_debit_account_id) or
st_line.account_id)
context.update({
'res.currency.compute.account': acc_cur,
})
amount = res_currency_obj.compute(cr, uid, st.currency.id,
company_currency_id, st_line.amount, context=context)
val = {
'name': st_line.name,
'date': st_line.date,
'ref': st_line.ref,
'move_id': move_id,
'partner_id': (((st_line.partner_id) and st_line.partner_id.id) or
False),
'account_id': (st_line.account_id) and st_line.account_id.id,
'credit': ((amount>0) and amount) or 0.0,
'debit': ((amount<0) and -amount) or 0.0,
'statement_id': st.id,
'journal_id': st.journal_id.id,
'period_id': period_id, # AB
'currency_id': st.currency.id,
'analytic_account_id': (st_line.analytic_account_id and
st_line.analytic_account_id.id or
False),
}
if st.currency.id <> company_currency_id:
amount_cur = res_currency_obj.compute(cr, uid, company_currency_id,
st.currency.id, amount, context=context)
val['amount_currency'] = -amount_cur
if (st_line.account_id and st_line.account_id.currency_id and
st_line.account_id.currency_id.id <> company_currency_id):
val['currency_id'] = st_line.account_id.currency_id.id
amount_cur = res_currency_obj.compute(cr, uid, company_currency_id,
st_line.account_id.currency_id.id, amount, context=context)
val['amount_currency'] = -amount_cur
move_line_id = account_move_line_obj.create(
cr, uid, val, context=context)
torec.append(move_line_id)
# Fill the secondary amount/currency
# if currency is not the same than the company
amount_currency = False
currency_id = False
if st.currency.id <> company_currency_id:
amount_currency = st_line.amount
currency_id = st.currency.id
account_move_line_obj.create(cr, uid, {
'name': st_line.name,
'date': st_line.date,
'ref': st_line.ref,
'move_id': move_id,
'partner_id': (((st_line.partner_id) and st_line.partner_id.id) or
False),
'account_id': account_id,
'credit': ((amount < 0) and -amount) or 0.0,
'debit': ((amount > 0) and amount) or 0.0,
'statement_id': st.id,
'journal_id': st.journal_id.id,
'period_id': period_id, # AB
'amount_currency': amount_currency,
'currency_id': currency_id,
}, context=context)
for line in account_move_line_obj.browse(cr, uid, [x.id for x in
account_move_obj.browse(cr, uid, move_id,
context=context).line_id],
context=context):
if line.state <> 'valid':
raise orm.except_orm(_('Error !'),
_('Journal Item "%s" is not valid') % line.name)
# Bank statements will not consider boolean on journal entry_posted
account_move_obj.post(cr, uid, [move_id], context=context)
"""
Account-banking:
- Write stored reconcile_id
- Pay invoices through workflow
Does not apply to voucher integration, but only to
payments and payment orders
"""
if st_line.reconcile_id:
account_move_line_obj.write(cr, uid, torec, {
(st_line.reconcile_id.line_partial_ids and
'reconcile_partial_id' or 'reconcile_id'):
st_line.reconcile_id.id }, context=context)
for move_line in (st_line.reconcile_id.line_id or []) + (
st_line.reconcile_id.line_partial_ids or []):
netsvc.LocalService("workflow").trg_trigger(
uid, 'account.move.line', move_line.id, cr)
#""" End account-banking """
return move_id
def button_confirm_bank(self, cr, uid, ids, context=None):
if context is None: context = {}
obj_seq = self.pool.get('ir.sequence')
if not isinstance(ids, list): ids = [ids]
noname_ids = self.search(cr, uid, [('id','in',ids),('name','=','/')])
for st in self.browse(cr, uid, noname_ids, context=context):
if st.journal_id.sequence_id:
year = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, st.date)).fiscalyear_id.id
c = {'fiscalyear_id': year}
st_number = obj_seq.get_id(cr, uid, st.journal_id.sequence_id.id, context=c)
self.write(cr, uid, ids, {'name': st_number})
return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context)
account_bank_statement()
class account_voucher(orm.Model):
_inherit = 'account.voucher'
def _get_period(self, cr, uid, context=None):
if context is None: context = {}
if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse(cr, uid , context.get('move_line_ids'))[0].period_id.id
context['period_id'] = res
return super(account_voucher, self)._get_period(cr, uid, context)
def create(self, cr, uid, values, context=None):
if values.get('period_id') == False and context.get('move_line_ids'):
values['period_id'] = self._get_period(cr, uid, context)
return super(account_voucher, self).create(cr, uid, values, context)
account_voucher()
class account_bank_statement_line(orm.Model):
'''
Extension on basic class:
1. Extra links to account.period and res.partner.bank for tracing and
matching.
2. Extra 'trans' field to carry the transaction id of the bank.
3. Readonly states for most fields except when in draft.
'''
_inherit = 'account.bank.statement.line'
_description = 'Bank Transaction'
def _get_period(self, cursor, user, context=None):
date = context.get('date', None)
periods = self.pool.get('account.period').find(cursor, user, dt=date)
return periods and periods[0] or False
def _get_currency(self, cursor, user, context=None):
'''
Get the default currency (required to allow other modules to function,
which assume currency to be a calculated field and thus optional)
Remark: this is only a fallback as the real default is in the journal,
which is inaccessible from within this method.
'''
res_users_obj = self.pool.get('res.users')
return res_users_obj.browse(cursor, user, user,
context=context).company_id.currency_id.id
def _get_invoice_id(self, cr, uid, ids, name, args, context=None):
res = {}
for st_line in self.browse(cr, uid, ids, context):
res[st_line.id] = False
for move_line in (
st_line.reconcile_id and
(st_line.reconcile_id.line_id or
st_line.reconcile_id.line_partial_ids) or
st_line.import_transaction_id and
st_line.import_transaction_id.move_line_id and
[st_line.import_transaction_id.move_line_id] or []):
if move_line.invoice:
res[st_line.id] = move_line.invoice.id
continue
return res
_columns = {
# Redefines
'amount': fields.float('Amount', readonly=True,
digits_compute=dp.get_precision('Account'),
states={'draft': [('readonly', False)]}),
'ref': fields.char('Ref.', size=32, readonly=True,
states={'draft': [('readonly', False)]}),
'name': fields.char('Name', size=64, required=False, readonly=True,
states={'draft': [('readonly', False)]}),
'date': fields.date('Date', required=True, readonly=True,
states={'draft': [('readonly', False)]}),
#'reconcile_amount': fields.function(_reconcile_amount,
# string='Amount reconciled', method=True, type='float'),
# New columns
'trans': fields.char('Bank Transaction ID', size=15, required=False,
readonly=True,
states={'draft':[('readonly', False)]},
),
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account',
required=False, readonly=True,
states={'draft':[('readonly', False)]},
),
'period_id': fields.many2one('account.period', 'Period', required=True,
states={'confirmed': [('readonly', True)]}),
'currency': fields.many2one('res.currency', 'Currency', required=True,
states={'confirmed': [('readonly', True)]}),
'reconcile_id': fields.many2one(
'account.move.reconcile', 'Reconciliation', readonly=True
),
'invoice_id': fields.function(
_get_invoice_id, method=True, string='Linked Invoice',
type='many2one', relation='account.invoice'
),
}
_defaults = {
'period_id': _get_period,
'currency': _get_currency,
}
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('base.'):
self._founder = mro[i]
break
def init(self, cr):
'''
Update existing iban accounts to comply to new regime
Note that usage of the ORM is not possible here, as the ORM cannot
search on values not provided by the client.
'''
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, cursor, uid, vals, context=None):
'''
Create dual function IBAN account for SEPA countries
'''
if vals.get('state') == 'iban':
iban = vals.get('acc_number',False) or vals.get('acc_number_domestic',False)
vals['acc_number'], vals['acc_number_domestic'] = (
self._correct_IBAN(iban))
return self._founder.create(cursor, 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(cr, uid, account['id'], vals, context)
return True
def search(self, cursor, 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 (_founder)
results = self._founder.search(cursor, 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(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, cursor, uid, ids):
'''
Check IBAN number
'''
for bank_acc in self.browse(cursor, uid, ids):
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, cursor, uid, ids, context=None):
'''
Return the local bank account number aka BBAN from the IBAN.
'''
res = {}
for record in self.browse(cursor, 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, cursor, uid, ids, acc_number,
partner_id, country_id, context=None):
'''
Trigger to find IBAN. When found:
1. Reformat BBAN
2. Autocomplete bank
'''
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(cursor, uid, country_id)
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(cursor, uid, ids[0])
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(cursor, uid, partner_id).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(cursor, uid, uid)
# 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(
cursor, uid, country_ids[0], context=context)
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, cursor, uid,
info.bic or iban_acc.BIC_searchkey,
name = info.bank
)
values['country_id'] = country_id or \
country_ids and country_ids[0] or \
False
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 %(country)s')
% {'country': 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'] = str(acc_number_fmt)
else:
values['acc_number'] = acc_number
result.update(warning(
_('Invalid format'),
_('The account number has the wrong format for %(country)s')
% {'country': country.name}
))
else:
values['acc_number'] = acc_number
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")
)
_constraints = [
# Cannot have this as a constraint as it is rejecting valid numbers from GB and DE
# It works much better without this constraint!
#(check_iban, _("The IBAN number doesn't seem to be correct"), ["acc_number"])
]
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, cursor, 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(
cursor, 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):
'''
Create other reference types as well.
Descendant classes can extend this function to add more reference
types, ie.
def _get_reference_type(self, cr, uid, context=None):
return super(my_class, self)._get_reference_type(cr, uid,
context=context) + [('my_ref', _('My reference')]
Don't forget to redefine the column "reference_type" as below or
your method will never be triggered.
'''
_inherit = 'account.invoice'
def test_undo_paid(self, cr, uid, ids, context=None):
"""
Called from the workflow. Used to unset paid state on
invoices that were paid with bank transfers which are being cancelled
"""
for invoice in self.read(cr, uid, ids, ['reconciled'], context):
if invoice['reconciled']:
return False
return True
def _get_reference_type(self, cr, uid, context=None):
'''
Return the list of reference types
'''
return [('none', _('Free Reference')),
('structured', _('Structured Reference')),
]
_columns = {
'reference_type': fields.selection(_get_reference_type,
'Reference Type', required=True
)
}
invoice()
class account_move_line(orm.Model):
_inherit = "account.move.line"
def get_balance(self, cr, uid, ids, context=None):
"""
Return the balance of any set of move lines.
Surely this exists somewhere in account base, but I missed it.
"""
total = 0.0
if not ids:
return total
for line in self.read(
cr, uid, ids, ['debit', 'credit'], context=context):
total += (line['debit'] or 0.0) - (line['credit'] or 0.0)
return total
account_move_line()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: