1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 import openerp.addons.decimal_precision as dp
25 from openerp.report import report_sxw
26 from openerp.tools import float_compare, float_round
30 class account_bank_statement(osv.osv):
31 def create(self, cr, uid, vals, context=None):
32 if vals.get('name', '/') == '/':
33 journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context))
34 vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context)
35 if 'line_ids' in vals:
36 for idx, line in enumerate(vals['line_ids']):
37 line[2]['sequence'] = idx + 1
38 return super(account_bank_statement, self).create(cr, uid, vals, context=context)
40 def write(self, cr, uid, ids, vals, context=None):
41 res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
42 account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
43 for statement in self.browse(cr, uid, ids, context):
44 for idx, line in enumerate(statement.line_ids):
45 account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context)
48 def _default_journal_id(self, cr, uid, context=None):
51 journal_pool = self.pool.get('account.journal')
52 journal_type = context.get('journal_type', False)
53 company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context)
55 ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
60 def _end_balance(self, cursor, user, ids, name, attr, context=None):
62 for statement in self.browse(cursor, user, ids, context=context):
63 res[statement.id] = statement.balance_start
64 for line in statement.line_ids:
65 res[statement.id] += line.amount
68 def _get_period(self, cr, uid, context=None):
69 periods = self.pool.get('account.period').find(cr, uid, context=context)
74 def _compute_default_statement_name(self, cr, uid, journal_id, context=None):
75 context = dict(context or {})
76 obj_seq = self.pool.get('ir.sequence')
77 period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context)
78 context['fiscalyear_id'] = period.fiscalyear_id.id
79 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None)
80 return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context)
82 def _currency(self, cursor, user, ids, name, args, context=None):
84 res_currency_obj = self.pool.get('res.currency')
85 res_users_obj = self.pool.get('res.users')
86 default_currency = res_users_obj.browse(cursor, user,
87 user, context=context).company_id.currency_id
88 for statement in self.browse(cursor, user, ids, context=context):
89 currency = statement.journal_id.currency
91 currency = default_currency
92 res[statement.id] = currency.id
94 for currency_id, currency_name in res_currency_obj.name_get(cursor,
95 user, [x for x in res.values()], context=context):
96 currency_names[currency_id] = currency_name
97 for statement_id in res.keys():
98 currency_id = res[statement_id]
99 res[statement_id] = (currency_id, currency_names[currency_id])
102 def _get_statement(self, cr, uid, ids, context=None):
104 for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
105 result[line.statement_id.id] = True
108 def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None):
110 for statement in self.browse(cr, uid, ids, context=context):
111 res[statement.id] = all([line.journal_entry_id.id for line in statement.line_ids])
114 _order = "date desc, id desc"
115 _name = "account.bank.statement"
116 _description = "Bank Statement"
117 _inherit = ['mail.thread']
120 'Reference', states={'draft': [('readonly', False)]},
121 readonly=True, # readonly for account_cash_statement
123 help='if you give the Name other then /, its created Accounting Entries Move '
124 'will be with same name as statement name. '
125 'This allows the statement entries to have the same references than the '
127 'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]},
128 select=True, copy=False),
129 'journal_id': fields.many2one('account.journal', 'Journal', required=True,
130 readonly=True, states={'draft':[('readonly',False)]}),
131 'period_id': fields.many2one('account.period', 'Period', required=True,
132 states={'confirm':[('readonly', True)]}),
133 'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'),
134 states={'confirm':[('readonly',True)]}),
135 'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'),
136 states={'confirm': [('readonly', True)]}, help="Computed using the cash control lines"),
137 'balance_end': fields.function(_end_balance,
139 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10),
140 'account.bank.statement.line': (_get_statement, ['amount'], 10),
142 string="Computed Balance", help='Balance as calculated based on Opening Balance and transaction lines'),
143 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
144 'line_ids': fields.one2many('account.bank.statement.line',
145 'statement_id', 'Statement lines',
146 states={'confirm':[('readonly', True)]}, copy=True),
147 'move_line_ids': fields.one2many('account.move.line', 'statement_id',
148 'Entry lines', states={'confirm':[('readonly',True)]}),
149 'state': fields.selection([('draft', 'New'),
150 ('open','Open'), # used by cash statements
151 ('confirm', 'Closed')],
152 'Status', required=True, readonly="1",
154 help='When new statement is created the status will be \'Draft\'.\n'
155 'And after getting confirmation from the bank it will be in \'Confirmed\' status.'),
156 'currency': fields.function(_currency, string='Currency',
157 type='many2one', relation='res.currency'),
158 '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.'),
159 'cash_control': fields.related('journal_id', 'cash_control' , type='boolean', relation='account.journal',string='Cash control'),
160 'all_lines_reconciled': fields.function(_all_lines_reconciled, string='All lines reconciled', type='boolean'),
165 'date': fields.date.context_today,
167 'journal_id': _default_journal_id,
168 'period_id': _get_period,
169 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=c),
172 def _check_company_id(self, cr, uid, ids, context=None):
173 for statement in self.browse(cr, uid, ids, context=context):
174 if statement.company_id.id != statement.period_id.company_id.id:
179 (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
182 def onchange_date(self, cr, uid, ids, date, company_id, context=None):
184 Find the correct period to use for the given date and company_id, return it and set it in the context
187 period_pool = self.pool.get('account.period')
192 ctx.update({'company_id': company_id})
193 pids = period_pool.find(cr, uid, dt=date, context=ctx)
195 res.update({'period_id': pids[0]})
196 context = dict(context, period_id=pids[0])
203 def button_dummy(self, cr, uid, ids, context=None):
204 return self.write(cr, uid, ids, {}, context=context)
206 def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
207 """Prepare the dict of values to create the move from a
208 statement line. This method may be overridden to implement custom
209 move generation (making sure to call super() to establish
210 a clean extension chain).
212 :param browse_record st_line: account.bank.statement.line record to
213 create the move from.
214 :param char st_line_number: will be used as the name of the generated account move
215 :return: dict of value to create() the account.move
218 'journal_id': st_line.statement_id.journal_id.id,
219 'period_id': st_line.statement_id.period_id.id,
220 'date': st_line.date,
221 'name': st_line_number,
225 def _get_counter_part_account(self, cr, uid, st_line, context=None):
226 """Retrieve the account to use in the counterpart move.
228 :param browse_record st_line: account.bank.statement.line record to create the move from.
229 :return: int/long of the account.account to use as counterpart
231 if st_line.amount >= 0:
232 return st_line.statement_id.journal_id.default_credit_account_id.id
233 return st_line.statement_id.journal_id.default_debit_account_id.id
235 def _get_counter_part_partner(self, cr, uid, st_line, context=None):
236 """Retrieve the partner to use in the counterpart move.
238 :param browse_record st_line: account.bank.statement.line record to create the move from.
239 :return: int/long of the res.partner to use as counterpart
241 return st_line.partner_id and st_line.partner_id.id or False
243 def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id, context=None):
244 """Compute the args to build the dict of values to create the counter part move line from a
245 statement line by calling the _prepare_move_line_vals.
247 :param browse_record st_line: account.bank.statement.line record to create the move from.
248 :param int/long move_id: ID of the account.move to link the move line
249 :param float amount: amount of the move line
250 :param int/long company_currency_id: ID of currency of the concerned company
251 :return: dict of value to create() the bank account.move.line
253 account_id = self._get_counter_part_account(cr, uid, st_line, context=context)
254 partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context)
255 debit = ((amount > 0) and amount) or 0.0
256 credit = ((amount < 0) and -amount) or 0.0
259 if st_line.statement_id.currency.id != company_currency_id:
260 amt_cur = st_line.amount
261 cur_id = st_line.statement_id.currency.id
262 elif st_line.currency_id and st_line.amount_currency:
263 amt_cur = st_line.amount_currency
264 cur_id = st_line.currency_id.id
265 return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
266 amount_currency=amt_cur, currency_id=cur_id, account_id=account_id,
267 partner_id=partner_id, context=context)
269 def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
270 amount_currency=False, account_id=False, partner_id=False, context=None):
271 """Prepare the dict of values to create the move line from a
274 :param browse_record st_line: account.bank.statement.line record to
275 create the move from.
276 :param int/long move_id: ID of the account.move to link the move line
277 :param float debit: debit amount of the move line
278 :param float credit: credit amount of the move line
279 :param int/long currency_id: ID of currency of the move line to create
280 :param float amount_currency: amount of the debit/credit expressed in the currency_id
281 :param int/long account_id: ID of the account to use in the move line if different
282 from the statement line account ID
283 :param int/long partner_id: ID of the partner to put on the move line
284 :return: dict of value to create() the account.move.line
286 acc_id = account_id or st_line.account_id.id
287 cur_id = currency_id or st_line.statement_id.currency.id
288 par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False)
290 'name': st_line.name,
291 'date': st_line.date,
294 'partner_id': par_id,
295 'account_id': acc_id,
298 'statement_id': st_line.statement_id.id,
299 'journal_id': st_line.statement_id.journal_id.id,
300 'period_id': st_line.statement_id.period_id.id,
301 'currency_id': amount_currency and cur_id,
302 'amount_currency': amount_currency,
305 def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
306 st = self.browse(cr, uid, st_id, context=context)
307 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)):
308 raise osv.except_osv(_('Error!'),
309 _('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end))
312 def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
313 return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
315 def check_status_condition(self, cr, uid, state, journal_type='bank'):
316 return state in ('draft','open')
318 def button_confirm_bank(self, cr, uid, ids, context=None):
322 for st in self.browse(cr, uid, ids, context=context):
323 j_type = st.journal_id.type
324 if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
327 self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
328 if (not st.journal_id.default_credit_account_id) \
329 or (not st.journal_id.default_debit_account_id):
330 raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
331 for line in st.move_line_ids:
332 if line.state != 'valid':
333 raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
335 for st_line in st.line_ids:
336 if not st_line.amount:
338 if st_line.account_id and not st_line.journal_entry_id.id:
339 #make an account move as before
341 'debit': st_line.amount < 0 and -st_line.amount or 0.0,
342 'credit': st_line.amount > 0 and st_line.amount or 0.0,
343 'account_id': st_line.account_id.id,
346 self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
347 elif not st_line.journal_entry_id.id:
348 raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
349 move_ids.append(st_line.journal_entry_id.id)
351 self.pool.get('account.move').post(cr, uid, move_ids, context=context)
352 self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
353 self.link_bank_to_partner(cr, uid, ids, context=context)
354 return self.write(cr, uid, ids, {'state': 'confirm', 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
356 def button_cancel(self, cr, uid, ids, context=None):
358 for st in self.browse(cr, uid, ids, context=context):
359 bnk_st_line_ids += [line.id for line in st.line_ids]
360 self.pool.get('account.bank.statement.line').cancel(cr, uid, bnk_st_line_ids, context=context)
361 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
363 def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
366 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
367 if journal.with_last_closing_balance:
368 cr.execute('SELECT balance_end_real \
369 FROM account_bank_statement \
370 WHERE journal_id = %s AND NOT state = %s \
371 ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft'))
373 return res and res[0] or 0.0
375 def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
378 balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context)
379 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
380 currency = journal.currency or journal.company_id.currency_id
381 res = {'balance_start': balance_start, 'company_id': journal.company_id.id, 'currency': currency.id}
382 if journal.type == 'cash':
383 res['cash_control'] = journal.cash_control
384 return {'value': res}
386 def unlink(self, cr, uid, ids, context=None):
387 statement_line_obj = self.pool['account.bank.statement.line']
388 for item in self.browse(cr, uid, ids, context=context):
389 if item.state != 'draft':
390 raise osv.except_osv(
391 _('Invalid Action!'),
392 _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
394 # Explicitly unlink bank statement lines
395 # so it will check that the related journal entries have
397 statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
398 return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
400 def button_journal_entries(self, cr, uid, ids, context=None):
401 ctx = (context or {}).copy()
402 ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
404 'name': _('Journal Items'),
407 'res_model':'account.move.line',
409 'type':'ir.actions.act_window',
410 'domain':[('statement_id','in',ids)],
414 def number_of_lines_reconciled(self, cr, uid, ids, context=None):
415 bsl_obj = self.pool.get('account.bank.statement.line')
416 return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
418 def link_bank_to_partner(self, cr, uid, ids, context=None):
419 for statement in self.browse(cr, uid, ids, context=context):
420 for st_line in statement.line_ids:
421 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:
422 self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context)
424 class account_bank_statement_line(osv.osv):
426 def create(self, cr, uid, vals, context=None):
427 if vals.get('amount_currency', 0) and not vals.get('amount', 0):
428 raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
429 return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
431 def unlink(self, cr, uid, ids, context=None):
432 for item in self.browse(cr, uid, ids, context=context):
433 if item.journal_entry_id:
434 raise osv.except_osv(
435 _('Invalid Action!'),
436 _('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
438 return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
440 def cancel(self, cr, uid, ids, context=None):
441 account_move_obj = self.pool.get('account.move')
443 for line in self.browse(cr, uid, ids, context=context):
444 if line.journal_entry_id:
445 move_ids.append(line.journal_entry_id.id)
446 for aml in line.journal_entry_id.line_id:
448 move_lines = [l.id for l in aml.reconcile_id.line_id]
449 move_lines.remove(aml.id)
450 self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
451 if len(move_lines) >= 2:
452 self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
454 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
455 account_move_obj.unlink(cr, uid, move_ids, context)
457 def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
458 """ Returns the data required to display a reconciliation, for each statement line id in ids """
460 if excluded_ids is None:
463 for st_line in self.browse(cr, uid, ids, context=context):
464 reconciliation_data = {}
465 if search_reconciliation_proposition:
466 reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
467 for mv_line in reconciliation_proposition:
468 excluded_ids.append(mv_line['id'])
469 reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
471 reconciliation_data['reconciliation_proposition'] = []
472 st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
473 reconciliation_data['st_line'] = st_line
474 ret.append(reconciliation_data)
478 def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
479 """ Returns the data required by the bank statement reconciliation widget to display a statement line """
482 statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
483 rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
485 if st_line.amount_currency and st_line.currency_id:
486 amount = st_line.amount_currency
487 amount_currency = st_line.amount
488 amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
489 amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
491 amount = st_line.amount
492 amount_currency_str = ""
493 amount_str = amount > 0 and amount or -amount
494 amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
499 'note': st_line.note or "",
500 'name': st_line.name,
501 'date': st_line.date,
503 'amount_str': amount_str, # Amount in the statement line currency
504 'currency_id': st_line.currency_id.id or statement_currency.id,
505 'partner_id': st_line.partner_id.id,
506 'statement_id': st_line.statement_id.id,
507 'account_code': st_line.journal_id.default_debit_account_id.code,
508 'account_name': st_line.journal_id.default_debit_account_id.name,
509 'partner_name': st_line.partner_id.name,
510 'communication_partner_name': st_line.partner_name,
511 'amount_currency_str': amount_currency_str, # Amount in the statement currency
512 'has_no_partner': not st_line.partner_id.id,
514 if st_line.partner_id.id:
516 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
518 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
522 def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
523 if excluded_ids is None:
525 domain = [('ref', '=', st_line.name),
526 ('reconcile_id', '=', False),
527 ('state', '=', 'valid'),
528 ('account_id.reconcile', '=', True),
529 ('id', 'not in', excluded_ids)]
532 def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
533 """ Returns move lines that constitute the best guess to reconcile a statement line. """
534 mv_line_pool = self.pool.get('account.move.line')
536 # Look for structured communication
538 domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
539 match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
541 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
542 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
543 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]
544 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
545 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
546 if (mv_line['partner_id']):
547 self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
548 mv_line['has_no_partner'] = False
551 # How to compare statement line amount and move lines amount
552 precision_digits = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
553 currency_id = st_line.currency_id.id or st_line.journal_id.currency.id
554 # NB : amount can't be == 0 ; so float precision is not an issue for amount > 0 or amount < 0
555 amount = st_line.amount_currency or st_line.amount
556 domain = [('reconcile_partial_id', '=', False)]
558 domain += [('currency_id', '=', currency_id)]
559 sign = 1 # correct the fact that st_line.amount is signed and debit/credit is not
560 amount_field = 'debit'
561 if currency_id == False:
563 amount_field = 'credit'
566 amount_field = 'amount_currency'
568 # Look for a matching amount
569 domain_exact_amount = domain + [(amount_field, '=', float_round(sign * amount, precision_digits=precision_digits))]
570 match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=domain_exact_amount)
574 if not st_line.partner_id.id:
577 # Look for a set of move line whose amount is <= to the line's amount
578 if amount > 0: # Make sure we can't mix receivable and payable
579 domain += [('account_id.type', '=', 'receivable')]
581 domain += [('account_id.type', '=', 'payable')]
582 if amount_field == 'amount_currency' and amount < 0:
583 domain += [(amount_field, '<', 0), (amount_field, '>', (sign * amount))]
585 domain += [(amount_field, '>', 0), (amount_field, '<', (sign * amount))]
586 mv_lines = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, limit=5, additional_domain=domain)
589 for line in mv_lines:
590 total += abs(line['debit'] - line['credit'])
591 if float_compare(total, abs(amount), precision_digits=precision_digits) != 1:
597 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):
598 """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
599 if excluded_ids is None:
601 if additional_domain is None:
602 additional_domain = []
603 st_line = self.browse(cr, uid, st_line_id, context=context)
604 return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
606 def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
607 if excluded_ids is None:
609 if additional_domain is None:
610 additional_domain = []
612 domain = additional_domain + [
613 ('reconcile_id', '=', False),
614 ('state', '=', 'valid'),
615 ('account_id.reconcile', '=', True)
617 if st_line.partner_id.id:
618 domain += [('partner_id', '=', st_line.partner_id.id)]
620 domain.append(('id', 'not in', excluded_ids))
623 '|', ('move_id.name', 'ilike', str),
624 '|', ('move_id.ref', 'ilike', str),
625 ('date_maturity', 'like', str),
627 if not st_line.partner_id.id:
628 domain.insert(-1, '|', )
629 domain.append(('partner_id.name', 'ilike', str))
631 domain.insert(-1, '|', )
632 domain.append(('name', 'ilike', str))
635 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):
636 """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
638 :param st_line: the browse record of the statement line
639 :param integers list excluded_ids: ids of move lines that should not be fetched
640 :param boolean count: just return the number of records
641 :param tuples list additional_domain: additional domain restrictions
643 mv_line_pool = self.pool.get('account.move.line')
644 domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
646 # Get move lines ; in case of a partial reconciliation, only consider one line
648 reconcile_partial_ids = []
649 actual_offset = offset
651 line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
652 lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
653 make_one_more_loop = False
655 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
656 #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
657 #in order to get the right number of items in the pager
658 make_one_more_loop = True
660 filtered_lines.append(line)
661 if line.reconcile_partial_id:
662 reconcile_partial_ids.append(line.reconcile_partial_id.id)
664 if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
666 actual_offset = actual_offset + limit
667 lines = limit and filtered_lines[:limit] or filtered_lines
669 # Either return number of lines
673 # Or return list of dicts representing the formatted move lines
675 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
676 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)
677 has_no_partner = not bool(st_line.partner_id.id)
678 for line in mv_lines:
679 line['has_no_partner'] = has_no_partner
682 def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
683 if currency_diff < 0:
684 account_id = st_line.company_id.expense_currency_exchange_account_id.id
686 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."))
688 account_id = st_line.company_id.income_currency_exchange_account_id.id
690 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."))
693 'name': _('change') + ': ' + (st_line.name or '/'),
694 'period_id': st_line.statement_id.period_id.id,
695 'journal_id': st_line.journal_id.id,
696 'partner_id': st_line.partner_id.id,
697 'company_id': st_line.company_id.id,
698 'statement_id': st_line.statement_id.id,
699 'debit': currency_diff < 0 and -currency_diff or 0,
700 'credit': currency_diff > 0 and currency_diff or 0,
701 'amount_currency': 0.0,
702 'date': st_line.date,
703 'account_id': account_id
706 def process_reconciliations(self, cr, uid, data, context=None):
708 self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
710 def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
711 """ 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.
713 :param int id: id of the bank statement line
714 :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
718 st_line = self.browse(cr, uid, id, context=context)
719 company_currency = st_line.journal_id.company_id.currency_id
720 statement_currency = st_line.journal_id.currency or company_currency
721 bs_obj = self.pool.get('account.bank.statement')
722 am_obj = self.pool.get('account.move')
723 aml_obj = self.pool.get('account.move.line')
724 currency_obj = self.pool.get('res.currency')
727 if st_line.journal_entry_id.id:
728 raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
729 for mv_line_dict in mv_line_dicts:
730 for field in ['debit', 'credit', 'amount_currency']:
731 if field not in mv_line_dict:
732 mv_line_dict[field] = 0.0
733 if mv_line_dict.get('counterpart_move_line_id'):
734 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
735 if mv_line.reconcile_id:
736 raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
739 move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
740 move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
741 move_id = am_obj.create(cr, uid, move_vals, context=context)
743 # Create the move line for the statement line
744 if st_line.statement_id.currency.id != company_currency.id:
745 if st_line.currency_id == company_currency:
746 amount = st_line.amount_currency
749 ctx['date'] = st_line.date
750 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
752 amount = st_line.amount
753 bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
754 aml_obj.create(cr, uid, bank_st_move_vals, context=context)
756 st_line_currency = st_line.currency_id or statement_currency
757 st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
759 for mv_line_dict in mv_line_dicts:
760 if mv_line_dict.get('is_tax_line'):
762 mv_line_dict['ref'] = move_name
763 mv_line_dict['move_id'] = move_id
764 mv_line_dict['period_id'] = st_line.statement_id.period_id.id
765 mv_line_dict['journal_id'] = st_line.journal_id.id
766 mv_line_dict['company_id'] = st_line.company_id.id
767 mv_line_dict['statement_id'] = st_line.statement_id.id
768 if mv_line_dict.get('counterpart_move_line_id'):
769 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
770 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
771 mv_line_dict['account_id'] = mv_line.account_id.id
772 if st_line_currency.id != company_currency.id:
774 ctx['date'] = st_line.date
775 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
776 mv_line_dict['currency_id'] = st_line_currency.id
777 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
778 debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
779 credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
780 elif st_line.currency_id and st_line_currency_rate:
781 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)
782 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)
784 debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
785 credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
786 if mv_line_dict.get('counterpart_move_line_id'):
787 #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
788 ctx['date'] = mv_line.date
789 debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
790 credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
791 mv_line_dict['credit'] = credit_at_old_rate
792 mv_line_dict['debit'] = debit_at_old_rate
793 if debit_at_old_rate - debit_at_current_rate:
794 currency_diff = debit_at_current_rate - debit_at_old_rate
795 to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
796 if credit_at_old_rate - credit_at_current_rate:
797 currency_diff = credit_at_current_rate - credit_at_old_rate
798 to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
800 mv_line_dict['debit'] = debit_at_current_rate
801 mv_line_dict['credit'] = credit_at_current_rate
802 elif statement_currency.id != company_currency.id:
803 #statement is in foreign currency but the transaction is in company currency
804 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
805 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
806 to_create.append(mv_line_dict)
808 move_line_pairs_to_reconcile = []
809 for mv_line_dict in to_create:
810 counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
811 if mv_line_dict.get('counterpart_move_line_id'):
812 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
813 del mv_line_dict['counterpart_move_line_id']
814 new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
815 if counterpart_move_line_id != None:
816 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
818 for pair in move_line_pairs_to_reconcile:
819 aml_obj.reconcile_partial(cr, uid, pair, context=context)
820 # Mark the statement line as reconciled
821 self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
823 # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
824 # return [('journal_entry_id', '=', False)]
825 # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
826 def _needaction_domain_get(self, cr, uid, context=None):
827 user = self.pool.get("res.users").browse(cr, uid, uid)
828 return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
830 _order = "statement_id desc, sequence"
831 _name = "account.bank.statement.line"
832 _description = "Bank Statement Line"
833 _inherit = ['ir.needaction_mixin']
835 'name': fields.char('Communication', required=True),
836 'date': fields.date('Date', required=True),
837 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
838 'partner_id': fields.many2one('res.partner', 'Partner'),
839 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
840 '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"),
841 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
842 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
843 '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)."),
844 'ref': fields.char('Reference'),
845 'note': fields.text('Notes'),
846 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
847 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
848 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
849 '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')),
850 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
853 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'),
854 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
857 class account_statement_operation_template(osv.osv):
858 _name = "account.statement.operation.template"
859 _description = "Preset for the lines that can be created in a bank statement reconciliation"
861 'name': fields.char('Button Label', required=True),
862 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type', 'not in', ('view', 'closed', 'consolidation'))]),
863 'label': fields.char('Label'),
864 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
865 'Amount type', required=True),
866 '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),
867 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use', 'in', ['purchase', 'all']), ('parent_id', '=', False)]),
868 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]),
871 'amount_type': 'percentage_of_balance',
875 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: