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(sefl, 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(sefl, 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 get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
517 """ Returns move lines that constitute the best guess to reconcile a statement line. """
518 if excluded_ids is None:
520 mv_line_pool = self.pool.get('account.move.line')
522 # Look for structured communication
524 structured_com_match_domain = [('ref', '=', st_line.name),('reconcile_id', '=', False),('state', '=', 'valid'),('account_id.reconcile', '=', True),('id', 'not in', excluded_ids)]
525 match_id = mv_line_pool.search(cr, uid, structured_com_match_domain, offset=0, limit=1, context=context)
527 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
528 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
529 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]
530 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
531 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
532 if (mv_line['partner_id']):
533 self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
534 mv_line['has_no_partner'] = False
537 # If there is no identified partner or structured communication, don't look further
538 if not st_line.partner_id.id:
541 # Look for a move line whose amount matches the statement line's amount
542 company_currency = st_line.journal_id.company_id.currency_id.id
543 statement_currency = st_line.journal_id.currency.id or company_currency
545 if statement_currency == company_currency:
546 amount_field = 'credit'
547 if st_line.amount > 0:
548 amount_field = 'debit'
552 amount_field = 'amount_currency'
553 if st_line.amount < 0:
555 if st_line.amount_currency:
556 amount = st_line.amount_currency
558 amount = st_line.amount
560 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))])
566 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):
567 """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
568 if excluded_ids is None:
570 if additional_domain is None:
571 additional_domain = []
572 st_line = self.browse(cr, uid, st_line_id, context=context)
573 return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
575 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):
576 """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
578 :param st_line: the browse record of the statement line
579 :param integers list excluded_ids: ids of move lines that should not be fetched
580 :param boolean count: just return the number of records
581 :param tuples list additional_domain: additional domain restrictions
583 if excluded_ids is None:
585 if additional_domain is None:
586 additional_domain = []
587 mv_line_pool = self.pool.get('account.move.line')
590 domain = additional_domain + [('reconcile_id', '=', False), ('state', '=', 'valid'), ('account_id.reconcile', '=', True)]
591 if st_line.partner_id.id:
592 domain += [('partner_id', '=', st_line.partner_id.id)]
594 domain.append(('id', 'not in', excluded_ids))
596 domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
597 if not st_line.partner_id.id:
598 domain.insert(-1, '|', )
599 domain.append(('partner_id.name', 'ilike', str))
601 domain.insert(-1, '|', )
602 domain.append(('name', 'ilike', str))
604 # Get move lines ; in case of a partial reconciliation, only consider one line
606 reconcile_partial_ids = []
607 actual_offset = offset
609 line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
610 lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
611 make_one_more_loop = False
613 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
614 #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
615 #in order to get the right number of items in the pager
616 make_one_more_loop = True
618 filtered_lines.append(line)
619 if line.reconcile_partial_id:
620 reconcile_partial_ids.append(line.reconcile_partial_id.id)
622 if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
624 actual_offset = actual_offset + limit
625 lines = limit and filtered_lines[:limit] or filtered_lines
627 # Either return number of lines
631 # Or return list of dicts representing the formatted move lines
633 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
634 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)
635 has_no_partner = not bool(st_line.partner_id.id)
636 for line in mv_lines:
637 line['has_no_partner'] = has_no_partner
640 def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
641 if currency_diff < 0:
642 account_id = st_line.company_id.expense_currency_exchange_account_id.id
644 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."))
646 account_id = st_line.company_id.income_currency_exchange_account_id.id
648 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."))
651 'name': _('change') + ': ' + (st_line.name or '/'),
652 'period_id': st_line.statement_id.period_id.id,
653 'journal_id': st_line.journal_id.id,
654 'partner_id': st_line.partner_id.id,
655 'company_id': st_line.company_id.id,
656 'statement_id': st_line.statement_id.id,
657 'debit': currency_diff < 0 and -currency_diff or 0,
658 'credit': currency_diff > 0 and currency_diff or 0,
659 'amount_currency': 0.0,
660 'date': st_line.date,
661 'account_id': account_id
664 def process_reconciliations(self, cr, uid, data, context=None):
666 self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
668 def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
669 """ 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.
671 :param int id: id of the bank statement line
672 :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
676 st_line = self.browse(cr, uid, id, context=context)
677 company_currency = st_line.journal_id.company_id.currency_id
678 statement_currency = st_line.journal_id.currency or company_currency
679 bs_obj = self.pool.get('account.bank.statement')
680 am_obj = self.pool.get('account.move')
681 aml_obj = self.pool.get('account.move.line')
682 currency_obj = self.pool.get('res.currency')
685 if st_line.journal_entry_id.id:
686 raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
687 for mv_line_dict in mv_line_dicts:
688 for field in ['debit', 'credit', 'amount_currency']:
689 if field not in mv_line_dict:
690 mv_line_dict[field] = 0.0
691 if mv_line_dict.get('counterpart_move_line_id'):
692 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
693 if mv_line.reconcile_id:
694 raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
697 move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
698 move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
699 move_id = am_obj.create(cr, uid, move_vals, context=context)
701 # Create the move line for the statement line
702 if st_line.statement_id.currency.id != company_currency.id:
703 if st_line.currency_id == company_currency:
704 amount = st_line.amount_currency
707 ctx['date'] = st_line.date
708 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
710 amount = st_line.amount
711 bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
712 aml_obj.create(cr, uid, bank_st_move_vals, context=context)
714 st_line_currency = st_line.currency_id or statement_currency
715 st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
717 for mv_line_dict in mv_line_dicts:
718 if mv_line_dict.get('is_tax_line'):
720 mv_line_dict['ref'] = move_name
721 mv_line_dict['move_id'] = move_id
722 mv_line_dict['period_id'] = st_line.statement_id.period_id.id
723 mv_line_dict['journal_id'] = st_line.journal_id.id
724 mv_line_dict['company_id'] = st_line.company_id.id
725 mv_line_dict['statement_id'] = st_line.statement_id.id
726 if mv_line_dict.get('counterpart_move_line_id'):
727 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
728 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
729 mv_line_dict['account_id'] = mv_line.account_id.id
730 if st_line_currency.id != company_currency.id:
732 ctx['date'] = st_line.date
733 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
734 mv_line_dict['currency_id'] = st_line_currency.id
735 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
736 debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
737 credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
738 elif st_line.currency_id and st_line_currency_rate:
739 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)
740 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)
742 debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
743 credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
744 if mv_line_dict.get('counterpart_move_line_id'):
745 #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
746 ctx['date'] = mv_line.date
747 debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
748 credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
749 mv_line_dict['credit'] = credit_at_old_rate
750 mv_line_dict['debit'] = debit_at_old_rate
751 if debit_at_old_rate - debit_at_current_rate:
752 currency_diff = debit_at_current_rate - debit_at_old_rate
753 to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
754 if credit_at_old_rate - credit_at_current_rate:
755 currency_diff = credit_at_current_rate - credit_at_old_rate
756 to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
758 mv_line_dict['debit'] = debit_at_current_rate
759 mv_line_dict['credit'] = credit_at_current_rate
760 elif statement_currency.id != company_currency.id:
761 #statement is in foreign currency but the transaction is in company currency
762 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
763 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
764 to_create.append(mv_line_dict)
766 move_line_pairs_to_reconcile = []
767 for mv_line_dict in to_create:
768 counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
769 if mv_line_dict.get('counterpart_move_line_id'):
770 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
771 del mv_line_dict['counterpart_move_line_id']
772 new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
773 if counterpart_move_line_id != None:
774 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
776 for pair in move_line_pairs_to_reconcile:
777 aml_obj.reconcile_partial(cr, uid, pair, context=context)
778 # Mark the statement line as reconciled
779 self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
781 # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
782 # return [('journal_entry_id', '=', False)]
783 # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
784 def _needaction_domain_get(self, cr, uid, context=None):
785 user = self.pool.get("res.users").browse(cr, uid, uid)
786 return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
788 _order = "statement_id desc, sequence"
789 _name = "account.bank.statement.line"
790 _description = "Bank Statement Line"
791 _inherit = ['ir.needaction_mixin']
793 'name': fields.char('Communication', required=True),
794 'date': fields.date('Date', required=True),
795 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
796 'partner_id': fields.many2one('res.partner', 'Partner'),
797 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
798 '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"),
799 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
800 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
801 '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)."),
802 'ref': fields.char('Reference'),
803 'note': fields.text('Notes'),
804 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
805 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
806 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
807 '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')),
808 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
811 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
814 class account_statement_operation_template(osv.osv):
815 _name = "account.statement.operation.template"
816 _description = "Preset for the lines that can be created in a bank statement reconciliation"
818 'name': fields.char('Button Label', required=True),
819 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','!=','view')]),
820 'label': fields.char('Label'),
821 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
822 'Amount type', required=True),
823 '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),
824 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'),
825 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
828 'amount_type': 'percentage_of_balance',
832 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: