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')]
591 if st_line.partner_id.id:
592 domain += [('partner_id', '=', st_line.partner_id.id),
593 '|', ('account_id.type', '=', 'receivable'),
594 ('account_id.type', '=', 'payable')]
596 domain += [('account_id.reconcile', '=', True), ('account_id.type', '=', 'other')]
598 domain.append(('id', 'not in', excluded_ids))
600 domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
601 if not st_line.partner_id.id:
602 domain.insert(-1, '|', )
603 domain.append(('partner_id.name', 'ilike', str))
605 # Get move lines ; in case of a partial reconciliation, only consider one line
607 reconcile_partial_ids = []
610 actual_offset = offset and offset+limit*shift or offset
611 actual_limit = limit and limit+limit*shift or limit
612 line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=actual_limit, order="date_maturity asc, id asc", context=context)
613 lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
615 did_filter_out_lines = False
617 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
618 did_filter_out_lines = True
620 filtered_lines.append(line)
621 if line.reconcile_partial_id:
622 reconcile_partial_ids.append(line.reconcile_partial_id.id)
624 if not limit or not did_filter_out_lines or len(filtered_lines) >= limit:
627 lines = limit and filtered_lines[:limit] or filtered_lines
629 # Either return number of lines
633 # Or return list of dicts representing the formatted move lines
635 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
636 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)
637 has_no_partner = not bool(st_line.partner_id.id)
638 for line in mv_lines:
639 line['has_no_partner'] = has_no_partner
642 def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
643 if currency_diff < 0:
644 account_id = st_line.company_id.expense_currency_exchange_account_id.id
646 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."))
648 account_id = st_line.company_id.income_currency_exchange_account_id.id
650 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."))
653 'name': _('change') + ': ' + (st_line.name or '/'),
654 'period_id': st_line.statement_id.period_id.id,
655 'journal_id': st_line.journal_id.id,
656 'partner_id': st_line.partner_id.id,
657 'company_id': st_line.company_id.id,
658 'statement_id': st_line.statement_id.id,
659 'debit': currency_diff < 0 and -currency_diff or 0,
660 'credit': currency_diff > 0 and currency_diff or 0,
661 'amount_currency': 0.0,
662 'date': st_line.date,
663 'account_id': account_id
666 def process_reconciliations(self, cr, uid, data, context=None):
668 self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
670 def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
671 """ 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.
673 :param int id: id of the bank statement line
674 :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
678 st_line = self.browse(cr, uid, id, context=context)
679 company_currency = st_line.journal_id.company_id.currency_id
680 statement_currency = st_line.journal_id.currency or company_currency
681 bs_obj = self.pool.get('account.bank.statement')
682 am_obj = self.pool.get('account.move')
683 aml_obj = self.pool.get('account.move.line')
684 currency_obj = self.pool.get('res.currency')
687 if st_line.journal_entry_id.id:
688 raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
689 for mv_line_dict in mv_line_dicts:
690 for field in ['debit', 'credit', 'amount_currency']:
691 if field not in mv_line_dict:
692 mv_line_dict[field] = 0.0
693 if mv_line_dict.get('counterpart_move_line_id'):
694 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
695 if mv_line.reconcile_id:
696 raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
699 move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
700 move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
701 move_id = am_obj.create(cr, uid, move_vals, context=context)
703 # Create the move line for the statement line
704 if st_line.statement_id.currency.id != company_currency.id:
705 if st_line.currency_id == company_currency:
706 amount = st_line.amount_currency
709 ctx['date'] = st_line.date
710 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
712 amount = st_line.amount
713 bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
714 aml_obj.create(cr, uid, bank_st_move_vals, context=context)
716 st_line_currency = st_line.currency_id or statement_currency
717 st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
719 for mv_line_dict in mv_line_dicts:
720 if mv_line_dict.get('is_tax_line'):
722 mv_line_dict['ref'] = move_name
723 mv_line_dict['move_id'] = move_id
724 mv_line_dict['period_id'] = st_line.statement_id.period_id.id
725 mv_line_dict['journal_id'] = st_line.journal_id.id
726 mv_line_dict['company_id'] = st_line.company_id.id
727 mv_line_dict['statement_id'] = st_line.statement_id.id
728 if mv_line_dict.get('counterpart_move_line_id'):
729 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
730 mv_line_dict['account_id'] = mv_line.account_id.id
731 if st_line_currency.id != company_currency.id:
733 ctx['date'] = st_line.date
734 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
735 mv_line_dict['currency_id'] = st_line_currency.id
736 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
737 debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
738 credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
739 elif st_line.currency_id and st_line_currency_rate:
740 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)
741 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)
743 debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
744 credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
745 if mv_line_dict.get('counterpart_move_line_id'):
746 #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
747 ctx['date'] = mv_line.date
748 debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
749 credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
750 mv_line_dict['credit'] = credit_at_old_rate
751 mv_line_dict['debit'] = debit_at_old_rate
752 if debit_at_old_rate - debit_at_current_rate:
753 currency_diff = debit_at_current_rate - debit_at_old_rate
754 to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
755 if credit_at_old_rate - credit_at_current_rate:
756 currency_diff = credit_at_current_rate - credit_at_old_rate
757 to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
759 mv_line_dict['debit'] = debit_at_current_rate
760 mv_line_dict['credit'] = credit_at_current_rate
761 elif statement_currency.id != company_currency.id:
762 #statement is in foreign currency but the transaction is in company currency
763 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
764 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
765 to_create.append(mv_line_dict)
767 move_line_pairs_to_reconcile = []
768 for mv_line_dict in to_create:
769 counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
770 if mv_line_dict.get('counterpart_move_line_id'):
771 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
772 del mv_line_dict['counterpart_move_line_id']
773 new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
774 if counterpart_move_line_id != None:
775 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
777 for pair in move_line_pairs_to_reconcile:
778 aml_obj.reconcile_partial(cr, uid, pair, context=context)
779 # Mark the statement line as reconciled
780 self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
782 # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
783 # return [('journal_entry_id', '=', False)]
784 # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
785 def _needaction_domain_get(self, cr, uid, context=None):
786 user = self.pool.get("res.users").browse(cr, uid, uid)
787 return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
789 _order = "statement_id desc, sequence"
790 _name = "account.bank.statement.line"
791 _description = "Bank Statement Line"
792 _inherit = ['ir.needaction_mixin']
794 'name': fields.char('Communication', required=True),
795 'date': fields.date('Date', required=True),
796 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
797 'partner_id': fields.many2one('res.partner', 'Partner'),
798 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
799 '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"),
800 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
801 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
802 '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)."),
803 'ref': fields.char('Reference'),
804 'note': fields.text('Notes'),
805 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
806 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
807 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
808 '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')),
809 'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
812 'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'),
813 'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
816 class account_statement_operation_template(osv.osv):
817 _name = "account.statement.operation.template"
818 _description = "Preset for the lines that can be created in a bank statement reconciliation"
820 'name': fields.char('Button Label', required=True),
821 'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','!=','view')]),
822 'label': fields.char('Label'),
823 'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
824 'Amount type', required=True),
825 '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),
826 'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'),
827 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
830 'amount_type': 'percentage_of_balance',
834 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: