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
29 class account_bank_statement(osv.osv):
30 def create(self, cr, uid, vals, context=None):
31 if vals.get('name', '/') == '/':
32 journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context))
33 vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context)
34 if 'line_ids' in vals:
35 for idx, line in enumerate(vals['line_ids']):
36 line[2]['sequence'] = idx + 1
37 return super(account_bank_statement, self).create(cr, uid, vals, context=context)
39 def write(self, cr, uid, ids, vals, context=None):
40 res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
41 account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
42 for statement in self.browse(cr, uid, ids, context):
43 for idx, line in enumerate(statement.line_ids):
44 account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context)
47 def _default_journal_id(self, cr, uid, context=None):
50 journal_pool = self.pool.get('account.journal')
51 journal_type = context.get('journal_type', False)
52 company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context)
54 ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
59 def _end_balance(self, cursor, user, ids, name, attr, context=None):
61 for statement in self.browse(cursor, user, ids, context=context):
62 res[statement.id] = statement.balance_start
63 for line in statement.line_ids:
64 res[statement.id] += line.amount
67 def _get_period(self, cr, uid, context=None):
68 periods = self.pool.get('account.period').find(cr, uid, context=context)
73 def _compute_default_statement_name(self, cr, uid, journal_id, context=None):
74 context = dict(context or {})
75 obj_seq = self.pool.get('ir.sequence')
76 period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context)
77 context['fiscalyear_id'] = period.fiscalyear_id.id
78 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None)
79 return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context)
81 def _currency(self, cursor, user, ids, name, args, context=None):
83 res_currency_obj = self.pool.get('res.currency')
84 res_users_obj = self.pool.get('res.users')
85 default_currency = res_users_obj.browse(cursor, user,
86 user, context=context).company_id.currency_id
87 for statement in self.browse(cursor, user, ids, context=context):
88 currency = statement.journal_id.currency
90 currency = default_currency
91 res[statement.id] = currency.id
93 for currency_id, currency_name in res_currency_obj.name_get(cursor,
94 user, [x for x in res.values()], context=context):
95 currency_names[currency_id] = currency_name
96 for statement_id in res.keys():
97 currency_id = res[statement_id]
98 res[statement_id] = (currency_id, currency_names[currency_id])
101 def _get_statement(self, cr, uid, ids, context=None):
103 for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
104 result[line.statement_id.id] = True
107 def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None):
109 for statement in self.browse(cr, uid, ids, context=context):
110 res[statement.id] = all([line.journal_entry_id.id for line in statement.line_ids])
113 _order = "date desc, id desc"
114 _name = "account.bank.statement"
115 _description = "Bank Statement"
116 _inherit = ['mail.thread']
119 'Reference', states={'draft': [('readonly', False)]},
120 readonly=True, # readonly for account_cash_statement
122 help='if you give the Name other then /, its created Accounting Entries Move '
123 'will be with same name as statement name. '
124 'This allows the statement entries to have the same references than the '
126 'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]},
127 select=True, copy=False),
128 'journal_id': fields.many2one('account.journal', 'Journal', required=True,
129 readonly=True, states={'draft':[('readonly',False)]}),
130 'period_id': fields.many2one('account.period', 'Period', required=True,
131 states={'confirm':[('readonly', True)]}),
132 'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'),
133 states={'confirm':[('readonly',True)]}),
134 'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'),
135 states={'confirm': [('readonly', True)]}, help="Computed using the cash control lines"),
136 'balance_end': fields.function(_end_balance,
138 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10),
139 'account.bank.statement.line': (_get_statement, ['amount'], 10),
141 string="Computed Balance", help='Balance as calculated based on Opening Balance and transaction lines'),
142 'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
143 'line_ids': fields.one2many('account.bank.statement.line',
144 'statement_id', 'Statement lines',
145 states={'confirm':[('readonly', True)]}, copy=True),
146 'move_line_ids': fields.one2many('account.move.line', 'statement_id',
147 'Entry lines', states={'confirm':[('readonly',True)]}),
148 'state': fields.selection([('draft', 'New'),
149 ('open','Open'), # used by cash statements
150 ('confirm', 'Closed')],
151 'Status', required=True, readonly="1",
153 help='When new statement is created the status will be \'Draft\'.\n'
154 'And after getting confirmation from the bank it will be in \'Confirmed\' status.'),
155 'currency': fields.function(_currency, string='Currency',
156 type='many2one', relation='res.currency'),
157 '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.'),
158 'cash_control': fields.related('journal_id', 'cash_control' , type='boolean', relation='account.journal',string='Cash control'),
159 'all_lines_reconciled': fields.function(_all_lines_reconciled, string='All lines reconciled', type='boolean'),
164 'date': fields.date.context_today,
166 'journal_id': _default_journal_id,
167 'period_id': _get_period,
168 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=c),
171 def _check_company_id(self, cr, uid, ids, context=None):
172 for statement in self.browse(cr, uid, ids, context=context):
173 if statement.company_id.id != statement.period_id.company_id.id:
178 (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
181 def onchange_date(self, cr, uid, ids, date, company_id, context=None):
183 Find the correct period to use for the given date and company_id, return it and set it in the context
186 period_pool = self.pool.get('account.period')
191 ctx.update({'company_id': company_id})
192 pids = period_pool.find(cr, uid, dt=date, context=ctx)
194 res.update({'period_id': pids[0]})
195 context = dict(context, period_id=pids[0])
202 def button_dummy(self, cr, uid, ids, context=None):
203 return self.write(cr, uid, ids, {}, context=context)
205 def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
206 """Prepare the dict of values to create the move from a
207 statement line. This method may be overridden to implement custom
208 move generation (making sure to call super() to establish
209 a clean extension chain).
211 :param browse_record st_line: account.bank.statement.line record to
212 create the move from.
213 :param char st_line_number: will be used as the name of the generated account move
214 :return: dict of value to create() the account.move
217 'journal_id': st_line.statement_id.journal_id.id,
218 'period_id': st_line.statement_id.period_id.id,
219 'date': st_line.date,
220 'name': st_line_number,
224 def _get_counter_part_account(self, cr, uid, st_line, context=None):
225 """Retrieve the account to use in the counterpart move.
227 :param browse_record st_line: account.bank.statement.line record to create the move from.
228 :return: int/long of the account.account to use as counterpart
230 if st_line.amount >= 0:
231 return st_line.statement_id.journal_id.default_credit_account_id.id
232 return st_line.statement_id.journal_id.default_debit_account_id.id
234 def _get_counter_part_partner(self, cr, uid, st_line, context=None):
235 """Retrieve the partner to use in the counterpart move.
237 :param browse_record st_line: account.bank.statement.line record to create the move from.
238 :return: int/long of the res.partner to use as counterpart
240 return st_line.partner_id and st_line.partner_id.id or False
242 def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id, context=None):
243 """Compute the args to build the dict of values to create the counter part move line from a
244 statement line by calling the _prepare_move_line_vals.
246 :param browse_record st_line: account.bank.statement.line record to create the move from.
247 :param int/long move_id: ID of the account.move to link the move line
248 :param float amount: amount of the move line
249 :param int/long company_currency_id: ID of currency of the concerned company
250 :return: dict of value to create() the bank account.move.line
252 account_id = self._get_counter_part_account(cr, uid, st_line, context=context)
253 partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context)
254 debit = ((amount > 0) and amount) or 0.0
255 credit = ((amount < 0) and -amount) or 0.0
258 if st_line.statement_id.currency.id != company_currency_id:
259 amt_cur = st_line.amount
260 cur_id = st_line.statement_id.currency.id
261 elif st_line.currency_id and st_line.amount_currency:
262 amt_cur = st_line.amount_currency
263 cur_id = st_line.currency_id.id
264 return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
265 amount_currency=amt_cur, currency_id=cur_id, account_id=account_id,
266 partner_id=partner_id, context=context)
268 def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
269 amount_currency=False, account_id=False, partner_id=False, context=None):
270 """Prepare the dict of values to create the move line from a
273 :param browse_record st_line: account.bank.statement.line record to
274 create the move from.
275 :param int/long move_id: ID of the account.move to link the move line
276 :param float debit: debit amount of the move line
277 :param float credit: credit amount of the move line
278 :param int/long currency_id: ID of currency of the move line to create
279 :param float amount_currency: amount of the debit/credit expressed in the currency_id
280 :param int/long account_id: ID of the account to use in the move line if different
281 from the statement line account ID
282 :param int/long partner_id: ID of the partner to put on the move line
283 :return: dict of value to create() the account.move.line
285 acc_id = account_id or st_line.account_id.id
286 cur_id = currency_id or st_line.statement_id.currency.id
287 par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False)
289 'name': st_line.name,
290 'date': st_line.date,
293 'partner_id': par_id,
294 'account_id': acc_id,
297 'statement_id': st_line.statement_id.id,
298 'journal_id': st_line.statement_id.journal_id.id,
299 'period_id': st_line.statement_id.period_id.id,
300 'currency_id': amount_currency and cur_id,
301 'amount_currency': amount_currency,
304 def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
305 st = self.browse(cr, uid, st_id, context=context)
306 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)):
307 raise osv.except_osv(_('Error!'),
308 _('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end))
311 def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
312 return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
314 def check_status_condition(self, cr, uid, state, journal_type='bank'):
315 return state in ('draft','open')
317 def button_confirm_bank(self, cr, uid, ids, context=None):
321 for st in self.browse(cr, uid, ids, context=context):
322 j_type = st.journal_id.type
323 if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
326 self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
327 if (not st.journal_id.default_credit_account_id) \
328 or (not st.journal_id.default_debit_account_id):
329 raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
330 for line in st.move_line_ids:
331 if line.state != 'valid':
332 raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
334 for st_line in st.line_ids:
335 if not st_line.amount:
337 if st_line.account_id and not st_line.journal_entry_id.id:
338 #make an account move as before
340 'debit': st_line.amount < 0 and -st_line.amount or 0.0,
341 'credit': st_line.amount > 0 and st_line.amount or 0.0,
342 'account_id': st_line.account_id.id,
345 self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
346 elif not st_line.journal_entry_id.id:
347 raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
348 move_ids.append(st_line.journal_entry_id.id)
350 self.pool.get('account.move').post(cr, uid, move_ids, context=context)
351 self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
352 self.link_bank_to_partner(cr, uid, ids, context=context)
353 return self.write(cr, uid, ids, {'state': 'confirm', 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
355 def button_cancel(self, cr, uid, ids, context=None):
357 for st in self.browse(cr, uid, ids, context=context):
358 bnk_st_line_ids += [line.id for line in st.line_ids]
359 self.pool.get('account.bank.statement.line').cancel(cr, uid, bnk_st_line_ids, context=context)
360 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
362 def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
365 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
366 if journal.with_last_closing_balance:
367 cr.execute('SELECT balance_end_real \
368 FROM account_bank_statement \
369 WHERE journal_id = %s AND NOT state = %s \
370 ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft'))
372 return res and res[0] or 0.0
374 def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
377 balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context)
378 journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
379 currency = journal.currency or journal.company_id.currency_id
380 res = {'balance_start': balance_start, 'company_id': journal.company_id.id, 'currency': currency.id}
381 if journal.type == 'cash':
382 res['cash_control'] = journal.cash_control
383 return {'value': res}
385 def unlink(self, cr, uid, ids, context=None):
386 statement_line_obj = self.pool['account.bank.statement.line']
387 for item in self.browse(cr, uid, ids, context=context):
388 if item.state != 'draft':
389 raise osv.except_osv(
390 _('Invalid Action!'),
391 _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
393 # Explicitly unlink bank statement lines
394 # so it will check that the related journal entries have
396 statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
397 return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
399 def button_journal_entries(self, cr, uid, ids, context=None):
400 ctx = (context or {}).copy()
401 ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
403 'name': _('Journal Items'),
406 'res_model':'account.move.line',
408 'type':'ir.actions.act_window',
409 'domain':[('statement_id','in',ids)],
413 def number_of_lines_reconciled(self, cr, uid, ids, context=None):
414 bsl_obj = self.pool.get('account.bank.statement.line')
415 return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
417 def link_bank_to_partner(self, cr, uid, ids, context=None):
418 for statement in self.browse(cr, uid, ids, context=context):
419 for st_line in statement.line_ids:
420 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:
421 self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context)
423 class account_bank_statement_line(osv.osv):
425 def create(self, cr, uid, vals, context=None):
426 if vals.get('amount_currency', 0) and not vals.get('amount', 0):
427 raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
428 return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
430 def unlink(self, cr, uid, ids, context=None):
431 for item in self.browse(cr, uid, ids, context=context):
432 if item.journal_entry_id:
433 raise osv.except_osv(
434 _('Invalid Action!'),
435 _('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
437 return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
439 def cancel(self, cr, uid, ids, context=None):
440 account_move_obj = self.pool.get('account.move')
442 for line in self.browse(cr, uid, ids, context=context):
443 if line.journal_entry_id:
444 move_ids.append(line.journal_entry_id.id)
445 for aml in line.journal_entry_id.line_id:
447 move_lines = [l.id for l in aml.reconcile_id.line_id]
448 move_lines.remove(aml.id)
449 self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
450 if len(move_lines) >= 2:
451 self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
453 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
454 account_move_obj.unlink(cr, uid, move_ids, context)
456 def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
457 """ Returns the data required to display a reconciliation, for each statement line id in ids """
459 if excluded_ids is None:
462 for st_line in self.browse(cr, uid, ids, context=context):
463 reconciliation_data = {}
464 if search_reconciliation_proposition:
465 reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
466 for mv_line in reconciliation_proposition:
467 excluded_ids.append(mv_line['id'])
468 reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
470 reconciliation_data['reconciliation_proposition'] = []
471 st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
472 reconciliation_data['st_line'] = st_line
473 ret.append(reconciliation_data)
477 def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
478 """ Returns the data required by the bank statement reconciliation widget to display a statement line """
481 statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
482 rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
484 if st_line.amount_currency and st_line.currency_id:
485 amount = st_line.amount_currency
486 amount_currency = st_line.amount
487 amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
488 amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
490 amount = st_line.amount
491 amount_currency_str = ""
492 amount_str = amount > 0 and amount or -amount
493 amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
498 'note': st_line.note or "",
499 'name': st_line.name,
500 'date': st_line.date,
502 'amount_str': amount_str, # Amount in the statement line currency
503 'currency_id': st_line.currency_id.id or statement_currency.id,
504 'partner_id': st_line.partner_id.id,
505 'statement_id': st_line.statement_id.id,
506 'account_code': st_line.journal_id.default_debit_account_id.code,
507 'account_name': st_line.journal_id.default_debit_account_id.name,
508 'partner_name': st_line.partner_id.name,
509 'communication_partner_name': st_line.partner_name,
510 'amount_currency_str': amount_currency_str, # Amount in the statement currency
511 'has_no_partner': not st_line.partner_id.id,
513 if st_line.partner_id.id:
515 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
517 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
521 def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
522 if excluded_ids is None:
524 domain = [('ref', '=', st_line.name),
525 ('reconcile_id', '=', False),
526 ('state', '=', 'valid'),
527 ('account_id.reconcile', '=', True),
528 ('id', 'not in', excluded_ids)]
531 def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
532 """ Returns move lines that constitute the best guess to reconcile a statement line. """
533 mv_line_pool = self.pool.get('account.move.line')
535 # Look for structured communication
537 domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
538 match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
540 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
541 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
542 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]
543 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
544 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
545 if (mv_line['partner_id']):
546 self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
547 mv_line['has_no_partner'] = False
550 # If there is no identified partner or structured communication, don't look further
551 if not st_line.partner_id.id:
554 # Look for a move line whose amount matches the statement line's amount
555 company_currency = st_line.journal_id.company_id.currency_id.id
556 statement_currency = st_line.journal_id.currency.id or company_currency
558 if statement_currency == company_currency:
559 amount_field = 'credit'
560 if st_line.amount > 0:
561 amount_field = 'debit'
565 amount_field = 'amount_currency'
566 if st_line.amount < 0:
568 if st_line.amount_currency:
569 amount = st_line.amount_currency
571 amount = st_line.amount
573 match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=[(amount_field, '=', (sign * amount))])
579 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):
580 """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
581 if excluded_ids is None:
583 if additional_domain is None:
584 additional_domain = []
585 st_line = self.browse(cr, uid, st_line_id, context=context)
586 return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
588 def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
589 if excluded_ids is None:
591 if additional_domain is None:
592 additional_domain = []
594 domain = additional_domain + [('reconcile_id', '=', False),
595 ('state', '=', 'valid'),
596 ('account_id.reconcile', '=', True)]
597 if st_line.partner_id.id:
598 domain += [('partner_id', '=', st_line.partner_id.id)]
600 domain.append(('id', 'not in', excluded_ids))
602 domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
603 if not st_line.partner_id.id:
604 domain.insert(-1, '|', )
605 domain.append(('partner_id.name', 'ilike', str))
607 domain.insert(-1, '|', )
608 domain.append(('name', 'ilike', str))
611 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):
612 """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
614 :param st_line: the browse record of the statement line
615 :param integers list excluded_ids: ids of move lines that should not be fetched
616 :param boolean count: just return the number of records
617 :param tuples list additional_domain: additional domain restrictions
619 mv_line_pool = self.pool.get('account.move.line')
620 domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
622 # Get move lines ; in case of a partial reconciliation, only consider one line
624 reconcile_partial_ids = []
625 actual_offset = offset
627 line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
628 lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
629 make_one_more_loop = False
631 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
632 #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
633 #in order to get the right number of items in the pager
634 make_one_more_loop = True
636 filtered_lines.append(line)
637 if line.reconcile_partial_id:
638 reconcile_partial_ids.append(line.reconcile_partial_id.id)
640 if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
642 actual_offset = actual_offset + limit
643 lines = limit and filtered_lines[:limit] or filtered_lines
645 # Either return number of lines
649 # Or return list of dicts representing the formatted move lines
651 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
652 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)
653 has_no_partner = not bool(st_line.partner_id.id)
654 for line in mv_lines:
655 line['has_no_partner'] = has_no_partner
658 def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
659 if currency_diff < 0:
660 account_id = st_line.company_id.expense_currency_exchange_account_id.id
662 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."))
664 account_id = st_line.company_id.income_currency_exchange_account_id.id
666 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."))
669 'name': _('change') + ': ' + (st_line.name or '/'),
670 'period_id': st_line.statement_id.period_id.id,
671 'journal_id': st_line.journal_id.id,
672 'partner_id': st_line.partner_id.id,
673 'company_id': st_line.company_id.id,
674 'statement_id': st_line.statement_id.id,
675 'debit': currency_diff < 0 and -currency_diff or 0,
676 'credit': currency_diff > 0 and currency_diff or 0,
677 'amount_currency': 0.0,
678 'date': st_line.date,
679 'account_id': account_id
682 def process_reconciliations(self, cr, uid, data, context=None):
684 self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
686 def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
687 """ 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.
689 :param int id: id of the bank statement line
690 :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
694 st_line = self.browse(cr, uid, id, context=context)
695 company_currency = st_line.journal_id.company_id.currency_id
696 statement_currency = st_line.journal_id.currency or company_currency
697 bs_obj = self.pool.get('account.bank.statement')
698 am_obj = self.pool.get('account.move')
699 aml_obj = self.pool.get('account.move.line')
700 currency_obj = self.pool.get('res.currency')
703 if st_line.journal_entry_id.id:
704 raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
705 for mv_line_dict in mv_line_dicts:
706 for field in ['debit', 'credit', 'amount_currency']:
707 if field not in mv_line_dict:
708 mv_line_dict[field] = 0.0
709 if mv_line_dict.get('counterpart_move_line_id'):
710 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
711 if mv_line.reconcile_id:
712 raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
715 move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
716 move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
717 move_id = am_obj.create(cr, uid, move_vals, context=context)
719 # Create the move line for the statement line
720 if st_line.statement_id.currency.id != company_currency.id:
721 if st_line.currency_id == company_currency:
722 amount = st_line.amount_currency
725 ctx['date'] = st_line.date
726 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
728 amount = st_line.amount
729 bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
730 aml_obj.create(cr, uid, bank_st_move_vals, context=context)
732 st_line_currency = st_line.currency_id or statement_currency
733 st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
735 for mv_line_dict in mv_line_dicts:
736 if mv_line_dict.get('is_tax_line'):
738 mv_line_dict['ref'] = move_name
739 mv_line_dict['move_id'] = move_id
740 mv_line_dict['period_id'] = st_line.statement_id.period_id.id
741 mv_line_dict['journal_id'] = st_line.journal_id.id
742 mv_line_dict['company_id'] = st_line.company_id.id
743 mv_line_dict['statement_id'] = st_line.statement_id.id
744 if mv_line_dict.get('counterpart_move_line_id'):
745 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
746 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
747 mv_line_dict['account_id'] = mv_line.account_id.id
748 if st_line_currency.id != company_currency.id:
750 ctx['date'] = st_line.date
751 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
752 mv_line_dict['currency_id'] = st_line_currency.id
753 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
754 debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
755 credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
756 elif st_line.currency_id and st_line_currency_rate:
757 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)
758 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)
760 debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
761 credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
762 if mv_line_dict.get('counterpart_move_line_id'):
763 #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
764 ctx['date'] = mv_line.date
765 debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
766 credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
767 mv_line_dict['credit'] = credit_at_old_rate
768 mv_line_dict['debit'] = debit_at_old_rate
769 if debit_at_old_rate - debit_at_current_rate:
770 currency_diff = debit_at_current_rate - debit_at_old_rate
771 to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
772 if credit_at_old_rate - credit_at_current_rate:
773 currency_diff = credit_at_current_rate - credit_at_old_rate
774 to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
776 mv_line_dict['debit'] = debit_at_current_rate
777 mv_line_dict['credit'] = credit_at_current_rate
778 elif statement_currency.id != company_currency.id:
779 #statement is in foreign currency but the transaction is in company currency
780 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
781 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
782 to_create.append(mv_line_dict)
784 move_line_pairs_to_reconcile = []
785 for mv_line_dict in to_create:
786 counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
787 if mv_line_dict.get('counterpart_move_line_id'):
788 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
789 del mv_line_dict['counterpart_move_line_id']
790 new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
791 if counterpart_move_line_id != None:
792 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
794 for pair in move_line_pairs_to_reconcile:
795 aml_obj.reconcile_partial(cr, uid, pair, context=context)
796 # Mark the statement line as reconciled
797 self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
799 # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
800 # return [('journal_entry_id', '=', False)]
801 # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
802 def _needaction_domain_get(self, cr, uid, context=None):
803 user = self.pool.get("res.users").browse(cr, uid, uid)
804 return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
806 _order = "statement_id desc, sequence"
807 _name = "account.bank.statement.line"
808 _description = "Bank Statement Line"
809 _inherit = ['ir.needaction_mixin']
811 'name': fields.char('Communication', required=True),
812 'date': fields.date('Date', required=True),
813 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
814 'partner_id': fields.many2one('res.partner', 'Partner'),
815 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
816 '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"),
817 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
818 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
819 '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)."),
820 'ref': fields.char('Reference'),
821 'note': fields.text('Notes'),
822 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
823 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
824 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
825 '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')),
826 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
829 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
832 class account_statement_operation_template(osv.osv):
833 _name = "account.statement.operation.template"
834 _description = "Preset for the lines that can be created in a bank statement reconciliation"
836 'name': fields.char('Button Label', required=True),
837 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','not in',('view','closed','consolidation'))]),
838 'label': fields.char('Journal Item Label'),
839 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')], 'Amount type', required=True),
840 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), required=True, 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')."),
841 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use','in',('purchase','all')), ('parent_id','=',False)]),
842 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]),
843 'company_id': fields.many2one('res.company', 'Company', required=True),
846 'amount_type': 'percentage_of_balance',
848 'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
851 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: