[MERGE] forward port of branch 8.0 up to 2b192be
[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(self, 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(self, 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 _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
517         if excluded_ids is None:
518             excluded_ids = []
519         domain = [('ref', '=', st_line.name),
520                   ('reconcile_id', '=', False),
521                   ('state', '=', 'valid'),
522                   ('account_id.reconcile', '=', True),
523                   ('id', 'not in', excluded_ids)]
524         return domain
525
526     def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
527         """ Returns move lines that constitute the best guess to reconcile a statement line. """
528         mv_line_pool = self.pool.get('account.move.line')
529
530         # Look for structured communication
531         if st_line.name:
532             domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
533             match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
534             if match_id:
535                 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
536                 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
537                 mv_line = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, mv_line_br, target_currency=target_currency, target_date=st_line.date, context=context)[0]
538                 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
539                 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
540                 if (mv_line['partner_id']):
541                     self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
542                     mv_line['has_no_partner'] = False
543                 return [mv_line]
544
545         # If there is no identified partner or structured communication, don't look further
546         if not st_line.partner_id.id:
547             return []
548
549         # Look for a move line whose amount matches the statement line's amount
550         company_currency = st_line.journal_id.company_id.currency_id.id
551         statement_currency = st_line.journal_id.currency.id or company_currency
552         sign = 1
553         if statement_currency == company_currency:
554             amount_field = 'credit'
555             if st_line.amount > 0:
556                 amount_field = 'debit'
557             else:
558                 sign = -1
559         else:
560             amount_field = 'amount_currency'
561             if st_line.amount < 0:
562                 sign = -1
563         if st_line.amount_currency:
564             amount = st_line.amount_currency
565         else:
566             amount = st_line.amount
567
568         match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=[(amount_field, '=', (sign * amount))])
569         if match_id:
570             return [match_id[0]]
571
572         return []
573
574     def get_move_lines_for_reconciliation_by_statement_line_id(self, cr, uid, st_line_id, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None):
575         """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
576         if excluded_ids is None:
577             excluded_ids = []
578         if additional_domain is None:
579             additional_domain = []
580         st_line = self.browse(cr, uid, st_line_id, context=context)
581         return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
582
583     def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
584         if excluded_ids is None:
585             excluded_ids = []
586         if additional_domain is None:
587             additional_domain = []
588         # Make domain
589         domain = additional_domain + [('reconcile_id', '=', False),
590                                       ('state', '=', 'valid'),
591                                       ('account_id.reconcile', '=', True)]
592         if st_line.partner_id.id:
593             domain += [('partner_id', '=', st_line.partner_id.id)]
594         if excluded_ids:
595             domain.append(('id', 'not in', excluded_ids))
596         if str:
597             domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
598             if not st_line.partner_id.id:
599                 domain.insert(-1, '|', )
600                 domain.append(('partner_id.name', 'ilike', str))
601             if str != '/':
602                 domain.insert(-1, '|', )
603                 domain.append(('name', 'ilike', str))
604         return domain
605
606     def get_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, offset=0, limit=None, count=False, additional_domain=None, context=None):
607         """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
608
609             :param st_line: the browse record of the statement line
610             :param integers list excluded_ids: ids of move lines that should not be fetched
611             :param boolean count: just return the number of records
612             :param tuples list additional_domain: additional domain restrictions
613         """
614         mv_line_pool = self.pool.get('account.move.line')
615         domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
616         
617         # Get move lines ; in case of a partial reconciliation, only consider one line
618         filtered_lines = []
619         reconcile_partial_ids = []
620         actual_offset = offset
621         while True:
622             line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
623             lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
624             make_one_more_loop = False
625             for line in lines:
626                 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
627                     #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
628                     #in order to get the right number of items in the pager
629                     make_one_more_loop = True
630                     continue
631                 filtered_lines.append(line)
632                 if line.reconcile_partial_id:
633                     reconcile_partial_ids.append(line.reconcile_partial_id.id)
634
635             if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
636                 break
637             actual_offset = actual_offset + limit
638         lines = limit and filtered_lines[:limit] or filtered_lines
639
640         # Either return number of lines
641         if count:
642             return len(lines)
643
644         # Or return list of dicts representing the formatted move lines
645         else:
646             target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
647             mv_lines = mv_line_pool.prepare_move_lines_for_reconciliation_widget(cr, uid, lines, target_currency=target_currency, target_date=st_line.date, context=context)
648             has_no_partner = not bool(st_line.partner_id.id)
649             for line in mv_lines:
650                 line['has_no_partner'] = has_no_partner
651             return mv_lines
652
653     def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
654         if currency_diff < 0:
655             account_id = st_line.company_id.expense_currency_exchange_account_id.id
656             if not account_id:
657                 raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
658         else:
659             account_id = st_line.company_id.income_currency_exchange_account_id.id
660             if not account_id:
661                 raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
662         return {
663             'move_id': move_id,
664             'name': _('change') + ': ' + (st_line.name or '/'),
665             'period_id': st_line.statement_id.period_id.id,
666             'journal_id': st_line.journal_id.id,
667             'partner_id': st_line.partner_id.id,
668             'company_id': st_line.company_id.id,
669             'statement_id': st_line.statement_id.id,
670             'debit': currency_diff < 0 and -currency_diff or 0,
671             'credit': currency_diff > 0 and currency_diff or 0,
672             'amount_currency': 0.0,
673             'date': st_line.date,
674             'account_id': account_id
675             }
676
677     def process_reconciliations(self, cr, uid, data, context=None):
678         for datum in data:
679             self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
680
681     def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
682         """ Creates a move line for each item of mv_line_dicts and for the statement line. Reconcile a new move line with its counterpart_move_line_id if specified. Finally, mark the statement line as reconciled by putting the newly created move id in the column journal_entry_id.
683
684             :param int id: id of the bank statement line
685             :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
686         """
687         if context is None:
688             context = {}
689         st_line = self.browse(cr, uid, id, context=context)
690         company_currency = st_line.journal_id.company_id.currency_id
691         statement_currency = st_line.journal_id.currency or company_currency
692         bs_obj = self.pool.get('account.bank.statement')
693         am_obj = self.pool.get('account.move')
694         aml_obj = self.pool.get('account.move.line')
695         currency_obj = self.pool.get('res.currency')
696
697         # Checks
698         if st_line.journal_entry_id.id:
699             raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
700         for mv_line_dict in mv_line_dicts:
701             for field in ['debit', 'credit', 'amount_currency']:
702                 if field not in mv_line_dict:
703                     mv_line_dict[field] = 0.0
704             if mv_line_dict.get('counterpart_move_line_id'):
705                 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
706                 if mv_line.reconcile_id:
707                     raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
708
709         # Create the move
710         move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
711         move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
712         move_id = am_obj.create(cr, uid, move_vals, context=context)
713
714         # Create the move line for the statement line
715         if st_line.statement_id.currency.id != company_currency.id:
716             if st_line.currency_id == company_currency:
717                 amount = st_line.amount_currency
718             else:
719                 ctx = context.copy()
720                 ctx['date'] = st_line.date
721                 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
722         else:
723             amount = st_line.amount
724         bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
725         aml_obj.create(cr, uid, bank_st_move_vals, context=context)
726         # Complete the dicts
727         st_line_currency = st_line.currency_id or statement_currency
728         st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
729         to_create = []
730         for mv_line_dict in mv_line_dicts:
731             if mv_line_dict.get('is_tax_line'):
732                 continue
733             mv_line_dict['ref'] = move_name
734             mv_line_dict['move_id'] = move_id
735             mv_line_dict['period_id'] = st_line.statement_id.period_id.id
736             mv_line_dict['journal_id'] = st_line.journal_id.id
737             mv_line_dict['company_id'] = st_line.company_id.id
738             mv_line_dict['statement_id'] = st_line.statement_id.id
739             if mv_line_dict.get('counterpart_move_line_id'):
740                 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
741                 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
742                 mv_line_dict['account_id'] = mv_line.account_id.id
743             if st_line_currency.id != company_currency.id:
744                 ctx = context.copy()
745                 ctx['date'] = st_line.date
746                 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
747                 mv_line_dict['currency_id'] = st_line_currency.id
748                 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
749                     debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
750                     credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
751                 elif st_line.currency_id and st_line_currency_rate:
752                     debit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit'] / st_line_currency_rate, context=ctx)
753                     credit_at_current_rate = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit'] / st_line_currency_rate, context=ctx)
754                 else:
755                     debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
756                     credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
757                 if mv_line_dict.get('counterpart_move_line_id'):
758                     #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
759                     ctx['date'] = mv_line.date
760                     debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
761                     credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
762                     mv_line_dict['credit'] = credit_at_old_rate
763                     mv_line_dict['debit'] = debit_at_old_rate
764                     if debit_at_old_rate - debit_at_current_rate:
765                         currency_diff = debit_at_current_rate - debit_at_old_rate
766                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
767                     if credit_at_old_rate - credit_at_current_rate:
768                         currency_diff = credit_at_current_rate - credit_at_old_rate
769                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
770                 else:
771                     mv_line_dict['debit'] = debit_at_current_rate
772                     mv_line_dict['credit'] = credit_at_current_rate
773             elif statement_currency.id != company_currency.id:
774                 #statement is in foreign currency but the transaction is in company currency
775                 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
776                 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
777             to_create.append(mv_line_dict)
778         # Create move lines
779         move_line_pairs_to_reconcile = []
780         for mv_line_dict in to_create:
781             counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
782             if mv_line_dict.get('counterpart_move_line_id'):
783                 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
784                 del mv_line_dict['counterpart_move_line_id']
785             new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
786             if counterpart_move_line_id != None:
787                 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
788         # Reconcile
789         for pair in move_line_pairs_to_reconcile:
790             aml_obj.reconcile_partial(cr, uid, pair, context=context)
791         # Mark the statement line as reconciled
792         self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
793
794     # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
795     # return [('journal_entry_id', '=', False)]
796     # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
797     def _needaction_domain_get(self, cr, uid, context=None):
798         user = self.pool.get("res.users").browse(cr, uid, uid)
799         return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
800
801     _order = "statement_id desc, sequence"
802     _name = "account.bank.statement.line"
803     _description = "Bank Statement Line"
804     _inherit = ['ir.needaction_mixin']
805     _columns = {
806         'name': fields.char('Communication', required=True),
807         'date': fields.date('Date', required=True),
808         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
809         'partner_id': fields.many2one('res.partner', 'Partner'),
810         'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
811         'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"),
812         'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
813         'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
814         'partner_name': fields.char('Partner Name', help="This field is used to record the third party name when importing bank statement in electronic format, when the partner doesn't exist yet in the database (or cannot be found)."),
815         'ref': fields.char('Reference'),
816         'note': fields.text('Notes'),
817         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
818         'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
819         'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
820         'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
821         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
822     }
823     _defaults = {
824         'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
825     }
826
827 class account_statement_operation_template(osv.osv):
828     _name = "account.statement.operation.template"
829     _description = "Preset for the lines that can be created in a bank statement reconciliation"
830     _columns = {
831         'name': fields.char('Button Label', required=True),
832         'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','!=','view')]),
833         'label': fields.char('Label'),
834         'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
835                                    'Amount type', required=True),
836         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), help="The amount will count as a debit if it is negative, as a credit if it is positive (except if amount type is 'Percentage of open balance').", required=True),
837         'tax_id': fields.many2one('account.tax', 'Tax', ondelete='cascade'),
838         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
839     }
840     _defaults = {
841         'amount_type': 'percentage_of_balance',
842         'amount': 100.0
843     }
844
845 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: