[FIX] fields: inherited fields get their attribute 'state' from their base field
[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 from openerp.tools import float_compare, float_round
27
28 import time
29
30 class account_bank_statement(osv.osv):
31     def create(self, cr, uid, vals, context=None):
32         if vals.get('name', '/') == '/':
33             journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context))
34             vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context)
35         if 'line_ids' in vals:
36             for idx, line in enumerate(vals['line_ids']):
37                 line[2]['sequence'] = idx + 1
38         return super(account_bank_statement, self).create(cr, uid, vals, context=context)
39
40     def write(self, cr, uid, ids, vals, context=None):
41         res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
42         account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
43         for statement in self.browse(cr, uid, ids, context):
44             for idx, line in enumerate(statement.line_ids):
45                 account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context)
46         return res
47
48     def _default_journal_id(self, cr, uid, context=None):
49         if context is None:
50             context = {}
51         journal_pool = self.pool.get('account.journal')
52         journal_type = context.get('journal_type', False)
53         company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context)
54         if journal_type:
55             ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
56             if ids:
57                 return ids[0]
58         return False
59
60     def _end_balance(self, cursor, user, ids, name, attr, context=None):
61         res = {}
62         for statement in self.browse(cursor, user, ids, context=context):
63             res[statement.id] = statement.balance_start
64             for line in statement.line_ids:
65                 res[statement.id] += line.amount
66         return res
67
68     def _get_period(self, cr, uid, context=None):
69         periods = self.pool.get('account.period').find(cr, uid, context=context)
70         if periods:
71             return periods[0]
72         return False
73
74     def _compute_default_statement_name(self, cr, uid, journal_id, context=None):
75         context = dict(context or {})
76         obj_seq = self.pool.get('ir.sequence')
77         period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context)
78         context['fiscalyear_id'] = period.fiscalyear_id.id
79         journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None)
80         return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context)
81
82     def _currency(self, cursor, user, ids, name, args, context=None):
83         res = {}
84         res_currency_obj = self.pool.get('res.currency')
85         res_users_obj = self.pool.get('res.users')
86         default_currency = res_users_obj.browse(cursor, user,
87                 user, context=context).company_id.currency_id
88         for statement in self.browse(cursor, user, ids, context=context):
89             currency = statement.journal_id.currency
90             if not currency:
91                 currency = default_currency
92             res[statement.id] = currency.id
93         currency_names = {}
94         for currency_id, currency_name in res_currency_obj.name_get(cursor,
95                 user, [x for x in res.values()], context=context):
96             currency_names[currency_id] = currency_name
97         for statement_id in res.keys():
98             currency_id = res[statement_id]
99             res[statement_id] = (currency_id, currency_names[currency_id])
100         return res
101
102     def _get_statement(self, cr, uid, ids, context=None):
103         result = {}
104         for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
105             result[line.statement_id.id] = True
106         return result.keys()
107
108     def _all_lines_reconciled(self, cr, uid, ids, name, args, context=None):
109         res = {}
110         for statement in self.browse(cr, uid, ids, context=context):
111             res[statement.id] = all([line.journal_entry_id.id for line in statement.line_ids])
112         return res
113
114     _order = "date desc, id desc"
115     _name = "account.bank.statement"
116     _description = "Bank Statement"
117     _inherit = ['mail.thread']
118     _columns = {
119         'name': fields.char(
120             'Reference', states={'draft': [('readonly', False)]},
121             readonly=True, # readonly for account_cash_statement
122             copy=False,
123             help='if you give the Name other then /, its created Accounting Entries Move '
124                  'will be with same name as statement name. '
125                  'This allows the statement entries to have the same references than the '
126                  'statement itself'),
127         'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]},
128                             select=True, copy=False),
129         'journal_id': fields.many2one('account.journal', 'Journal', required=True,
130             readonly=True, states={'draft':[('readonly',False)]}),
131         'period_id': fields.many2one('account.period', 'Period', required=True,
132             states={'confirm':[('readonly', True)]}),
133         'balance_start': fields.float('Starting Balance', digits_compute=dp.get_precision('Account'),
134             states={'confirm':[('readonly',True)]}),
135         'balance_end_real': fields.float('Ending Balance', digits_compute=dp.get_precision('Account'),
136             states={'confirm': [('readonly', True)]}, help="Computed using the cash control lines"),
137         'balance_end': fields.function(_end_balance,
138             store = {
139                 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10),
140                 'account.bank.statement.line': (_get_statement, ['amount'], 10),
141             },
142             string="Computed Balance", help='Balance as calculated based on Opening Balance and transaction lines'),
143         'company_id': fields.related('journal_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
144         'line_ids': fields.one2many('account.bank.statement.line',
145                                     'statement_id', 'Statement lines',
146                                     states={'confirm':[('readonly', True)]}, copy=True),
147         'move_line_ids': fields.one2many('account.move.line', 'statement_id',
148                                          'Entry lines', states={'confirm':[('readonly',True)]}),
149         'state': fields.selection([('draft', 'New'),
150                                    ('open','Open'), # used by cash statements
151                                    ('confirm', 'Closed')],
152                                    'Status', required=True, readonly="1",
153                                    copy=False,
154                                    help='When new statement is created the status will be \'Draft\'.\n'
155                                         'And after getting confirmation from the bank it will be in \'Confirmed\' status.'),
156         'currency': fields.function(_currency, string='Currency',
157             type='many2one', relation='res.currency'),
158         '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.'),
159         'cash_control': fields.related('journal_id', 'cash_control' , type='boolean', relation='account.journal',string='Cash control'),
160         'all_lines_reconciled': fields.function(_all_lines_reconciled, string='All lines reconciled', type='boolean'),
161     }
162
163     _defaults = {
164         'name': '/', 
165         'date': fields.date.context_today,
166         'state': 'draft',
167         'journal_id': _default_journal_id,
168         'period_id': _get_period,
169         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=c),
170     }
171
172     def _check_company_id(self, cr, uid, ids, context=None):
173         for statement in self.browse(cr, uid, ids, context=context):
174             if statement.company_id.id != statement.period_id.company_id.id:
175                 return False
176         return True
177
178     _constraints = [
179         (_check_company_id, 'The journal and period chosen have to belong to the same company.', ['journal_id','period_id']),
180     ]
181
182     def onchange_date(self, cr, uid, ids, date, company_id, context=None):
183         """
184             Find the correct period to use for the given date and company_id, return it and set it in the context
185         """
186         res = {}
187         period_pool = self.pool.get('account.period')
188
189         if context is None:
190             context = {}
191         ctx = context.copy()
192         ctx.update({'company_id': company_id})
193         pids = period_pool.find(cr, uid, dt=date, context=ctx)
194         if pids:
195             res.update({'period_id': pids[0]})
196             context = dict(context, period_id=pids[0])
197
198         return {
199             'value':res,
200             'context':context,
201         }
202
203     def button_dummy(self, cr, uid, ids, context=None):
204         return self.write(cr, uid, ids, {}, context=context)
205
206     def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
207         """Prepare the dict of values to create the move from a
208            statement line. This method may be overridden to implement custom
209            move generation (making sure to call super() to establish
210            a clean extension chain).
211
212            :param browse_record st_line: account.bank.statement.line record to
213                   create the move from.
214            :param char st_line_number: will be used as the name of the generated account move
215            :return: dict of value to create() the account.move
216         """
217         return {
218             'journal_id': st_line.statement_id.journal_id.id,
219             'period_id': st_line.statement_id.period_id.id,
220             'date': st_line.date,
221             'name': st_line_number,
222             'ref': st_line.ref,
223         }
224
225     def _get_counter_part_account(self, cr, uid, st_line, context=None):
226         """Retrieve the account to use in the counterpart move.
227
228            :param browse_record st_line: account.bank.statement.line record to create the move from.
229            :return: int/long of the account.account to use as counterpart
230         """
231         if st_line.amount >= 0:
232             return st_line.statement_id.journal_id.default_credit_account_id.id
233         return st_line.statement_id.journal_id.default_debit_account_id.id
234
235     def _get_counter_part_partner(self, cr, uid, st_line, context=None):
236         """Retrieve the partner to use in the counterpart move.
237
238            :param browse_record st_line: account.bank.statement.line record to create the move from.
239            :return: int/long of the res.partner to use as counterpart
240         """
241         return st_line.partner_id and st_line.partner_id.id or False
242
243     def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id, context=None):
244         """Compute the args to build the dict of values to create the counter part move line from a
245            statement line by calling the _prepare_move_line_vals. 
246
247            :param browse_record st_line: account.bank.statement.line record to create the move from.
248            :param int/long move_id: ID of the account.move to link the move line
249            :param float amount: amount of the move line
250            :param int/long company_currency_id: ID of currency of the concerned company
251            :return: dict of value to create() the bank account.move.line
252         """
253         account_id = self._get_counter_part_account(cr, uid, st_line, context=context)
254         partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context)
255         debit = ((amount > 0) and amount) or 0.0
256         credit = ((amount < 0) and -amount) or 0.0
257         cur_id = False
258         amt_cur = False
259         if st_line.statement_id.currency.id != company_currency_id:
260             amt_cur = st_line.amount
261             cur_id = st_line.statement_id.currency.id
262         elif st_line.currency_id and st_line.amount_currency:
263             amt_cur = st_line.amount_currency
264             cur_id = st_line.currency_id.id
265         return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
266             amount_currency=amt_cur, currency_id=cur_id, account_id=account_id,
267             partner_id=partner_id, context=context)
268
269     def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
270                 amount_currency=False, account_id=False, partner_id=False, context=None):
271         """Prepare the dict of values to create the move line from a
272            statement line.
273
274            :param browse_record st_line: account.bank.statement.line record to
275                   create the move from.
276            :param int/long move_id: ID of the account.move to link the move line
277            :param float debit: debit amount of the move line
278            :param float credit: credit amount of the move line
279            :param int/long currency_id: ID of currency of the move line to create
280            :param float amount_currency: amount of the debit/credit expressed in the currency_id
281            :param int/long account_id: ID of the account to use in the move line if different
282                   from the statement line account ID
283            :param int/long partner_id: ID of the partner to put on the move line
284            :return: dict of value to create() the account.move.line
285         """
286         acc_id = account_id or st_line.account_id.id
287         cur_id = currency_id or st_line.statement_id.currency.id
288         par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False)
289         return {
290             'name': st_line.name,
291             'date': st_line.date,
292             'ref': st_line.ref,
293             'move_id': move_id,
294             'partner_id': par_id,
295             'account_id': acc_id,
296             'credit': credit,
297             'debit': debit,
298             'statement_id': st_line.statement_id.id,
299             'journal_id': st_line.statement_id.journal_id.id,
300             'period_id': st_line.statement_id.period_id.id,
301             'currency_id': amount_currency and cur_id,
302             'amount_currency': amount_currency,
303         }
304
305     def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
306         st = self.browse(cr, uid, st_id, context=context)
307         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)):
308             raise osv.except_osv(_('Error!'),
309                     _('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end))
310         return True
311
312     def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
313         return self.write(cr, uid, ids, {'state':'confirm'}, context=context)
314
315     def check_status_condition(self, cr, uid, state, journal_type='bank'):
316         return state in ('draft','open')
317
318     def button_confirm_bank(self, cr, uid, ids, context=None):
319         if context is None:
320             context = {}
321
322         for st in self.browse(cr, uid, ids, context=context):
323             j_type = st.journal_id.type
324             if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
325                 continue
326
327             self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
328             if (not st.journal_id.default_credit_account_id) \
329                     or (not st.journal_id.default_debit_account_id):
330                 raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
331             for line in st.move_line_ids:
332                 if line.state != 'valid':
333                     raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
334             move_ids = []
335             for st_line in st.line_ids:
336                 if not st_line.amount:
337                     continue
338                 if st_line.account_id and not st_line.journal_entry_id.id:
339                     #make an account move as before
340                     vals = {
341                         'debit': st_line.amount < 0 and -st_line.amount or 0.0,
342                         'credit': st_line.amount > 0 and st_line.amount or 0.0,
343                         'account_id': st_line.account_id.id,
344                         'name': st_line.name
345                     }
346                     self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
347                 elif not st_line.journal_entry_id.id:
348                     raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
349                 move_ids.append(st_line.journal_entry_id.id)
350             if move_ids:
351                 self.pool.get('account.move').post(cr, uid, move_ids, context=context)
352             self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
353         self.link_bank_to_partner(cr, uid, ids, context=context)
354         return self.write(cr, uid, ids, {'state': 'confirm', 'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
355
356     def button_cancel(self, cr, uid, ids, context=None):
357         bnk_st_line_ids = []
358         for st in self.browse(cr, uid, ids, context=context):
359             bnk_st_line_ids += [line.id for line in st.line_ids]
360         self.pool.get('account.bank.statement.line').cancel(cr, uid, bnk_st_line_ids, context=context)
361         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
362
363     def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
364         res = False
365         if journal_id:
366             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
367             if journal.with_last_closing_balance:
368                 cr.execute('SELECT balance_end_real \
369                       FROM account_bank_statement \
370                       WHERE journal_id = %s AND NOT state = %s \
371                       ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft'))
372                 res = cr.fetchone()
373         return res and res[0] or 0.0
374
375     def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
376         if not journal_id:
377             return {}
378         balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context)
379         journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
380         currency = journal.currency or journal.company_id.currency_id
381         res = {'balance_start': balance_start, 'company_id': journal.company_id.id, 'currency': currency.id}
382         if journal.type == 'cash':
383             res['cash_control'] = journal.cash_control
384         return {'value': res}
385
386     def unlink(self, cr, uid, ids, context=None):
387         statement_line_obj = self.pool['account.bank.statement.line']
388         for item in self.browse(cr, uid, ids, context=context):
389             if item.state != 'draft':
390                 raise osv.except_osv(
391                     _('Invalid Action!'), 
392                     _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
393                 )
394             # Explicitly unlink bank statement lines
395             # so it will check that the related journal entries have
396             # been deleted first
397             statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
398         return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
399
400     def button_journal_entries(self, cr, uid, ids, context=None):
401         ctx = (context or {}).copy()
402         ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
403         return {
404             'name': _('Journal Items'),
405             'view_type':'form',
406             'view_mode':'tree',
407             'res_model':'account.move.line',
408             'view_id':False,
409             'type':'ir.actions.act_window',
410             'domain':[('statement_id','in',ids)],
411             'context':ctx,
412         }
413
414     def number_of_lines_reconciled(self, cr, uid, ids, context=None):
415         bsl_obj = self.pool.get('account.bank.statement.line')
416         return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
417
418     def link_bank_to_partner(self, cr, uid, ids, context=None):
419         for statement in self.browse(cr, uid, ids, context=context):
420             for st_line in statement.line_ids:
421                 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:
422                     self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context)
423
424 class account_bank_statement_line(osv.osv):
425
426     def create(self, cr, uid, vals, context=None):
427         if vals.get('amount_currency', 0) and not vals.get('amount', 0):
428             raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
429         return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
430
431     def unlink(self, cr, uid, ids, context=None):
432         for item in self.browse(cr, uid, ids, context=context):
433             if item.journal_entry_id:
434                 raise osv.except_osv(
435                     _('Invalid Action!'), 
436                     _('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
437                 )
438         return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
439
440     def cancel(self, cr, uid, ids, context=None):
441         account_move_obj = self.pool.get('account.move')
442         move_ids = []
443         for line in self.browse(cr, uid, ids, context=context):
444             if line.journal_entry_id:
445                 move_ids.append(line.journal_entry_id.id)
446                 for aml in line.journal_entry_id.line_id:
447                     if aml.reconcile_id:
448                         move_lines = [l.id for l in aml.reconcile_id.line_id]
449                         move_lines.remove(aml.id)
450                         self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
451                         if len(move_lines) >= 2:
452                             self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
453         if move_ids:
454             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
455             account_move_obj.unlink(cr, uid, move_ids, context)
456
457     def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
458         """ Returns the data required to display a reconciliation, for each statement line id in ids """
459         ret = []
460         if excluded_ids is None:
461             excluded_ids = []
462
463         for st_line in self.browse(cr, uid, ids, context=context):
464             reconciliation_data = {}
465             if search_reconciliation_proposition:
466                 reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
467                 for mv_line in reconciliation_proposition:
468                     excluded_ids.append(mv_line['id'])
469                 reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
470             else:
471                 reconciliation_data['reconciliation_proposition'] = []
472             st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
473             reconciliation_data['st_line'] = st_line
474             ret.append(reconciliation_data)
475
476         return ret
477
478     def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
479         """ Returns the data required by the bank statement reconciliation widget to display a statement line """
480         if context is None:
481             context = {}
482         statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
483         rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
484
485         if st_line.amount_currency and st_line.currency_id:
486             amount = st_line.amount_currency
487             amount_currency = st_line.amount
488             amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
489             amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
490         else:
491             amount = st_line.amount
492             amount_currency_str = ""
493         amount_str = amount > 0 and amount or -amount
494         amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
495
496         data = {
497             'id': st_line.id,
498             'ref': st_line.ref,
499             'note': st_line.note or "",
500             'name': st_line.name,
501             'date': st_line.date,
502             'amount': amount,
503             'amount_str': amount_str, # Amount in the statement line currency
504             'currency_id': st_line.currency_id.id or statement_currency.id,
505             'partner_id': st_line.partner_id.id,
506             'statement_id': st_line.statement_id.id,
507             'account_code': st_line.journal_id.default_debit_account_id.code,
508             'account_name': st_line.journal_id.default_debit_account_id.name,
509             'partner_name': st_line.partner_id.name,
510             'communication_partner_name': st_line.partner_name,
511             'amount_currency_str': amount_currency_str, # Amount in the statement currency
512             'has_no_partner': not st_line.partner_id.id,
513         }
514         if st_line.partner_id.id:
515             if amount > 0:
516                 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
517             else:
518                 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
519
520         return data
521
522     def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
523         if excluded_ids is None:
524             excluded_ids = []
525         domain = [('ref', '=', st_line.name),
526                   ('reconcile_id', '=', False),
527                   ('state', '=', 'valid'),
528                   ('account_id.reconcile', '=', True),
529                   ('id', 'not in', excluded_ids)]
530         return domain
531
532     def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
533         """ Returns move lines that constitute the best guess to reconcile a statement line. """
534         mv_line_pool = self.pool.get('account.move.line')
535
536         # Look for structured communication
537         if st_line.name:
538             domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
539             match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
540             if match_id:
541                 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
542                 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
543                 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]
544                 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
545                 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
546                 if (mv_line['partner_id']):
547                     self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
548                     mv_line['has_no_partner'] = False
549                 return [mv_line]
550
551         # How to compare statement line amount and move lines amount
552         precision_digits = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
553         currency_id = st_line.currency_id.id or st_line.journal_id.currency.id
554         # NB : amount can't be == 0 ; so float precision is not an issue for amount > 0 or amount < 0
555         amount = st_line.amount_currency or st_line.amount
556         domain = [('reconcile_partial_id', '=', False)]
557         if currency_id:
558             domain += [('currency_id', '=', currency_id)]
559         sign = 1 # correct the fact that st_line.amount is signed and debit/credit is not
560         amount_field = 'debit'
561         if currency_id == False:
562             if amount < 0:
563                 amount_field = 'credit'
564                 sign = -1
565         else:
566             amount_field = 'amount_currency'
567
568         # Look for a matching amount
569         domain_exact_amount = domain + [(amount_field, '=', float_round(sign * amount, precision_digits=precision_digits))]
570         match_id = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, offset=0, limit=1, additional_domain=domain_exact_amount)
571         if match_id:
572             return match_id
573
574         if not st_line.partner_id.id:
575             return []
576
577         # Look for a set of move line whose amount is <= to the line's amount
578         if amount > 0: # Make sure we can't mix receivable and payable
579             domain += [('account_id.type', '=', 'receivable')]
580         else:
581             domain += [('account_id.type', '=', 'payable')]
582         if amount_field == 'amount_currency' and amount < 0:
583             domain += [(amount_field, '<', 0), (amount_field, '>', (sign * amount))]
584         else:
585             domain += [(amount_field, '>', 0), (amount_field, '<', (sign * amount))]
586         mv_lines = self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, limit=5, additional_domain=domain)
587         ret = []
588         total = 0
589         for line in mv_lines:
590             total += abs(line['debit'] - line['credit'])
591             if float_compare(total, abs(amount), precision_digits=precision_digits) != 1:
592                 ret.append(line)
593             else:
594                 break
595         return ret
596
597     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):
598         """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
599         if excluded_ids is None:
600             excluded_ids = []
601         if additional_domain is None:
602             additional_domain = []
603         st_line = self.browse(cr, uid, st_line_id, context=context)
604         return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
605
606     def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
607         if excluded_ids is None:
608             excluded_ids = []
609         if additional_domain is None:
610             additional_domain = []
611         # Make domain
612         domain = additional_domain + [
613             ('reconcile_id', '=', False),
614             ('state', '=', 'valid'),
615             ('account_id.reconcile', '=', True)
616         ]
617         if st_line.partner_id.id:
618             domain += [('partner_id', '=', st_line.partner_id.id)]
619         if excluded_ids:
620             domain.append(('id', 'not in', excluded_ids))
621         if str:
622             domain += [
623                 '|', ('move_id.name', 'ilike', str),
624                 '|', ('move_id.ref', 'ilike', str),
625                 ('date_maturity', 'like', str),
626             ]
627             if not st_line.partner_id.id:
628                 domain.insert(-1, '|', )
629                 domain.append(('partner_id.name', 'ilike', str))
630             if str != '/':
631                 domain.insert(-1, '|', )
632                 domain.append(('name', 'ilike', str))
633         return domain
634
635     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):
636         """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
637
638             :param st_line: the browse record of the statement line
639             :param integers list excluded_ids: ids of move lines that should not be fetched
640             :param boolean count: just return the number of records
641             :param tuples list additional_domain: additional domain restrictions
642         """
643         mv_line_pool = self.pool.get('account.move.line')
644         domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
645         
646         # Get move lines ; in case of a partial reconciliation, only consider one line
647         filtered_lines = []
648         reconcile_partial_ids = []
649         actual_offset = offset
650         while True:
651             line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
652             lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
653             make_one_more_loop = False
654             for line in lines:
655                 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
656                     #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
657                     #in order to get the right number of items in the pager
658                     make_one_more_loop = True
659                     continue
660                 filtered_lines.append(line)
661                 if line.reconcile_partial_id:
662                     reconcile_partial_ids.append(line.reconcile_partial_id.id)
663
664             if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
665                 break
666             actual_offset = actual_offset + limit
667         lines = limit and filtered_lines[:limit] or filtered_lines
668
669         # Either return number of lines
670         if count:
671             return len(lines)
672
673         # Or return list of dicts representing the formatted move lines
674         else:
675             target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
676             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)
677             has_no_partner = not bool(st_line.partner_id.id)
678             for line in mv_lines:
679                 line['has_no_partner'] = has_no_partner
680             return mv_lines
681
682     def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
683         if currency_diff < 0:
684             account_id = st_line.company_id.expense_currency_exchange_account_id.id
685             if not account_id:
686                 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."))
687         else:
688             account_id = st_line.company_id.income_currency_exchange_account_id.id
689             if not account_id:
690                 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."))
691         return {
692             'move_id': move_id,
693             'name': _('change') + ': ' + (st_line.name or '/'),
694             'period_id': st_line.statement_id.period_id.id,
695             'journal_id': st_line.journal_id.id,
696             'partner_id': st_line.partner_id.id,
697             'company_id': st_line.company_id.id,
698             'statement_id': st_line.statement_id.id,
699             'debit': currency_diff < 0 and -currency_diff or 0,
700             'credit': currency_diff > 0 and currency_diff or 0,
701             'amount_currency': 0.0,
702             'date': st_line.date,
703             'account_id': account_id
704             }
705
706     def process_reconciliations(self, cr, uid, data, context=None):
707         for datum in data:
708             self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
709
710     def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
711         """ 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.
712
713             :param int id: id of the bank statement line
714             :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
715         """
716         if context is None:
717             context = {}
718         st_line = self.browse(cr, uid, id, context=context)
719         company_currency = st_line.journal_id.company_id.currency_id
720         statement_currency = st_line.journal_id.currency or company_currency
721         bs_obj = self.pool.get('account.bank.statement')
722         am_obj = self.pool.get('account.move')
723         aml_obj = self.pool.get('account.move.line')
724         currency_obj = self.pool.get('res.currency')
725
726         # Checks
727         if st_line.journal_entry_id.id:
728             raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
729         for mv_line_dict in mv_line_dicts:
730             for field in ['debit', 'credit', 'amount_currency']:
731                 if field not in mv_line_dict:
732                     mv_line_dict[field] = 0.0
733             if mv_line_dict.get('counterpart_move_line_id'):
734                 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
735                 if mv_line.reconcile_id:
736                     raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
737
738         # Create the move
739         move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
740         move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
741         move_id = am_obj.create(cr, uid, move_vals, context=context)
742
743         # Create the move line for the statement line
744         if st_line.statement_id.currency.id != company_currency.id:
745             if st_line.currency_id == company_currency:
746                 amount = st_line.amount_currency
747             else:
748                 ctx = context.copy()
749                 ctx['date'] = st_line.date
750                 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
751         else:
752             amount = st_line.amount
753         bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
754         aml_obj.create(cr, uid, bank_st_move_vals, context=context)
755         # Complete the dicts
756         st_line_currency = st_line.currency_id or statement_currency
757         st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
758         to_create = []
759         for mv_line_dict in mv_line_dicts:
760             if mv_line_dict.get('is_tax_line'):
761                 continue
762             mv_line_dict['ref'] = move_name
763             mv_line_dict['move_id'] = move_id
764             mv_line_dict['period_id'] = st_line.statement_id.period_id.id
765             mv_line_dict['journal_id'] = st_line.journal_id.id
766             mv_line_dict['company_id'] = st_line.company_id.id
767             mv_line_dict['statement_id'] = st_line.statement_id.id
768             if mv_line_dict.get('counterpart_move_line_id'):
769                 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
770                 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
771                 mv_line_dict['account_id'] = mv_line.account_id.id
772             if st_line_currency.id != company_currency.id:
773                 ctx = context.copy()
774                 ctx['date'] = st_line.date
775                 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
776                 mv_line_dict['currency_id'] = st_line_currency.id
777                 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
778                     debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
779                     credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
780                 elif st_line.currency_id and st_line_currency_rate:
781                     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)
782                     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)
783                 else:
784                     debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
785                     credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
786                 if mv_line_dict.get('counterpart_move_line_id'):
787                     #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
788                     ctx['date'] = mv_line.date
789                     debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
790                     credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
791                     mv_line_dict['credit'] = credit_at_old_rate
792                     mv_line_dict['debit'] = debit_at_old_rate
793                     if debit_at_old_rate - debit_at_current_rate:
794                         currency_diff = debit_at_current_rate - debit_at_old_rate
795                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
796                     if credit_at_old_rate - credit_at_current_rate:
797                         currency_diff = credit_at_current_rate - credit_at_old_rate
798                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
799                 else:
800                     mv_line_dict['debit'] = debit_at_current_rate
801                     mv_line_dict['credit'] = credit_at_current_rate
802             elif statement_currency.id != company_currency.id:
803                 #statement is in foreign currency but the transaction is in company currency
804                 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
805                 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
806             to_create.append(mv_line_dict)
807         # Create move lines
808         move_line_pairs_to_reconcile = []
809         for mv_line_dict in to_create:
810             counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
811             if mv_line_dict.get('counterpart_move_line_id'):
812                 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
813                 del mv_line_dict['counterpart_move_line_id']
814             new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
815             if counterpart_move_line_id != None:
816                 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
817         # Reconcile
818         for pair in move_line_pairs_to_reconcile:
819             aml_obj.reconcile_partial(cr, uid, pair, context=context)
820         # Mark the statement line as reconciled
821         self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
822
823     # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
824     # return [('journal_entry_id', '=', False)]
825     # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
826     def _needaction_domain_get(self, cr, uid, context=None):
827         user = self.pool.get("res.users").browse(cr, uid, uid)
828         return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
829
830     _order = "statement_id desc, sequence"
831     _name = "account.bank.statement.line"
832     _description = "Bank Statement Line"
833     _inherit = ['ir.needaction_mixin']
834     _columns = {
835         'name': fields.char('Communication', required=True),
836         'date': fields.date('Date', required=True),
837         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
838         'partner_id': fields.many2one('res.partner', 'Partner'),
839         'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
840         '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"),
841         'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
842         'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
843         '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)."),
844         'ref': fields.char('Reference'),
845         'note': fields.text('Notes'),
846         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
847         'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
848         'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
849         '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')),
850         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
851     }
852     _defaults = {
853         'name': lambda self,cr,uid,context={}: self.pool.get('ir.sequence').get(cr, uid, 'account.bank.statement.line'),
854         'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
855     }
856
857 class account_statement_operation_template(osv.osv):
858     _name = "account.statement.operation.template"
859     _description = "Preset for the lines that can be created in a bank statement reconciliation"
860     _columns = {
861         'name': fields.char('Button Label', required=True),
862         'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type', 'not in', ('view', 'closed', 'consolidation'))]),
863         'label': fields.char('Label'),
864         'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')],
865                                    'Amount type', required=True),
866         '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),
867         'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use', 'in', ['purchase', 'all']), ('parent_id', '=', False)]),
868         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]),
869     }
870     _defaults = {
871         'amount_type': 'percentage_of_balance',
872         'amount': 100.0
873     }
874
875 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: