[FIX] website_forum_doc fixes
[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         statement_line_obj = self.pool['account.bank.statement.line']
387         for item in self.browse(cr, uid, ids, context=context):
388             if item.state != 'draft':
389                 raise osv.except_osv(
390                     _('Invalid Action!'), 
391                     _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
392                 )
393             # Explicitly unlink bank statement lines
394             # so it will check that the related journal entries have
395             # been deleted first
396             statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
397         return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
398
399     def button_journal_entries(self, cr, uid, ids, context=None):
400         ctx = (context or {}).copy()
401         ctx['journal_id'] = self.browse(cr, uid, ids[0], context=context).journal_id.id
402         return {
403             'name': _('Journal Items'),
404             'view_type':'form',
405             'view_mode':'tree',
406             'res_model':'account.move.line',
407             'view_id':False,
408             'type':'ir.actions.act_window',
409             'domain':[('statement_id','in',ids)],
410             'context':ctx,
411         }
412
413     def number_of_lines_reconciled(self, cr, uid, ids, context=None):
414         bsl_obj = self.pool.get('account.bank.statement.line')
415         return bsl_obj.search_count(cr, uid, [('statement_id', 'in', ids), ('journal_entry_id', '!=', False)], context=context)
416
417     def link_bank_to_partner(self, cr, uid, ids, context=None):
418         for statement in self.browse(cr, uid, ids, context=context):
419             for st_line in statement.line_ids:
420                 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:
421                     self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context)
422
423 class account_bank_statement_line(osv.osv):
424
425     def create(self, cr, uid, vals, context=None):
426         if vals.get('amount_currency', 0) and not vals.get('amount', 0):
427             raise osv.except_osv(_('Error!'), _('If "Amount Currency" is specified, then "Amount" must be as well.'))
428         return super(account_bank_statement_line, self).create(cr, uid, vals, context=context)
429
430     def unlink(self, cr, uid, ids, context=None):
431         for item in self.browse(cr, uid, ids, context=context):
432             if item.journal_entry_id:
433                 raise osv.except_osv(
434                     _('Invalid Action!'), 
435                     _('In order to delete a bank statement line, you must first cancel it to delete related journal items.')
436                 )
437         return super(account_bank_statement_line, self).unlink(cr, uid, ids, context=context)
438
439     def cancel(self, cr, uid, ids, context=None):
440         account_move_obj = self.pool.get('account.move')
441         move_ids = []
442         for line in self.browse(cr, uid, ids, context=context):
443             if line.journal_entry_id:
444                 move_ids.append(line.journal_entry_id.id)
445                 for aml in line.journal_entry_id.line_id:
446                     if aml.reconcile_id:
447                         move_lines = [l.id for l in aml.reconcile_id.line_id]
448                         move_lines.remove(aml.id)
449                         self.pool.get('account.move.reconcile').unlink(cr, uid, [aml.reconcile_id.id], context=context)
450                         if len(move_lines) >= 2:
451                             self.pool.get('account.move.line').reconcile_partial(cr, uid, move_lines, 'auto', context=context)
452         if move_ids:
453             account_move_obj.button_cancel(cr, uid, move_ids, context=context)
454             account_move_obj.unlink(cr, uid, move_ids, context)
455
456     def get_data_for_reconciliations(self, cr, uid, ids, excluded_ids=None, search_reconciliation_proposition=True, context=None):
457         """ Returns the data required to display a reconciliation, for each statement line id in ids """
458         ret = []
459         if excluded_ids is None:
460             excluded_ids = []
461
462         for st_line in self.browse(cr, uid, ids, context=context):
463             reconciliation_data = {}
464             if search_reconciliation_proposition:
465                 reconciliation_proposition = self.get_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
466                 for mv_line in reconciliation_proposition:
467                     excluded_ids.append(mv_line['id'])
468                 reconciliation_data['reconciliation_proposition'] = reconciliation_proposition
469             else:
470                 reconciliation_data['reconciliation_proposition'] = []
471             st_line = self.get_statement_line_for_reconciliation(cr, uid, st_line, context=context)
472             reconciliation_data['st_line'] = st_line
473             ret.append(reconciliation_data)
474
475         return ret
476
477     def get_statement_line_for_reconciliation(self, cr, uid, st_line, context=None):
478         """ Returns the data required by the bank statement reconciliation widget to display a statement line """
479         if context is None:
480             context = {}
481         statement_currency = st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
482         rml_parser = report_sxw.rml_parse(cr, uid, 'reconciliation_widget_asl', context=context)
483
484         if st_line.amount_currency and st_line.currency_id:
485             amount = st_line.amount_currency
486             amount_currency = st_line.amount
487             amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency
488             amount_currency_str = rml_parser.formatLang(amount_currency_str, currency_obj=statement_currency)
489         else:
490             amount = st_line.amount
491             amount_currency_str = ""
492         amount_str = amount > 0 and amount or -amount
493         amount_str = rml_parser.formatLang(amount_str, currency_obj=st_line.currency_id or statement_currency)
494
495         data = {
496             'id': st_line.id,
497             'ref': st_line.ref,
498             'note': st_line.note or "",
499             'name': st_line.name,
500             'date': st_line.date,
501             'amount': amount,
502             'amount_str': amount_str, # Amount in the statement line currency
503             'currency_id': st_line.currency_id.id or statement_currency.id,
504             'partner_id': st_line.partner_id.id,
505             'statement_id': st_line.statement_id.id,
506             'account_code': st_line.journal_id.default_debit_account_id.code,
507             'account_name': st_line.journal_id.default_debit_account_id.name,
508             'partner_name': st_line.partner_id.name,
509             'communication_partner_name': st_line.partner_name,
510             'amount_currency_str': amount_currency_str, # Amount in the statement currency
511             'has_no_partner': not st_line.partner_id.id,
512         }
513         if st_line.partner_id.id:
514             if amount > 0:
515                 data['open_balance_account_id'] = st_line.partner_id.property_account_receivable.id
516             else:
517                 data['open_balance_account_id'] = st_line.partner_id.property_account_payable.id
518
519         return data
520
521     def _domain_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
522         if excluded_ids is None:
523             excluded_ids = []
524         domain = [('ref', '=', st_line.name),
525                   ('reconcile_id', '=', False),
526                   ('state', '=', 'valid'),
527                   ('account_id.reconcile', '=', True),
528                   ('id', 'not in', excluded_ids)]
529         return domain
530
531     def get_reconciliation_proposition(self, cr, uid, st_line, excluded_ids=None, context=None):
532         """ Returns move lines that constitute the best guess to reconcile a statement line. """
533         mv_line_pool = self.pool.get('account.move.line')
534
535         # Look for structured communication
536         if st_line.name:
537             domain = self._domain_reconciliation_proposition(cr, uid, st_line, excluded_ids=excluded_ids, context=context)
538             match_id = mv_line_pool.search(cr, uid, domain, offset=0, limit=1, context=context)
539             if match_id:
540                 mv_line_br = mv_line_pool.browse(cr, uid, match_id, context=context)
541                 target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
542                 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]
543                 mv_line['has_no_partner'] = not bool(st_line.partner_id.id)
544                 # If the structured communication matches a move line that is associated with a partner, we can safely associate the statement line with the partner
545                 if (mv_line['partner_id']):
546                     self.write(cr, uid, st_line.id, {'partner_id': mv_line['partner_id']}, context=context)
547                     mv_line['has_no_partner'] = False
548                 return [mv_line]
549
550         # If there is no identified partner or structured communication, don't look further
551         if not st_line.partner_id.id:
552             return []
553
554         # Look for a move line whose amount matches the statement line's amount
555         company_currency = st_line.journal_id.company_id.currency_id.id
556         statement_currency = st_line.journal_id.currency.id or company_currency
557         sign = 1
558         if statement_currency == company_currency:
559             amount_field = 'credit'
560             if st_line.amount > 0:
561                 amount_field = 'debit'
562             else:
563                 sign = -1
564         else:
565             amount_field = 'amount_currency'
566             if st_line.amount < 0:
567                 sign = -1
568         if st_line.amount_currency:
569             amount = st_line.amount_currency
570         else:
571             amount = st_line.amount
572
573         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))])
574         if match_id:
575             return [match_id[0]]
576
577         return []
578
579     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):
580         """ Bridge between the web client reconciliation widget and get_move_lines_for_reconciliation (which expects a browse record) """
581         if excluded_ids is None:
582             excluded_ids = []
583         if additional_domain is None:
584             additional_domain = []
585         st_line = self.browse(cr, uid, st_line_id, context=context)
586         return self.get_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids, str, offset, limit, count, additional_domain, context=context)
587
588     def _domain_move_lines_for_reconciliation(self, cr, uid, st_line, excluded_ids=None, str=False, additional_domain=None, context=None):
589         if excluded_ids is None:
590             excluded_ids = []
591         if additional_domain is None:
592             additional_domain = []
593         # Make domain
594         domain = additional_domain + [('reconcile_id', '=', False),
595                                       ('state', '=', 'valid'),
596                                       ('account_id.reconcile', '=', True)]
597         if st_line.partner_id.id:
598             domain += [('partner_id', '=', st_line.partner_id.id)]
599         if excluded_ids:
600             domain.append(('id', 'not in', excluded_ids))
601         if str:
602             domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)]
603             if not st_line.partner_id.id:
604                 domain.insert(-1, '|', )
605                 domain.append(('partner_id.name', 'ilike', str))
606             if str != '/':
607                 domain.insert(-1, '|', )
608                 domain.append(('name', 'ilike', str))
609         return domain
610
611     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):
612         """ Find the move lines that could be used to reconcile a statement line. If count is true, only returns the count.
613
614             :param st_line: the browse record of the statement line
615             :param integers list excluded_ids: ids of move lines that should not be fetched
616             :param boolean count: just return the number of records
617             :param tuples list additional_domain: additional domain restrictions
618         """
619         mv_line_pool = self.pool.get('account.move.line')
620         domain = self._domain_move_lines_for_reconciliation(cr, uid, st_line, excluded_ids=excluded_ids, str=str, additional_domain=additional_domain, context=context)
621         
622         # Get move lines ; in case of a partial reconciliation, only consider one line
623         filtered_lines = []
624         reconcile_partial_ids = []
625         actual_offset = offset
626         while True:
627             line_ids = mv_line_pool.search(cr, uid, domain, offset=actual_offset, limit=limit, order="date_maturity asc, id asc", context=context)
628             lines = mv_line_pool.browse(cr, uid, line_ids, context=context)
629             make_one_more_loop = False
630             for line in lines:
631                 if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids:
632                     #if we filtered a line because it is partially reconciled with an already selected line, we must do one more loop
633                     #in order to get the right number of items in the pager
634                     make_one_more_loop = True
635                     continue
636                 filtered_lines.append(line)
637                 if line.reconcile_partial_id:
638                     reconcile_partial_ids.append(line.reconcile_partial_id.id)
639
640             if not limit or not make_one_more_loop or len(filtered_lines) >= limit:
641                 break
642             actual_offset = actual_offset + limit
643         lines = limit and filtered_lines[:limit] or filtered_lines
644
645         # Either return number of lines
646         if count:
647             return len(lines)
648
649         # Or return list of dicts representing the formatted move lines
650         else:
651             target_currency = st_line.currency_id or st_line.journal_id.currency or st_line.journal_id.company_id.currency_id
652             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)
653             has_no_partner = not bool(st_line.partner_id.id)
654             for line in mv_lines:
655                 line['has_no_partner'] = has_no_partner
656             return mv_lines
657
658     def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None):
659         if currency_diff < 0:
660             account_id = st_line.company_id.expense_currency_exchange_account_id.id
661             if not account_id:
662                 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."))
663         else:
664             account_id = st_line.company_id.income_currency_exchange_account_id.id
665             if not account_id:
666                 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."))
667         return {
668             'move_id': move_id,
669             'name': _('change') + ': ' + (st_line.name or '/'),
670             'period_id': st_line.statement_id.period_id.id,
671             'journal_id': st_line.journal_id.id,
672             'partner_id': st_line.partner_id.id,
673             'company_id': st_line.company_id.id,
674             'statement_id': st_line.statement_id.id,
675             'debit': currency_diff < 0 and -currency_diff or 0,
676             'credit': currency_diff > 0 and currency_diff or 0,
677             'amount_currency': 0.0,
678             'date': st_line.date,
679             'account_id': account_id
680             }
681
682     def process_reconciliations(self, cr, uid, data, context=None):
683         for datum in data:
684             self.process_reconciliation(cr, uid, datum[0], datum[1], context=context)
685
686     def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None):
687         """ 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.
688
689             :param int id: id of the bank statement line
690             :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it
691         """
692         if context is None:
693             context = {}
694         st_line = self.browse(cr, uid, id, context=context)
695         company_currency = st_line.journal_id.company_id.currency_id
696         statement_currency = st_line.journal_id.currency or company_currency
697         bs_obj = self.pool.get('account.bank.statement')
698         am_obj = self.pool.get('account.move')
699         aml_obj = self.pool.get('account.move.line')
700         currency_obj = self.pool.get('res.currency')
701
702         # Checks
703         if st_line.journal_entry_id.id:
704             raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.'))
705         for mv_line_dict in mv_line_dicts:
706             for field in ['debit', 'credit', 'amount_currency']:
707                 if field not in mv_line_dict:
708                     mv_line_dict[field] = 0.0
709             if mv_line_dict.get('counterpart_move_line_id'):
710                 mv_line = aml_obj.browse(cr, uid, mv_line_dict.get('counterpart_move_line_id'), context=context)
711                 if mv_line.reconcile_id:
712                     raise osv.except_osv(_('Error!'), _('A selected move line was already reconciled.'))
713
714         # Create the move
715         move_name = (st_line.statement_id.name or st_line.name) + "/" + str(st_line.sequence)
716         move_vals = bs_obj._prepare_move(cr, uid, st_line, move_name, context=context)
717         move_id = am_obj.create(cr, uid, move_vals, context=context)
718
719         # Create the move line for the statement line
720         if st_line.statement_id.currency.id != company_currency.id:
721             if st_line.currency_id == company_currency:
722                 amount = st_line.amount_currency
723             else:
724                 ctx = context.copy()
725                 ctx['date'] = st_line.date
726                 amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=ctx)
727         else:
728             amount = st_line.amount
729         bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
730         aml_obj.create(cr, uid, bank_st_move_vals, context=context)
731         # Complete the dicts
732         st_line_currency = st_line.currency_id or statement_currency
733         st_line_currency_rate = st_line.currency_id and (st_line.amount_currency / st_line.amount) or False
734         to_create = []
735         for mv_line_dict in mv_line_dicts:
736             if mv_line_dict.get('is_tax_line'):
737                 continue
738             mv_line_dict['ref'] = move_name
739             mv_line_dict['move_id'] = move_id
740             mv_line_dict['period_id'] = st_line.statement_id.period_id.id
741             mv_line_dict['journal_id'] = st_line.journal_id.id
742             mv_line_dict['company_id'] = st_line.company_id.id
743             mv_line_dict['statement_id'] = st_line.statement_id.id
744             if mv_line_dict.get('counterpart_move_line_id'):
745                 mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context)
746                 mv_line_dict['partner_id'] = mv_line.partner_id.id or st_line.partner_id.id
747                 mv_line_dict['account_id'] = mv_line.account_id.id
748             if st_line_currency.id != company_currency.id:
749                 ctx = context.copy()
750                 ctx['date'] = st_line.date
751                 mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit']
752                 mv_line_dict['currency_id'] = st_line_currency.id
753                 if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
754                     debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate)
755                     credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate)
756                 elif st_line.currency_id and st_line_currency_rate:
757                     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)
758                     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)
759                 else:
760                     debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
761                     credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
762                 if mv_line_dict.get('counterpart_move_line_id'):
763                     #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line
764                     ctx['date'] = mv_line.date
765                     debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx)
766                     credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx)
767                     mv_line_dict['credit'] = credit_at_old_rate
768                     mv_line_dict['debit'] = debit_at_old_rate
769                     if debit_at_old_rate - debit_at_current_rate:
770                         currency_diff = debit_at_current_rate - debit_at_old_rate
771                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, -currency_diff, move_id, context=context))
772                     if credit_at_old_rate - credit_at_current_rate:
773                         currency_diff = credit_at_current_rate - credit_at_old_rate
774                         to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context))
775                 else:
776                     mv_line_dict['debit'] = debit_at_current_rate
777                     mv_line_dict['credit'] = credit_at_current_rate
778             elif statement_currency.id != company_currency.id:
779                 #statement is in foreign currency but the transaction is in company currency
780                 prorata_factor = (mv_line_dict['debit'] - mv_line_dict['credit']) / st_line.amount_currency
781                 mv_line_dict['amount_currency'] = prorata_factor * st_line.amount
782             to_create.append(mv_line_dict)
783         # Create move lines
784         move_line_pairs_to_reconcile = []
785         for mv_line_dict in to_create:
786             counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict
787             if mv_line_dict.get('counterpart_move_line_id'):
788                 counterpart_move_line_id = mv_line_dict['counterpart_move_line_id']
789                 del mv_line_dict['counterpart_move_line_id']
790             new_aml_id = aml_obj.create(cr, uid, mv_line_dict, context=context)
791             if counterpart_move_line_id != None:
792                 move_line_pairs_to_reconcile.append([new_aml_id, counterpart_move_line_id])
793         # Reconcile
794         for pair in move_line_pairs_to_reconcile:
795             aml_obj.reconcile_partial(cr, uid, pair, context=context)
796         # Mark the statement line as reconciled
797         self.write(cr, uid, id, {'journal_entry_id': move_id}, context=context)
798
799     # FIXME : if it wasn't for the multicompany security settings in account_security.xml, the method would just
800     # return [('journal_entry_id', '=', False)]
801     # Unfortunately, that spawns a "no access rights" error ; it shouldn't.
802     def _needaction_domain_get(self, cr, uid, context=None):
803         user = self.pool.get("res.users").browse(cr, uid, uid)
804         return ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id]), ('journal_entry_id', '=', False)]
805
806     _order = "statement_id desc, sequence"
807     _name = "account.bank.statement.line"
808     _description = "Bank Statement Line"
809     _inherit = ['ir.needaction_mixin']
810     _columns = {
811         'name': fields.char('Communication', required=True),
812         'date': fields.date('Date', required=True),
813         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
814         'partner_id': fields.many2one('res.partner', 'Partner'),
815         'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
816         '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"),
817         'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
818         'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
819         '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)."),
820         'ref': fields.char('Reference'),
821         'note': fields.text('Notes'),
822         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
823         'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
824         'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
825         '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')),
826         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
827     }
828     _defaults = {
829         'date': lambda self,cr,uid,context={}: context.get('date', fields.date.context_today(self,cr,uid,context=context)),
830     }
831
832 class account_statement_operation_template(osv.osv):
833     _name = "account.statement.operation.template"
834     _description = "Preset for the lines that can be created in a bank statement reconciliation"
835     _columns = {
836         'name': fields.char('Button Label', required=True),
837         'account_id': fields.many2one('account.account', 'Account', ondelete='cascade', domain=[('type','not in',('view','closed','consolidation'))]),
838         'label': fields.char('Journal Item Label'),
839         'amount_type': fields.selection([('fixed', 'Fixed'),('percentage_of_total','Percentage of total amount'),('percentage_of_balance', 'Percentage of open balance')], 'Amount type', required=True),
840         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account'), required=True, 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')."),
841         'tax_id': fields.many2one('account.tax', 'Tax', ondelete='restrict', domain=[('type_tax_use','in',('purchase','all')), ('parent_id','=',False)]),
842         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='set null', domain=[('type','!=','view'), ('state','not in',('close','cancelled'))]),
843         'company_id': fields.many2one('res.company', 'Company', required=True),
844     }
845     _defaults = {
846         'amount_type': 'percentage_of_balance',
847         'amount': 100.0,
848         'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
849     }
850
851 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: