[FIX] account: bank statement reconciliation: when creating a new move line to reconc...
[odoo/odoo.git] / addons / account / account_bank_statement.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 import openerp.addons.decimal_precision as dp
25 from openerp.report import report_sxw
26
27 import time
28
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)
38
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)
45         return res
46
47     def _default_journal_id(self, cr, uid, context=None):
48         if context is None:
49             context = {}
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)
53         if journal_type:
54             ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
55             if ids:
56                 return ids[0]
57         return False
58
59     def _end_balance(self, cursor, user, ids, name, attr, context=None):
60         res = {}
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
65         return res
66
67     def _get_period(self, cr, uid, context=None):
68         periods = self.pool.get('account.period').find(cr, uid, context=context)
69         if periods:
70             return periods[0]
71         return False
72
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)
80
81     def _currency(self, cursor, user, ids, name, args, context=None):
82         res = {}
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
89             if not currency:
90                 currency = default_currency
91             res[statement.id] = currency.id
92         currency_names = {}
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])
99         return res
100
101     def _get_statement(self, cr, uid, ids, context=None):
102         result = {}
103         for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
104             result[line.statement_id.id] = True
105         return result.keys()
106
107     def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None):
108         res = {}
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])
111         return res
112
113     _order = "date desc, id desc"
114     _name = "account.bank.statement"
115     _description = "Bank Statement"
116     _inherit = ['mail.thread']
117     _columns = {
118         'name': fields.char(
119             'Reference', states={'draft': [('readonly', False)]},
120             readonly=True, # readonly for account_cash_statement
121             copy=False,
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 '
125                  'statement itself'),
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,
137             store = {
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),
140             },
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",
152                                    copy=False,
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'),
160     }
161
162     _defaults = {
163         'name': '/', 
164         'date': fields.date.context_today,
165         'state': 'draft',
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),
169     }
170
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:
174                 return False
175         return True
176
177     _constraints = [
178         (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
179     ]
180
181     def onchange_date(self, cr, uid, ids, date, company_id, context=None):
182         """
183             Find the correct period to use for the given date and company_id, return it and set it in the context
184         """
185         res = {}
186         period_pool = self.pool.get('account.period')
187
188         if context is None:
189             context = {}
190         ctx = context.copy()
191         ctx.update({'company_id': company_id})
192         pids = period_pool.find(cr, uid, dt=date, context=ctx)
193         if pids:
194             res.update({'period_id': pids[0]})
195             context = dict(context, period_id=pids[0])
196
197         return {
198             'value':res,
199             'context':context,
200         }
201
202     def button_dummy(self, cr, uid, ids, context=None):
203         return self.write(cr, uid, ids, {}, context=context)
204
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).
210
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
215         """
216         return {
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,
221             'ref': st_line.ref,
222         }
223
224     def _get_counter_part_account(sefl, cr, uid, st_line, context=None):
225         """Retrieve the account to use in the counterpart move.
226
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
229         """
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
233
234     def _get_counter_part_partner(sefl, cr, uid, st_line, context=None):
235         """Retrieve the partner to use in the counterpart move.
236
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
239         """
240         return st_line.partner_id and st_line.partner_id.id or False
241
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. 
245
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
251         """
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
256         cur_id = False
257         amt_cur = False
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)
267
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
271            statement line.
272
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
284         """
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)
288         return {
289             'name': st_line.name,
290             'date': st_line.date,
291             'ref': st_line.ref,
292             'move_id': move_id,
293             'partner_id': par_id,
294             'account_id': acc_id,
295             'credit': credit,
296             'debit': debit,
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,
302         }
303
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))
309         return True
310
311     def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
312         return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
313
314     def check_status_condition(self, cr, uid, state, journal_type='bank'):
315         return state in ('draft','open')
316
317     def button_confirm_bank(self, cr, uid, ids, context=None):
318         if context is None:
319             context = {}
320
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):
324                 continue
325
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.'))
333             move_ids = []
334             for st_line in st.line_ids:
335                 if not st_line.amount:
336                     continue
337                 if st_line.account_id and not st_line.journal_entry_id.id:
338                     #make an account move as before
339                     vals = {
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,
343                         'name': st_line.name
344                     }
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)
349             if move_ids:
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)
354
355     def button_cancel(self, cr, uid, ids, context=None):
356         bnk_st_line_ids = []
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)
361
362     def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
363         res = False
364         if journal_id:
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'))
371                 res = cr.fetchone()
372         return res and res[0] or 0.0
373
374     def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
375         if not journal_id:
376             return {}
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}
384
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.')
391                 )
392         return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
393
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
397         return {
398             'name': _('Journal Items'),
399             'view_type':'form',
400             'view_mode':'tree',
401             'res_model':'account.move.line',
402             'view_id':False,
403             'type':'ir.actions.act_window',
404             'domain':[('statement_id','in',ids)],
405             'context':ctx,
406         }
407
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)
411
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)
417
418 class account_bank_statement_line(osv.osv):
419
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)
424
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.')
431                 )
432         return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
433
434     def cancel(self, cr, uid, ids, context=None):
435         account_move_obj = self.pool.get('account.move')
436         move_ids = []
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:
441                     if aml.reconcile_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)
447         if move_ids:
448             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
449             account_move_obj.unlink(cr, uid, move_ids, context)
450
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 """
453         ret = []
454         if excluded_ids is None:
455             excluded_ids = []
456
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
464             else:
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)
469
470         return ret
471
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 """
474         if context is None:
475             context = {}
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)
478
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)
484         else:
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)
489
490         data = {
491             'id': st_line.id,
492             'ref': st_line.ref,
493             'note': st_line.note or "",
494             'name': st_line.name,
495             'date': st_line.date,
496             'amount': amount,
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,
507         }
508         if st_line.partner_id.id:
509             if amount > 0:
510                 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
511             else:
512                 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
513
514         return data
515
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:
519             excluded_ids = []
520         mv_line_pool = self.pool.get('account.move.line')
521
522         # Look for structured communication
523         if st_line.name:
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)
526             if match_id:
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
535                 return [mv_line]
536
537         # If there is no identified partner or structured communication, don't look further
538         if not st_line.partner_id.id:
539             return []
540
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
544         sign = 1
545         if statement_currency == company_currency:
546             amount_field = 'credit'
547             if st_line.amount > 0:
548                 amount_field = 'debit'
549             else:
550                 sign = -1
551         else:
552             amount_field = 'amount_currency'
553             if st_line.amount < 0:
554                 sign = -1
555         if st_line.amount_currency:
556             amount = st_line.amount_currency
557         else:
558             amount = st_line.amount
559
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))])
561         if match_id:
562             return [match_id[0]]
563
564         return []
565
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:
569             excluded_ids = []
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)
574
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.
577
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
582         """
583         if excluded_ids is None:
584             excluded_ids = []
585         if additional_domain is None:
586             additional_domain = []
587         mv_line_pool = self.pool.get('account.move.line')
588
589         # Make domain
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)]
593         if excluded_ids:
594             domain.append(('id', 'not in', excluded_ids))
595         if str:
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))
600             if str != '/':
601                 domain.insert(-1, '|', )
602                 domain.append(('name', 'ilike', str))
603
604         # Get move lines ; in case of a partial reconciliation, only consider one line
605         filtered_lines = []
606         reconcile_partial_ids = []
607         actual_offset = offset
608         while True:
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
612             for line in lines:
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
617                     continue
618                 filtered_lines.append(line)
619                 if line.reconcile_partial_id:
620                     reconcile_partial_ids.append(line.reconcile_partial_id.id)
621
622             if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
623                 break
624             actual_offset = actual_offset + limit
625         lines = limit and filtered_lines[:limit] or filtered_lines
626
627         # Either return number of lines
628         if count:
629             return len(lines)
630
631         # Or return list of dicts representing the formatted move lines
632         else:
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
638             return mv_lines
639
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
643             if not account_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."))
645         else:
646             account_id = st_line.company_id.income_currency_exchange_account_id.id
647             if not account_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."))
649         return {
650             'move_id': move_id,
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
662             }
663
664     def process_reconciliations(self, cr, uid, data, context=None):
665         for datum in data:
666             self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
667
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.
670
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
673         """
674         if context is None:
675             context = {}
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')
683
684         # Checks
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.'))
695
696         # Create the move
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)
700
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
705             else:
706                 ctx = context.copy()
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)
709         else:
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)
713         # Complete the dicts
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
716         to_create = []
717         for mv_line_dict in mv_line_dicts:
718             if mv_line_dict.get('is_tax_line'):
719                 continue
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:
731                 ctx = context.copy()
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)
741                 else:
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))
757                 else:
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)
765         # Create move lines
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])
775         # Reconcile
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)
780
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)]
787
788     _order = "statement_id desc, sequence"
789     _name = "account.bank.statement.line"
790     _description = "Bank Statement Line"
791     _inherit = ['ir.needaction_mixin']
792     _columns = {
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."),
809     }
810     _defaults = {
811         'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'),
812         'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
813     }
814
815 class account_statement_operation_template(osv.osv):
816     _name = "account.statement.operation.template"
817     _description = "Preset for the lines that can be created in a bank statement reconciliation"
818     _columns = {
819         'name': fields.char('Button Label', required=True),
820         'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','!=','view')]),
821         'label': fields.char('Label'),
822         'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
823                                    'Amount type', required=True),
824         '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),
825         'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'),
826         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
827     }
828     _defaults = {
829         'amount_type': 'percentage_of_balance',
830         'amount': 100.0
831     }
832
833 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: