odoo/addons/account/account_bank_statement.py

970 lines
55 KiB
Python

# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 fields, osv
from openerp.tools import float_is_zero
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from openerp.report import report_sxw
from openerp.tools import float_compare, float_round
import time
class account_bank_statement(osv.osv):
def create(self, cr, uid, vals, context=None):
if vals.get('name', '/') == '/':
journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context))
vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context)
if 'line_ids' in vals:
for idx, line in enumerate(vals['line_ids']):
line[2]['sequence'] = idx + 1
return super(account_bank_statement, self).create(cr, uid, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
for statement in self.browse(cr, uid, ids, context):
for idx, line in enumerate(statement.line_ids):
account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context)
return res
def _default_journal_id(self, cr, uid, context=None):
if context is None:
context = {}
journal_pool = self.pool.get('account.journal')
journal_type = context.get('journal_type', False)
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context)
if journal_type:
ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
if ids:
return ids[0]
return False
def _end_balance(self, cursor, user, ids, name, attr, context=None):
res = {}
for statement in self.browse(cursor, user, ids, context=context):
res[statement.id] = statement.balance_start
for line in statement.line_ids:
res[statement.id] += line.amount
return res
def _get_period(self, cr, uid, context=None):
periods = self.pool.get('account.period').find(cr, uid, context=context)
if periods:
return periods[0]
return False
def _compute_default_statement_name(self, cr, uid, journal_id, context=None):
context = dict(context or {})
obj_seq = self.pool.get('ir.sequence')
period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context)
context['fiscalyear_id'] = period.fiscalyear_id.id
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None)
return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context)
def _currency(self, cursor, user, ids, name, args, context=None):
res = {}
res_currency_obj = self.pool.get('res.currency')
res_users_obj = self.pool.get('res.users')
default_currency = res_users_obj.browse(cursor, user,
user, context=context).company_id.currency_id
for statement in self.browse(cursor, user, ids, context=context):
currency = statement.journal_id.currency
if not currency:
currency = default_currency
res[statement.id] = currency.id
currency_names = {}
for currency_id, currency_name in res_currency_obj.name_get(cursor,
user, [x for x in res.values()], context=context):
currency_names[currency_id] = currency_name
for statement_id in res.keys():
currency_id = res[statement_id]
res[statement_id] = (currency_id, currency_names[currency_id])
return res
def _get_statement(self, cr, uid, ids, context=None):
result = {}
for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
result[line.statement_id.id] = True
return result.keys()
def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None):
res = {}
for statement in self.browse(cr, uid, ids, context=context):
res[statement.id] = all([line.journal_entry_id.id or line.account_id.id for line in statement.line_ids])
return res
_order = "date desc, id desc"
_name = "account.bank.statement"
_description = "Bank Statement"
_inherit = ['mail.thread']
_columns = {
'name': fields.char(
'Reference', states={'draft': [('readonly', False)]},
readonly=True, # readonly for account_cash_statement
copy=False,
help='if you give the Name other then /, its created Accounting Entries Move '
'will be with same name as statement name. '
'This allows the statement entries to have the same references than the '
'statement itself'),
'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]},
select=True, copy=False),
'journal_id': fields.many2one('account.journal', 'Journal', required=True,
readonly=True, states={'draft':[('readonly',False)]}),
'period_id': fields.many2one('account.period', 'Period', required=True,
states={'confirm':[('readonly', True)]}),
'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'),
states={'confirm':[('readonly',True)]}),
'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'),
states={'confirm': [('readonly', True)]}, help="Computed using the cash control lines"),
'balance_end': fields.function(_end_balance,
store = {
'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10),
'account.bank.statement.line': (_get_statement, ['amount'], 10),
},
string="Computed Balance", help='Balance as calculated based on Opening Balance and transaction lines'),
'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
'line_ids': fields.one2many('account.bank.statement.line',
'statement_id', 'Statement lines',
states={'confirm':[('readonly', True)]}, copy=True),
'move_line_ids': fields.one2many('account.move.line', 'statement_id',
'Entry lines', states={'confirm':[('readonly',True)]}),
'state': fields.selection([('draft', 'New'),
('open','Open'), # used by cash statements
('confirm', 'Closed')],
'Status', required=True, readonly="1",
copy=False,
help='When new statement is created the status will be \'Draft\'.\n'
'And after getting confirmation from the bank it will be in \'Confirmed\' status.'),
'currency': fields.function(_currency, string='Currency',
type='many2one', relation='res.currency'),
'account_id': fields.related('journal_id', 'default_debit_account_id', type='many2one', relation='account.account', string='Account used in this journal', readonly=True, help='used in statement reconciliation domain, but shouldn\'t be used elswhere.'),
'cash_control': fields.related('journal_id', 'cash_control' , type='boolean', relation='account.journal',string='Cash control'),
'all_lines_reconciled': fields.function(_all_lines_reconciled, string='All lines reconciled', type='boolean'),
}
_defaults = {
'name': '/',
'date': fields.date.context_today,
'state': 'draft',
'journal_id': _default_journal_id,
'period_id': _get_period,
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=c),
}
def _check_company_id(self, cr, uid, ids, context=None):
for statement in self.browse(cr, uid, ids, context=context):
if statement.company_id.id != statement.period_id.company_id.id:
return False
return True
_constraints = [
(_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
]
def onchange_date(self, cr, uid, ids, date, company_id, context=None):
"""
Find the correct period to use for the given date and company_id, return it and set it in the context
"""
res = {}
period_pool = self.pool.get('account.period')
if context is None:
context = {}
ctx = context.copy()
ctx.update({'company_id': company_id})
pids = period_pool.find(cr, uid, dt=date, context=ctx)
if pids:
res.update({'period_id': pids[0]})
context = dict(context, period_id=pids[0])
return {
'value':res,
'context':context,
}
def button_dummy(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {}, context=context)
def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
"""Prepare the dict of values to create the move from a
statement line. This method may be overridden to implement custom
move generation (making sure to call super() to establish
a clean extension chain).
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param char st_line_number: will be used as the name of the generated account move
:return: dict of value to create() the account.move
"""
return {
'journal_id': st_line.statement_id.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'date': st_line.date,
'name': st_line_number,
'ref': st_line.ref,
}
def _get_counter_part_account(self, cr, uid, st_line, context=None):
"""Retrieve the account to use in the counterpart move.
:param browse_record st_line: account.bank.statement.line record to create the move from.
:return: int/long of the account.account to use as counterpart
"""
if st_line.amount >= 0:
return st_line.statement_id.journal_id.default_credit_account_id.id
return st_line.statement_id.journal_id.default_debit_account_id.id
def _get_counter_part_partner(self, cr, uid, st_line, context=None):
"""Retrieve the partner to use in the counterpart move.
:param browse_record st_line: account.bank.statement.line record to create the move from.
:return: int/long of the res.partner to use as counterpart
"""
return st_line.partner_id and st_line.partner_id.id or False
def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id, context=None):
"""Compute the args to build the dict of values to create the counter part move line from a
statement line by calling the _prepare_move_line_vals.
:param browse_record st_line: account.bank.statement.line record to create the move from.
:param int/long move_id: ID of the account.move to link the move line
:param float amount: amount of the move line
:param int/long company_currency_id: ID of currency of the concerned company
:return: dict of value to create() the bank account.move.line
"""
account_id = self._get_counter_part_account(cr, uid, st_line, context=context)
partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context)
debit = ((amount > 0) and amount) or 0.0
credit = ((amount < 0) and -amount) or 0.0
cur_id = False
amt_cur = False
if st_line.statement_id.currency.id != company_currency_id:
amt_cur = st_line.amount
cur_id = st_line.statement_id.currency.id
elif st_line.currency_id and st_line.amount_currency:
amt_cur = st_line.amount_currency
cur_id = st_line.currency_id.id
return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
amount_currency=amt_cur, currency_id=cur_id, account_id=account_id,
partner_id=partner_id, context=context)
def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
amount_currency=False, account_id=False, partner_id=False, context=None):
"""Prepare the dict of values to create the move line from a
statement line.
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param int/long move_id: ID of the account.move to link the move line
:param float debit: debit amount of the move line
:param float credit: credit amount of the move line
:param int/long currency_id: ID of currency of the move line to create
:param float amount_currency: amount of the debit/credit expressed in the currency_id
:param int/long account_id: ID of the account to use in the move line if different
from the statement line account ID
:param int/long partner_id: ID of the partner to put on the move line
:return: dict of value to create() the account.move.line
"""
acc_id = account_id or st_line.account_id.id
cur_id = currency_id or st_line.statement_id.currency.id
par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False)
return {
'name': st_line.name,
'date': st_line.date,
'ref': st_line.ref,
'move_id': move_id,
'partner_id': par_id,
'account_id': acc_id,
'credit': credit,
'debit': debit,
'statement_id': st_line.statement_id.id,
'journal_id': st_line.statement_id.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'currency_id': amount_currency and cur_id,
'amount_currency': amount_currency,
}
def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
st = self.browse(cr, uid, st_id, context=context)
if not ((abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001) or (abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001)):
raise osv.except_osv(_('Error!'),
_('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end))
return True
def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
def check_status_condition(self, cr, uid, state, journal_type='bank'):
return state in ('draft','open')
def button_confirm_bank(self, cr, uid, ids, context=None):
if context is None:
context = {}
for st in self.browse(cr, uid, ids, context=context):
j_type = st.journal_id.type
if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
continue
self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
if (not st.journal_id.default_credit_account_id) \
or (not st.journal_id.default_debit_account_id):
raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
for line in st.move_line_ids:
if line.state != 'valid':
raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
move_ids = []
for st_line in st.line_ids:
if not st_line.amount:
continue
if st_line.account_id and not st_line.journal_entry_id.id:
#make an account move as before
vals = {
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
'credit': st_line.amount > 0 and st_line.amount or 0.0,
'account_id': st_line.account_id.id,
'name': st_line.name
}
self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
elif not st_line.journal_entry_id.id:
raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
move_ids.append(st_line.journal_entry_id.id)
if move_ids:
self.pool.get('account.move').post(cr, uid, move_ids, context=context)
self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
self.link_bank_to_partner(cr, uid, ids, context=context)
return self.write(cr, uid, ids, {'state': 'confirm', 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
def button_cancel(self, cr, uid, ids, context=None):
bnk_st_line_ids = []
for st in self.browse(cr, uid, ids, context=context):
bnk_st_line_ids += [line.id for line in st.line_ids]
self.pool.get('account.bank.statement.line').cancel(cr, uid, bnk_st_line_ids, context=context)
return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
res = False
if journal_id:
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
if journal.with_last_closing_balance:
cr.execute('SELECT balance_end_real \
FROM account_bank_statement \
WHERE journal_id = %s AND NOT state = %s \
ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft'))
res = cr.fetchone()
return res and res[0] or 0.0
def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
if not journal_id:
return {}
balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context)
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
currency = journal.currency or journal.company_id.currency_id
res = {'balance_start': balance_start, 'company_id': journal.company_id.id, 'currency': currency.id}
if journal.type == 'cash':
res['cash_control'] = journal.cash_control
return {'value': res}
def unlink(self, cr, uid, ids, context=None):
statement_line_obj = self.pool['account.bank.statement.line']
for item in self.browse(cr, uid, ids, context=context):
if item.state != 'draft':
raise osv.except_osv(
_('Invalid Action!'),
_('In order to delete a bank statement, you must first cancel it to delete related journal items.')
)
# Explicitly unlink bank statement lines
# so it will check that the related journal entries have
# been deleted first
statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
def button_journal_entries(self, cr, uid, ids, context=None):
ctx = (context or {}).copy()
ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
return {
'name': _('Journal Items'),
'view_type':'form',
'view_mode':'tree',
'res_model':'account.move.line',
'view_id':False,
'type':'ir.actions.act_window',
'domain':[('statement_id','in',ids)],
'context':ctx,
}
def number_of_lines_reconciled(self, cr, uid, ids, context=None):
bsl_obj = self.pool.get('account.bank.statement.line')
return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
def link_bank_to_partner(self, cr, uid, ids, context=None):
for statement in self.browse(cr, uid, ids, context=context):
for st_line in statement.line_ids:
if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id.id != st_line.partner_id.id:
# Update the partner informations of the bank account, possibly overriding existing ones
bank_obj = self.pool.get('res.partner.bank')
bank_vals = bank_obj.onchange_partner_id(cr, uid, [st_line.bank_account_id.id], st_line.partner_id.id, context=context)['value']
bank_vals.update({'partner_id': st_line.partner_id.id})
bank_obj.write(cr, uid, [st_line.bank_account_id.id], bank_vals, context=context)
class account_bank_statement_line(osv.osv):
def create(self, cr, uid, vals, context=None):
if vals.get('amount_currency', 0) and not vals.get('amount', 0):
raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
def unlink(self, cr, uid, ids, context=None):
for item in self.browse(cr, uid, ids, context=context):
if item.journal_entry_id:
raise osv.except_osv(
_('Invalid Action!'),
_('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
)
return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
def cancel(self, cr, uid, ids, context=None):
account_move_obj = self.pool.get('account.move')
move_ids = []
for line in self.browse(cr, uid, ids, context=context):
if line.journal_entry_id:
move_ids.append(line.journal_entry_id.id)
for aml in line.journal_entry_id.line_id:
if aml.reconcile_id:
move_lines = [l.id for l in aml.reconcile_id.line_id]
move_lines.remove(aml.id)
self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
if len(move_lines) >= 2:
self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
if move_ids:
account_move_obj.button_cancel(cr, uid, move_ids, context=context)
account_move_obj.unlink(cr, uid, move_ids, context)
def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
""" Returns the data required to display a reconciliation, for each statement line id in ids """
ret = []
if excluded_ids is None:
excluded_ids = []
for st_line in self.browse(cr, uid, ids, context=context):
reconciliation_data = {}
if search_reconciliation_proposition:
reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
for mv_line in reconciliation_proposition:
excluded_ids.append(mv_line['id'])
reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
else:
reconciliation_data['reconciliation_proposition'] = []
st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
reconciliation_data['st_line'] = st_line
ret.append(reconciliation_data)
return ret
def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
""" Returns the data required by the bank statement reconciliation widget to display a statement line """
if context is None:
context = {}
statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
if st_line.amount_currency and st_line.currency_id:
amount = st_line.amount_currency
amount_currency = st_line.amount
amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
else:
amount = st_line.amount
amount_currency_str = ""
amount_str = amount > 0 and amount or -amount
amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
data = {
'id': st_line.id,
'ref': st_line.ref,
'note': st_line.note or "",
'name': st_line.name,
'date': st_line.date,
'amount': amount,
'amount_str': amount_str, # Amount in the statement line currency
'currency_id': st_line.currency_id.id or statement_currency.id,
'partner_id': st_line.partner_id.id,
'statement_id': st_line.statement_id.id,
'account_code': st_line.journal_id.default_debit_account_id.code,
'account_name': st_line.journal_id.default_debit_account_id.name,
'partner_name': st_line.partner_id.name,
'communication_partner_name': st_line.partner_name,
'amount_currency_str': amount_currency_str, # Amount in the statement currency
'has_no_partner': not st_line.partner_id.id,
}
if st_line.partner_id.id:
if amount > 0:
data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
else:
data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
return data
def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
if excluded_ids is None:
excluded_ids = []
domain = [('ref', '=', st_line.name),
('reconcile_id', '=', False),
('state', '=', 'valid'),
('account_id.reconcile', '=', True),
('id', 'not in', excluded_ids),
('partner_id', 'in', (False, st_line.partner_id.id))]
return domain
def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
""" Returns move lines that constitute the best guess to reconcile a statement line. """
mv_line_pool = self.pool.get('account.move.line')
# Look for structured communication
if st_line.name:
domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=2, context=context)
if match_id and len(match_id) == 1:
mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
mv_line = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, mv_line_br, target_currency=target_currency, target_date=st_line.date, context=context)[0]
mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
# If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
if (mv_line['partner_id']):
self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
mv_line['has_no_partner'] = False
return [mv_line]
# How to compare statement line amount and move lines amount
precision_digits = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
currency_id = st_line.currency_id.id or st_line.journal_id.currency.id
# NB : amount can't be == 0 ; so float precision is not an issue for amount > 0 or amount < 0
amount = st_line.amount_currency or st_line.amount
domain = [('reconcile_partial_id', '=', False)]
if currency_id:
domain += [('currency_id', '=', currency_id)]
sign = 1 # correct the fact that st_line.amount is signed and debit/credit is not
amount_field = 'debit'
if currency_id == False:
if amount < 0:
amount_field = 'credit'
sign = -1
else:
amount_field = 'amount_currency'
# Look for a matching amount
domain_exact_amount = domain + [(amount_field, '=', float_round(sign * amount, precision_digits=precision_digits))]
domain_exact_amount_ref = domain_exact_amount + [('ref', '=', st_line.ref)]
match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=2, additional_domain=domain_exact_amount_ref)
if not match_id:
match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=2, additional_domain=domain_exact_amount)
if match_id and len(match_id) == 1:
return match_id
if not st_line.partner_id.id:
return []
# Look for a set of move line whose amount is <= to the line's amount
if amount > 0: # Make sure we can't mix receivable and payable
domain += [('account_id.type', '=', 'receivable')]
else:
domain += [('account_id.type', '=', 'payable')]
if amount_field == 'amount_currency' and amount < 0:
domain += [(amount_field, '<', 0), (amount_field, '>=', (sign * amount))]
else:
domain += [(amount_field, '>', 0), (amount_field, '<=', (sign * amount))]
mv_lines = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, limit=5, additional_domain=domain, context=context)
ret = []
total = 0
for line in mv_lines:
total += abs(line['debit'] - line['credit'])
if float_compare(total, abs(amount), precision_digits=precision_digits) != 1:
ret.append(line)
else:
break
return ret
def get_move_lines_for_reconciliation_by_statement_line_id(self, cr, uid, st_line_id, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None):
""" Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
if excluded_ids is None:
excluded_ids = []
if additional_domain is None:
additional_domain = []
st_line = self.browse(cr, uid, st_line_id, context=context)
return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
if excluded_ids is None:
excluded_ids = []
if additional_domain is None:
additional_domain = []
# Make domain
domain = additional_domain + [
('reconcile_id', '=', False),
('state', '=', 'valid'),
('account_id.reconcile', '=', True)
]
if st_line.partner_id.id:
domain += [('partner_id', '=', st_line.partner_id.id)]
if excluded_ids:
domain.append(('id', 'not in', excluded_ids))
if str:
domain += [
'|', ('move_id.name', 'ilike', str),
'|', ('move_id.ref', 'ilike', str),
('date_maturity', 'like', str),
]
if not st_line.partner_id.id:
domain.insert(-1, '|', )
domain.append(('partner_id.name', 'ilike', str))
if str != '/':
domain.insert(-1, '|', )
domain.append(('name', 'ilike', str))
return domain
def get_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None):
""" Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
:param st_line: the browse record of the statement line
:param integers list excluded_ids: ids of move lines that should not be fetched
:param boolean count: just return the number of records
:param tuples list additional_domain: additional domain restrictions
"""
mv_line_pool = self.pool.get('account.move.line')
domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
# Get move lines ; in case of a partial reconciliation, only keep one line (the first whose amount is greater than
# the residual amount because it is presumably the invoice, which is the relevant item in this situation)
filtered_lines = []
reconcile_partial_ids = []
actual_offset = offset
while True:
line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
make_one_more_loop = False
for line in lines:
if line.reconcile_partial_id and \
(line.reconcile_partial_id.id in reconcile_partial_ids or \
abs(line.debit - line.credit) < abs(line.amount_residual)):
#if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
#in order to get the right number of items in the pager
make_one_more_loop = True
continue
filtered_lines.append(line)
if line.reconcile_partial_id:
reconcile_partial_ids.append(line.reconcile_partial_id.id)
if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
break
actual_offset = actual_offset + limit
lines = limit and filtered_lines[:limit] or filtered_lines
# Either return number of lines
if count:
return len(lines)
# Or return list of dicts representing the formatted move lines
else:
target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
mv_lines = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, lines, target_currency=target_currency, target_date=st_line.date, context=context)
has_no_partner = not bool(st_line.partner_id.id)
for line in mv_lines:
line['has_no_partner'] = has_no_partner
return mv_lines
def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
if currency_diff < 0:
account_id = st_line.company_id.expense_currency_exchange_account_id.id
if not account_id:
raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
else:
account_id = st_line.company_id.income_currency_exchange_account_id.id
if not account_id:
raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
return {
'move_id': move_id,
'name': _('change') + ': ' + (st_line.name or '/'),
'period_id': st_line.statement_id.period_id.id,
'journal_id': st_line.journal_id.id,
'partner_id': st_line.partner_id.id,
'company_id': st_line.company_id.id,
'statement_id': st_line.statement_id.id,
'debit': currency_diff < 0 and -currency_diff or 0,
'credit': currency_diff > 0 and currency_diff or 0,
'amount_currency': 0.0,
'date': st_line.date,
'account_id': account_id
}
def _get_exchange_lines(self, cr, uid, st_line, mv_line, currency_diff, currency_id, move_id, context=None):
'''
Prepare the two lines in company currency due to currency rate difference.
:param line: browse record of the voucher.line for which we want to create currency rate difference accounting
entries
:param move_id: Account move wher the move lines will be.
:param currency_diff: Amount to be posted.
:param company_currency: id of currency of the company to which the voucher belong
:param current_currency: id of currency of the voucher
:return: the account move line and its counterpart to create, depicted as mapping between fieldname and value
:rtype: tuple of dict
'''
if currency_diff > 0:
exchange_account_id = st_line.company_id.expense_currency_exchange_account_id.id
else:
exchange_account_id = st_line.company_id.income_currency_exchange_account_id.id
# Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
# the receivable/payable account may have a secondary currency, which render this field mandatory
if mv_line.account_id.currency_id:
account_currency_id = mv_line.account_id.currency_id.id
else:
account_currency_id = st_line.company_id.currency_id.id != currency_id and currency_id or False
move_line = {
'journal_id': st_line.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'name': _('change') + ': ' + (st_line.name or '/'),
'account_id': mv_line.account_id.id,
'move_id': move_id,
'partner_id': st_line.partner_id.id,
'currency_id': account_currency_id,
'amount_currency': 0.0,
'quantity': 1,
'credit': currency_diff > 0 and currency_diff or 0.0,
'debit': currency_diff < 0 and -currency_diff or 0.0,
'date': st_line.date,
'counterpart_move_line_id': mv_line.id,
}
move_line_counterpart = {
'journal_id': st_line.journal_id.id,
'period_id': st_line.statement_id.period_id.id,
'name': _('change') + ': ' + (st_line.name or '/'),
'account_id': exchange_account_id,
'move_id': move_id,
'amount_currency': 0.0,
'partner_id': st_line.partner_id.id,
'currency_id': account_currency_id,
'quantity': 1,
'debit': currency_diff > 0 and currency_diff or 0.0,
'credit': currency_diff < 0 and -currency_diff or 0.0,
'date': st_line.date,
}
return (move_line, move_line_counterpart)
def process_reconciliations(self, cr, uid, data, context=None):
for datum in data:
self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
""" Creates a move line for each item of mv_line_dicts and for the statement line. Reconcile a new move line with its counterpart_move_line_id if specified. Finally, mark the statement line as reconciled by putting the newly created move id in the column journal_entry_id.
:param int id: id of the bank statement line
:param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
"""
if context is None:
context = {}
st_line = self.browse(cr, uid, id, context=context)
company_currency = st_line.journal_id.company_id.currency_id
statement_currency = st_line.journal_id.currency or company_currency
bs_obj = self.pool.get('account.bank.statement')
am_obj = self.pool.get('account.move')
aml_obj = self.pool.get('account.move.line')
currency_obj = self.pool.get('res.currency')
# Checks
if st_line.journal_entry_id.id:
raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
for mv_line_dict in mv_line_dicts:
for field in ['debit', 'credit', 'amount_currency']:
if field not in mv_line_dict:
mv_line_dict[field] = 0.0
if mv_line_dict.get('counterpart_move_line_id'):
mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
if mv_line.reconcile_id:
raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
# Create the move
move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
move_id = am_obj.create(cr, uid, move_vals, context=context)
# Create the move line for the statement line
if st_line.statement_id.currency.id != company_currency.id:
if st_line.currency_id == company_currency:
amount = st_line.amount_currency
else:
ctx = context.copy()
ctx['date'] = st_line.date
amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
else:
amount = st_line.amount
bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
aml_obj.create(cr, uid, bank_st_move_vals, context=context)
# Complete the dicts
st_line_currency = st_line.currency_id or statement_currency
st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
to_create = []
for mv_line_dict in mv_line_dicts:
if mv_line_dict.get('is_tax_line'):
continue
mv_line_dict['ref'] = move_name
mv_line_dict['move_id'] = move_id
mv_line_dict['period_id'] = st_line.statement_id.period_id.id
mv_line_dict['journal_id'] = st_line.journal_id.id
mv_line_dict['company_id'] = st_line.company_id.id
mv_line_dict['statement_id'] = st_line.statement_id.id
if mv_line_dict.get('counterpart_move_line_id'):
mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
mv_line_dict['account_id'] = mv_line.account_id.id
if st_line_currency.id != company_currency.id:
ctx = context.copy()
ctx['date'] = st_line.date
mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
mv_line_dict['currency_id'] = st_line_currency.id
if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
elif st_line.currency_id and st_line_currency_rate:
debit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=ctx)
credit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=ctx)
else:
debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
if mv_line_dict.get('counterpart_move_line_id'):
#post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
ctx['date'] = mv_line.date
if mv_line.currency_id.id == mv_line_dict['currency_id'] \
and float_is_zero(abs(mv_line.amount_currency) - abs(mv_line_dict['amount_currency']), precision_rounding=mv_line.currency_id.rounding):
debit_at_old_rate = mv_line.credit
credit_at_old_rate = mv_line.debit
else:
debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
mv_line_dict['credit'] = credit_at_old_rate
mv_line_dict['debit'] = debit_at_old_rate
if debit_at_old_rate - debit_at_current_rate:
currency_diff = debit_at_current_rate - debit_at_old_rate
to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
if credit_at_old_rate - credit_at_current_rate:
currency_diff = credit_at_current_rate - credit_at_old_rate
to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
if mv_line.currency_id and mv_line_dict['currency_id'] == mv_line.currency_id.id:
amount_unreconciled = mv_line.amount_residual_currency
else:
amount_unreconciled = currency_obj.compute(cr, uid, company_currency.id, mv_line_dict['currency_id'] , mv_line.amount_residual, context=ctx)
if float_is_zero(mv_line_dict['amount_currency'] + amount_unreconciled, precision_rounding=mv_line.currency_id.rounding):
amount = mv_line_dict['debit'] or mv_line_dict['credit']
sign = -1 if mv_line_dict['debit'] else 1
currency_rate_difference = sign * (mv_line.amount_residual - amount)
if not company_currency.is_zero(currency_rate_difference):
exchange_lines = self._get_exchange_lines(cr, uid, st_line, mv_line, currency_rate_difference, mv_line_dict['currency_id'], move_id, context=context)
for exchange_line in exchange_lines:
to_create.append(exchange_line)
else:
mv_line_dict['debit'] = debit_at_current_rate
mv_line_dict['credit'] = credit_at_current_rate
elif statement_currency.id != company_currency.id:
#statement is in foreign currency but the transaction is in company currency
prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
to_create.append(mv_line_dict)
# If the reconciliation is performed in another currency than the company currency, the amounts are converted to get the right debit/credit.
# If there is more than 1 debit and 1 credit, this can induce a rounding error, which we put in the foreign exchane gain/loss account.
if st_line_currency.id != company_currency.id:
diff_amount = bank_st_move_vals['debit'] - bank_st_move_vals['credit'] \
+ sum(aml['debit'] for aml in to_create) - sum(aml['credit'] for aml in to_create)
if not company_currency.is_zero(diff_amount):
diff_aml = self.get_currency_rate_line(cr, uid, st_line, diff_amount, move_id, context=context)
diff_aml['name'] = _('Rounding error from currency conversion')
to_create.append(diff_aml)
# Create move lines
move_line_pairs_to_reconcile = []
for mv_line_dict in to_create:
counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
if mv_line_dict.get('counterpart_move_line_id'):
counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
del mv_line_dict['counterpart_move_line_id']
new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
if counterpart_move_line_id != None:
move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
# Reconcile
for pair in move_line_pairs_to_reconcile:
aml_obj.reconcile_partial(cr, uid, pair, context=context)
# Mark the statement line as reconciled
self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
# FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
# return [('journal_entry_id', '=', False)]
# Unfortunately, that spawns a "no access rights" error ; it shouldn't.
def _needaction_domain_get(self, cr, uid, context=None):
user = self.pool.get("res.users").browse(cr, uid, uid)
return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False), ('account_id', '=', False)]
_order = "statement_id desc, sequence"
_name = "account.bank.statement.line"
_description = "Bank Statement Line"
_inherit = ['ir.needaction_mixin']
_columns = {
'name': fields.char('Communication', required=True),
'date': fields.date('Date', required=True),
'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
'partner_id': fields.many2one('res.partner', 'Partner'),
'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"),
'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
'partner_name': fields.char('Partner Name', help="This field is used to record the third party name when importing bank statement in electronic format, when the partner doesn't exist yet in the database (or cannot be found)."),
'ref': fields.char('Reference'),
'note': fields.text('Notes'),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
}
_defaults = {
'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line', context=context),
'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
}
class account_statement_operation_template(osv.osv):
_name = "account.statement.operation.template"
_description = "Preset for the lines that can be created in a bank statement reconciliation"
_columns = {
'name': fields.char('Button Label', required=True),
'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type', 'not in', ('view', 'closed', 'consolidation'))]),
'label': fields.char('Label'),
'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
'Amount type', required=True),
'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), help="The amount will count as a debit if it is negative, as a credit if it is positive (except if amount type is 'Percentage of open balance').", required=True),
'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use', 'in', ['purchase', 'all']), ('parent_id', '=', False)]),
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]),
}
_defaults = {
'amount_type': 'percentage_of_balance',
'amount': 100.0
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: