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 for item in self.browse(cr, uid, ids, context=context):
387 if item.state != 'draft':
388 raise osv.except_osv(
389 _('Invalid Action!'),
390 _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
392 return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
394 def button_journal_entries(self, cr, uid, ids, context=None):
395 ctx = (context or {}).copy()
396 ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
398 'name': _('Journal Items'),
401 'res_model':'account.move.line',
403 'type':'ir.actions.act_window',
404 'domain':[('statement_id','in',ids)],
408 def number_of_lines_reconciled(self, cr, uid, ids, context=None):
409 bsl_obj = self.pool.get('account.bank.statement.line')
410 return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
412 def link_bank_to_partner(self, cr, uid, ids, context=None):
413 for statement in self.browse(cr, uid, ids, context=context):
414 for st_line in statement.line_ids:
415 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:
416 self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context)
418 class account_bank_statement_line(osv.osv):
420 def create(self, cr, uid, vals, context=None):
421 if vals.get('amount_currency', 0) and not vals.get('amount', 0):
422 raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
423 return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
425 def unlink(self, cr, uid, ids, context=None):
426 for item in self.browse(cr, uid, ids, context=context):
427 if item.journal_entry_id:
428 raise osv.except_osv(
429 _('Invalid Action!'),
430 _('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
432 return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
434 def cancel(self, cr, uid, ids, context=None):
435 account_move_obj = self.pool.get('account.move')
437 for line in self.browse(cr, uid, ids, context=context):
438 if line.journal_entry_id:
439 move_ids.append(line.journal_entry_id.id)
440 for aml in line.journal_entry_id.line_id:
442 move_lines = [l.id for l in aml.reconcile_id.line_id]
443 move_lines.remove(aml.id)
444 self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
445 if len(move_lines) >= 2:
446 self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
448 account_move_obj.button_cancel(cr, uid, move_ids, context=context)
449 account_move_obj.unlink(cr, uid, move_ids, context)
451 def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
452 """ Returns the data required to display a reconciliation, for each statement line id in ids """
454 if excluded_ids is None:
457 for st_line in self.browse(cr, uid, ids, context=context):
458 reconciliation_data = {}
459 if search_reconciliation_proposition:
460 reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
461 for mv_line in reconciliation_proposition:
462 excluded_ids.append(mv_line['id'])
463 reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
465 reconciliation_data['reconciliation_proposition'] = []
466 st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
467 reconciliation_data['st_line'] = st_line
468 ret.append(reconciliation_data)
472 def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
473 """ Returns the data required by the bank statement reconciliation widget to display a statement line """
476 statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
477 rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
479 if st_line.amount_currency and st_line.currency_id:
480 amount = st_line.amount_currency
481 amount_currency = st_line.amount
482 amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
483 amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
485 amount = st_line.amount
486 amount_currency_str = ""
487 amount_str = amount > 0 and amount or -amount
488 amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
493 'note': st_line.note or "",
494 'name': st_line.name,
495 'date': st_line.date,
497 'amount_str': amount_str, # Amount in the statement line currency
498 'currency_id': st_line.currency_id.id or statement_currency.id,
499 'partner_id': st_line.partner_id.id,
500 'statement_id': st_line.statement_id.id,
501 'account_code': st_line.journal_id.default_debit_account_id.code,
502 'account_name': st_line.journal_id.default_debit_account_id.name,
503 'partner_name': st_line.partner_id.name,
504 'communication_partner_name': st_line.partner_name,
505 'amount_currency_str': amount_currency_str, # Amount in the statement currency
506 'has_no_partner': not st_line.partner_id.id,
508 if st_line.partner_id.id:
510 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
512 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
516 def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
517 if excluded_ids is None:
519 domain = [('ref', '=', st_line.name),
520 ('reconcile_id', '=', False),
521 ('state', '=', 'valid'),
522 ('account_id.reconcile', '=', True),
523 ('id', 'not in', excluded_ids)]
526 def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
527 """ Returns move lines that constitute the best guess to reconcile a statement line. """
528 mv_line_pool = self.pool.get('account.move.line')
530 # Look for structured communication
532 domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
533 match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
535 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
536 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
537 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]
538 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
539 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
540 if (mv_line['partner_id']):
541 self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
542 mv_line['has_no_partner'] = False
545 # If there is no identified partner or structured communication, don't look further
546 if not st_line.partner_id.id:
549 # Look for a move line whose amount matches the statement line's amount
550 company_currency = st_line.journal_id.company_id.currency_id.id
551 statement_currency = st_line.journal_id.currency.id or company_currency
553 if statement_currency == company_currency:
554 amount_field = 'credit'
555 if st_line.amount > 0:
556 amount_field = 'debit'
560 amount_field = 'amount_currency'
561 if st_line.amount < 0:
563 if st_line.amount_currency:
564 amount = st_line.amount_currency
566 amount = st_line.amount
568 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))])
574 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):
575 """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
576 if excluded_ids is None:
578 if additional_domain is None:
579 additional_domain = []
580 st_line = self.browse(cr, uid, st_line_id, context=context)
581 return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
583 def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
584 if excluded_ids is None:
586 if additional_domain is None:
587 additional_domain = []
589 domain = additional_domain + [('reconcile_id', '=', False),
590 ('state', '=', 'valid'),
591 ('account_id.reconcile', '=', True)]
592 if st_line.partner_id.id:
593 domain += [('partner_id', '=', st_line.partner_id.id)]
595 domain.append(('id', 'not in', excluded_ids))
597 domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
598 if not st_line.partner_id.id:
599 domain.insert(-1, '|', )
600 domain.append(('partner_id.name', 'ilike', str))
602 domain.insert(-1, '|', )
603 domain.append(('name', 'ilike', str))
606 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):
607 """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
609 :param st_line: the browse record of the statement line
610 :param integers list excluded_ids: ids of move lines that should not be fetched
611 :param boolean count: just return the number of records
612 :param tuples list additional_domain: additional domain restrictions
614 mv_line_pool = self.pool.get('account.move.line')
615 domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
617 # Get move lines ; in case of a partial reconciliation, only consider one line
619 reconcile_partial_ids = []
620 actual_offset = offset
622 line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
623 lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
624 make_one_more_loop = False
626 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
627 #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
628 #in order to get the right number of items in the pager
629 make_one_more_loop = True
631 filtered_lines.append(line)
632 if line.reconcile_partial_id:
633 reconcile_partial_ids.append(line.reconcile_partial_id.id)
635 if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
637 actual_offset = actual_offset + limit
638 lines = limit and filtered_lines[:limit] or filtered_lines
640 # Either return number of lines
644 # Or return list of dicts representing the formatted move lines
646 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
647 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)
648 has_no_partner = not bool(st_line.partner_id.id)
649 for line in mv_lines:
650 line['has_no_partner'] = has_no_partner
653 def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
654 if currency_diff < 0:
655 account_id = st_line.company_id.expense_currency_exchange_account_id.id
657 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."))
659 account_id = st_line.company_id.income_currency_exchange_account_id.id
661 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."))
664 'name': _('change') + ': ' + (st_line.name or '/'),
665 'period_id': st_line.statement_id.period_id.id,
666 'journal_id': st_line.journal_id.id,
667 'partner_id': st_line.partner_id.id,
668 'company_id': st_line.company_id.id,
669 'statement_id': st_line.statement_id.id,
670 'debit': currency_diff < 0 and -currency_diff or 0,
671 'credit': currency_diff > 0 and currency_diff or 0,
672 'amount_currency': 0.0,
673 'date': st_line.date,
674 'account_id': account_id
677 def process_reconciliations(self, cr, uid, data, context=None):
679 self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
681 def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
682 """ 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.
684 :param int id: id of the bank statement line
685 :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
689 st_line = self.browse(cr, uid, id, context=context)
690 company_currency = st_line.journal_id.company_id.currency_id
691 statement_currency = st_line.journal_id.currency or company_currency
692 bs_obj = self.pool.get('account.bank.statement')
693 am_obj = self.pool.get('account.move')
694 aml_obj = self.pool.get('account.move.line')
695 currency_obj = self.pool.get('res.currency')
698 if st_line.journal_entry_id.id:
699 raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
700 for mv_line_dict in mv_line_dicts:
701 for field in ['debit', 'credit', 'amount_currency']:
702 if field not in mv_line_dict:
703 mv_line_dict[field] = 0.0
704 if mv_line_dict.get('counterpart_move_line_id'):
705 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
706 if mv_line.reconcile_id:
707 raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
710 move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
711 move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
712 move_id = am_obj.create(cr, uid, move_vals, context=context)
714 # Create the move line for the statement line
715 if st_line.statement_id.currency.id != company_currency.id:
716 if st_line.currency_id == company_currency:
717 amount = st_line.amount_currency
720 ctx['date'] = st_line.date
721 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
723 amount = st_line.amount
724 bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
725 aml_obj.create(cr, uid, bank_st_move_vals, context=context)
727 st_line_currency = st_line.currency_id or statement_currency
728 st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
730 for mv_line_dict in mv_line_dicts:
731 if mv_line_dict.get('is_tax_line'):
733 mv_line_dict['ref'] = move_name
734 mv_line_dict['move_id'] = move_id
735 mv_line_dict['period_id'] = st_line.statement_id.period_id.id
736 mv_line_dict['journal_id'] = st_line.journal_id.id
737 mv_line_dict['company_id'] = st_line.company_id.id
738 mv_line_dict['statement_id'] = st_line.statement_id.id
739 if mv_line_dict.get('counterpart_move_line_id'):
740 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
741 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
742 mv_line_dict['account_id'] = mv_line.account_id.id
743 if st_line_currency.id != company_currency.id:
745 ctx['date'] = st_line.date
746 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
747 mv_line_dict['currency_id'] = st_line_currency.id
748 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
749 debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
750 credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
751 elif st_line.currency_id and st_line_currency_rate:
752 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)
753 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)
755 debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
756 credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
757 if mv_line_dict.get('counterpart_move_line_id'):
758 #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
759 ctx['date'] = mv_line.date
760 debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
761 credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
762 mv_line_dict['credit'] = credit_at_old_rate
763 mv_line_dict['debit'] = debit_at_old_rate
764 if debit_at_old_rate - debit_at_current_rate:
765 currency_diff = debit_at_current_rate - debit_at_old_rate
766 to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
767 if credit_at_old_rate - credit_at_current_rate:
768 currency_diff = credit_at_current_rate - credit_at_old_rate
769 to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
771 mv_line_dict['debit'] = debit_at_current_rate
772 mv_line_dict['credit'] = credit_at_current_rate
773 elif statement_currency.id != company_currency.id:
774 #statement is in foreign currency but the transaction is in company currency
775 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
776 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
777 to_create.append(mv_line_dict)
779 move_line_pairs_to_reconcile = []
780 for mv_line_dict in to_create:
781 counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
782 if mv_line_dict.get('counterpart_move_line_id'):
783 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
784 del mv_line_dict['counterpart_move_line_id']
785 new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
786 if counterpart_move_line_id != None:
787 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
789 for pair in move_line_pairs_to_reconcile:
790 aml_obj.reconcile_partial(cr, uid, pair, context=context)
791 # Mark the statement line as reconciled
792 self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
794 # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
795 # return [('journal_entry_id', '=', False)]
796 # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
797 def _needaction_domain_get(self, cr, uid, context=None):
798 user = self.pool.get("res.users").browse(cr, uid, uid)
799 return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
801 _order = "statement_id desc, sequence"
802 _name = "account.bank.statement.line"
803 _description = "Bank Statement Line"
804 _inherit = ['ir.needaction_mixin']
806 'name': fields.char('Communication', required=True),
807 'date': fields.date('Date', required=True),
808 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
809 'partner_id': fields.many2one('res.partner', 'Partner'),
810 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
811 '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"),
812 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
813 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
814 '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)."),
815 'ref': fields.char('Reference'),
816 'note': fields.text('Notes'),
817 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
818 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
819 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
820 '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')),
821 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
824 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
827 class account_statement_operation_template(osv.osv):
828 _name = "account.statement.operation.template"
829 _description = "Preset for the lines that can be created in a bank statement reconciliation"
831 'name': fields.char('Button Label', required=True),
832 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','!=','view')]),
833 'label': fields.char('Label'),
834 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
835 'Amount type', required=True),
836 '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),
837 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'),
838 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
841 'amount_type': 'percentage_of_balance',
845 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: