[FIX] account: reconcile: no useless revalidation
[odoo/odoo.git] / addons / account / account_move_line.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 import sys
23 import time
24 from datetime import datetime
25 from operator import itemgetter
26
27 from lxml import etree
28
29 from openerp import netsvc
30 from openerp.osv import fields, osv, orm
31 from openerp.tools.translate import _
32 import openerp.addons.decimal_precision as dp
33 from openerp import tools
34
35 class account_move_line(osv.osv):
36     _name = "account.move.line"
37     _description = "Journal Items"
38
39     def _query_get(self, cr, uid, obj='l', context=None):
40         fiscalyear_obj = self.pool.get('account.fiscalyear')
41         fiscalperiod_obj = self.pool.get('account.period')
42         account_obj = self.pool.get('account.account')
43         fiscalyear_ids = []
44         if context is None:
45             context = {}
46         initial_bal = context.get('initial_bal', False)
47         company_clause = " "
48         if context.get('company_id', False):
49             company_clause = " AND " +obj+".company_id = %s" % context.get('company_id', False)
50         if not context.get('fiscalyear', False):
51             if context.get('all_fiscalyear', False):
52                 #this option is needed by the aged balance report because otherwise, if we search only the draft ones, an open invoice of a closed fiscalyear won't be displayed
53                 fiscalyear_ids = fiscalyear_obj.search(cr, uid, [])
54             else:
55                 fiscalyear_ids = fiscalyear_obj.search(cr, uid, [('state', '=', 'draft')])
56         else:
57             #for initial balance as well as for normal query, we check only the selected FY because the best practice is to generate the FY opening entries
58             fiscalyear_ids = [context['fiscalyear']]
59
60         fiscalyear_clause = (','.join([str(x) for x in fiscalyear_ids])) or '0'
61         state = context.get('state', False)
62         where_move_state = ''
63         where_move_lines_by_date = ''
64
65         if context.get('date_from', False) and context.get('date_to', False):
66             if initial_bal:
67                 where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date < '" +context['date_from']+"')"
68             else:
69                 where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date >= '" +context['date_from']+"' AND date <= '"+context['date_to']+"')"
70
71         if state:
72             if state.lower() not in ['all']:
73                 where_move_state= " AND "+obj+".move_id IN (SELECT id FROM account_move WHERE account_move.state = '"+state+"')"
74         if context.get('period_from', False) and context.get('period_to', False) and not context.get('periods', False):
75             if initial_bal:
76                 period_company_id = fiscalperiod_obj.browse(cr, uid, context['period_from'], context=context).company_id.id
77                 first_period = fiscalperiod_obj.search(cr, uid, [('company_id', '=', period_company_id)], order='date_start', limit=1)[0]
78                 context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, first_period, context['period_from'])
79             else:
80                 context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, context['period_from'], context['period_to'])
81         if context.get('periods', False):
82             if initial_bal:
83                 query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date)
84                 period_ids = fiscalperiod_obj.search(cr, uid, [('id', 'in', context['periods'])], order='date_start', limit=1)
85                 if period_ids and period_ids[0]:
86                     first_period = fiscalperiod_obj.browse(cr, uid, period_ids[0], context=context)
87                     ids = ','.join([str(x) for x in context['periods']])
88                     query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND date_start <= '%s' AND id NOT IN (%s)) %s %s" % (fiscalyear_clause, first_period.date_start, ids, where_move_state, where_move_lines_by_date)
89             else:
90                 ids = ','.join([str(x) for x in context['periods']])
91                 query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND id IN (%s)) %s %s" % (fiscalyear_clause, ids, where_move_state, where_move_lines_by_date)
92         else:
93             query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date)
94
95         if initial_bal and not context.get('periods', False) and not where_move_lines_by_date:
96             #we didn't pass any filter in the context, and the initial balance can't be computed using only the fiscalyear otherwise entries will be summed twice
97             #so we have to invalidate this query
98             raise osv.except_osv(_('Warning!'),_("You have not supplied enough arguments to compute the initial balance, please select a period and a journal in the context."))
99
100
101         if context.get('journal_ids', False):
102             query += ' AND '+obj+'.journal_id IN (%s)' % ','.join(map(str, context['journal_ids']))
103
104         if context.get('chart_account_id', False):
105             child_ids = account_obj._get_children_and_consol(cr, uid, [context['chart_account_id']], context=context)
106             query += ' AND '+obj+'.account_id IN (%s)' % ','.join(map(str, child_ids))
107
108         query += company_clause
109         return query
110
111     def _amount_residual(self, cr, uid, ids, field_names, args, context=None):
112         """
113            This function returns the residual amount on a receivable or payable account.move.line.
114            By default, it returns an amount in the currency of this journal entry (maybe different
115            of the company currency), but if you pass 'residual_in_company_currency' = True in the
116            context then the returned amount will be in company currency.
117         """
118         res = {}
119         if context is None:
120             context = {}
121         cur_obj = self.pool.get('res.currency')
122         for move_line in self.browse(cr, uid, ids, context=context):
123             res[move_line.id] = {
124                 'amount_residual': 0.0,
125                 'amount_residual_currency': 0.0,
126             }
127
128             if move_line.reconcile_id:
129                 continue
130             if not move_line.account_id.type in ('payable', 'receivable'):
131                 #this function does not suport to be used on move lines not related to payable or receivable accounts
132                 continue
133
134             if move_line.currency_id:
135                 move_line_total = move_line.amount_currency
136                 sign = move_line.amount_currency < 0 and -1 or 1
137             else:
138                 move_line_total = move_line.debit - move_line.credit
139                 sign = (move_line.debit - move_line.credit) < 0 and -1 or 1
140             line_total_in_company_currency =  move_line.debit - move_line.credit
141             context_unreconciled = context.copy()
142             if move_line.reconcile_partial_id:
143                 for payment_line in move_line.reconcile_partial_id.line_partial_ids:
144                     if payment_line.id == move_line.id:
145                         continue
146                     if payment_line.currency_id and move_line.currency_id and payment_line.currency_id.id == move_line.currency_id.id:
147                             move_line_total += payment_line.amount_currency
148                     else:
149                         if move_line.currency_id:
150                             context_unreconciled.update({'date': payment_line.date})
151                             amount_in_foreign_currency = cur_obj.compute(cr, uid, move_line.company_id.currency_id.id, move_line.currency_id.id, (payment_line.debit - payment_line.credit), round=False, context=context_unreconciled)
152                             move_line_total += amount_in_foreign_currency
153                         else:
154                             move_line_total += (payment_line.debit - payment_line.credit)
155                     line_total_in_company_currency += (payment_line.debit - payment_line.credit)
156
157             result = move_line_total
158             res[move_line.id]['amount_residual_currency'] =  sign * (move_line.currency_id and self.pool.get('res.currency').round(cr, uid, move_line.currency_id, result) or result)
159             res[move_line.id]['amount_residual'] = sign * line_total_in_company_currency
160         return res
161
162     def default_get(self, cr, uid, fields, context=None):
163         data = self._default_get(cr, uid, fields, context=context)
164         for f in data.keys():
165             if f not in fields:
166                 del data[f]
167         return data
168
169     def _prepare_analytic_line(self, cr, uid, obj_line, context=None):
170         """
171         Prepare the values given at the create() of account.analytic.line upon the validation of a journal item having
172         an analytic account. This method is intended to be extended in other modules.
173
174         :param obj_line: browse record of the account.move.line that triggered the analytic line creation
175         """
176         return {'name': obj_line.name,
177                 'date': obj_line.date,
178                 'account_id': obj_line.analytic_account_id.id,
179                 'unit_amount': obj_line.quantity,
180                 'product_id': obj_line.product_id and obj_line.product_id.id or False,
181                 'product_uom_id': obj_line.product_uom_id and obj_line.product_uom_id.id or False,
182                 'amount': (obj_line.credit or  0.0) - (obj_line.debit or 0.0),
183                 'general_account_id': obj_line.account_id.id,
184                 'journal_id': obj_line.journal_id.analytic_journal_id.id,
185                 'ref': obj_line.ref,
186                 'move_id': obj_line.id,
187                 'user_id': uid,
188                }
189
190     def create_analytic_lines(self, cr, uid, ids, context=None):
191         acc_ana_line_obj = self.pool.get('account.analytic.line')
192         for obj_line in self.browse(cr, uid, ids, context=context):
193             if obj_line.analytic_account_id:
194                 if not obj_line.journal_id.analytic_journal_id:
195                     raise osv.except_osv(_('No Analytic Journal!'),_("You have to define an analytic journal on the '%s' journal!") % (obj_line.journal_id.name, ))
196                 if obj_line.analytic_lines:
197                     acc_ana_line_obj.unlink(cr,uid,[obj.id for obj in obj_line.analytic_lines])
198                 vals_line = self._prepare_analytic_line(cr, uid, obj_line, context=context)
199                 acc_ana_line_obj.create(cr, uid, vals_line)
200         return True
201
202     def _default_get_move_form_hook(self, cursor, user, data):
203         '''Called in the end of default_get method for manual entry in account_move form'''
204         if data.has_key('analytic_account_id'):
205             del(data['analytic_account_id'])
206         if data.has_key('account_tax_id'):
207             del(data['account_tax_id'])
208         return data
209
210     def convert_to_period(self, cr, uid, context=None):
211         if context is None:
212             context = {}
213         period_obj = self.pool.get('account.period')
214         #check if the period_id changed in the context from client side
215         if context.get('period_id', False):
216             period_id = context.get('period_id')
217             if type(period_id) == str:
218                 ids = period_obj.search(cr, uid, [('name', 'ilike', period_id)])
219                 context.update({
220                     'period_id': ids and ids[0] or False
221                 })
222         return context
223
224     def _default_get(self, cr, uid, fields, context=None):
225         #default_get should only do the following:
226         #   -propose the next amount in debit/credit in order to balance the move
227         #   -propose the next account from the journal (default debit/credit account) accordingly
228         if context is None:
229             context = {}
230         account_obj = self.pool.get('account.account')
231         period_obj = self.pool.get('account.period')
232         journal_obj = self.pool.get('account.journal')
233         move_obj = self.pool.get('account.move')
234         tax_obj = self.pool.get('account.tax')
235         fiscal_pos_obj = self.pool.get('account.fiscal.position')
236         partner_obj = self.pool.get('res.partner')
237         currency_obj = self.pool.get('res.currency')
238
239         if not context.get('journal_id', False):
240             context['journal_id'] = context.get('search_default_journal_id', False)
241         if not context.get('period_id', False):
242             context['period_id'] = context.get('search_default_period_id', False)
243         context = self.convert_to_period(cr, uid, context)
244
245         # Compute simple values
246         data = super(account_move_line, self).default_get(cr, uid, fields, context=context)
247         if context.get('journal_id'):
248             total = 0.0
249             #in account.move form view, it is not possible to compute total debit and credit using
250             #a browse record. So we must use the context to pass the whole one2many field and compute the total
251             if context.get('line_id'):
252                 for move_line_dict in move_obj.resolve_2many_commands(cr, uid, 'line_id', context.get('line_id'), context=context):
253                     data['name'] = data.get('name') or move_line_dict.get('name')
254                     data['partner_id'] = data.get('partner_id') or move_line_dict.get('partner_id')
255                     total += move_line_dict.get('debit', 0.0) - move_line_dict.get('credit', 0.0)
256             elif context.get('period_id'):
257                 #find the date and the ID of the last unbalanced account.move encoded by the current user in that journal and period
258                 move_id = False
259                 cr.execute('''SELECT move_id, date FROM account_move_line
260                     WHERE journal_id = %s AND period_id = %s AND create_uid = %s AND state = %s
261                     ORDER BY id DESC limit 1''', (context['journal_id'], context['period_id'], uid, 'draft'))
262                 res = cr.fetchone()
263                 move_id = res and res[0] or False
264                 data['date'] = res and res[1] or period_obj.browse(cr, uid, context['period_id'], context=context).date_start
265                 data['move_id'] = move_id
266                 if move_id:
267                     #if there exist some unbalanced accounting entries that match the journal and the period,
268                     #we propose to continue the same move by copying the ref, the name, the partner...
269                     move = move_obj.browse(cr, uid, move_id, context=context)
270                     data.setdefault('name', move.line_id[-1].name)
271                     for l in move.line_id:
272                         data['partner_id'] = data.get('partner_id') or l.partner_id.id
273                         data['ref'] = data.get('ref') or l.ref
274                         total += (l.debit or 0.0) - (l.credit or 0.0)
275
276             #compute the total of current move
277             data['debit'] = total < 0 and -total or 0.0
278             data['credit'] = total > 0 and total or 0.0
279             #pick the good account on the journal accordingly if the next proposed line will be a debit or a credit
280             journal_data = journal_obj.browse(cr, uid, context['journal_id'], context=context)
281             account = total > 0 and journal_data.default_credit_account_id or journal_data.default_debit_account_id
282             #map the account using the fiscal position of the partner, if needed
283             part = data.get('partner_id') and partner_obj.browse(cr, uid, data['partner_id'], context=context) or False
284             if account and data.get('partner_id'):
285                 account = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, account.id)
286                 account = account_obj.browse(cr, uid, account, context=context)
287             data['account_id'] =  account and account.id or False
288             #compute the amount in secondary currency of the account, if needed
289             if account and account.currency_id:
290                 data['currency_id'] = account.currency_id.id
291                 #set the context for the multi currency change
292                 compute_ctx = context.copy()
293                 compute_ctx.update({
294                         #the following 2 parameters are used to choose the currency rate, in case where the account
295                         #doesn't work with an outgoing currency rate method 'at date' but 'average'
296                         'res.currency.compute.account': account,
297                         'res.currency.compute.account_invert': True,
298                     })
299                 if data.get('date'):
300                     compute_ctx.update({'date': data['date']})
301                 data['amount_currency'] = currency_obj.compute(cr, uid, account.company_id.currency_id.id, data['currency_id'], -total, context=compute_ctx)
302         data = self._default_get_move_form_hook(cr, uid, data)
303         return data
304
305     def on_create_write(self, cr, uid, id, context=None):
306         if not id:
307             return []
308         ml = self.browse(cr, uid, id, context=context)
309         return map(lambda x: x.id, ml.move_id.line_id)
310
311     def _balance(self, cr, uid, ids, name, arg, context=None):
312         if context is None:
313             context = {}
314         c = context.copy()
315         c['initital_bal'] = True
316         sql = """SELECT l1.id, COALESCE(SUM(l2.debit-l2.credit), 0)
317                     FROM account_move_line l1 LEFT JOIN account_move_line l2
318                     ON (l1.account_id = l2.account_id
319                       AND l2.id <= l1.id
320                       AND """ + \
321                 self._query_get(cr, uid, obj='l2', context=c) + \
322                 ") WHERE l1.id IN %s GROUP BY l1.id"
323
324         cr.execute(sql, [tuple(ids)])
325         return dict(cr.fetchall())
326
327     def _invoice(self, cursor, user, ids, name, arg, context=None):
328         invoice_obj = self.pool.get('account.invoice')
329         res = {}
330         for line_id in ids:
331             res[line_id] = False
332         cursor.execute('SELECT l.id, i.id ' \
333                         'FROM account_move_line l, account_invoice i ' \
334                         'WHERE l.move_id = i.move_id ' \
335                         'AND l.id IN %s',
336                         (tuple(ids),))
337         invoice_ids = []
338         for line_id, invoice_id in cursor.fetchall():
339             res[line_id] = invoice_id
340             invoice_ids.append(invoice_id)
341         invoice_names = {False: ''}
342         for invoice_id, name in invoice_obj.name_get(cursor, user, invoice_ids, context=context):
343             invoice_names[invoice_id] = name
344         for line_id in res.keys():
345             invoice_id = res[line_id]
346             res[line_id] = (invoice_id, invoice_names[invoice_id])
347         return res
348
349     def name_get(self, cr, uid, ids, context=None):
350         if not ids:
351             return []
352         result = []
353         for line in self.browse(cr, uid, ids, context=context):
354             if line.ref:
355                 result.append((line.id, (line.move_id.name or '')+' ('+line.ref+')'))
356             else:
357                 result.append((line.id, line.move_id.name))
358         return result
359
360     def _balance_search(self, cursor, user, obj, name, args, domain=None, context=None):
361         if context is None:
362             context = {}
363         if not args:
364             return []
365         where = ' AND '.join(map(lambda x: '(abs(sum(debit-credit))'+x[1]+str(x[2])+')',args))
366         cursor.execute('SELECT id, SUM(debit-credit) FROM account_move_line \
367                      GROUP BY id, debit, credit having '+where)
368         res = cursor.fetchall()
369         if not res:
370             return [('id', '=', '0')]
371         return [('id', 'in', [x[0] for x in res])]
372
373     def _invoice_search(self, cursor, user, obj, name, args, context=None):
374         if not args:
375             return []
376         invoice_obj = self.pool.get('account.invoice')
377         i = 0
378         while i < len(args):
379             fargs = args[i][0].split('.', 1)
380             if len(fargs) > 1:
381                 args[i] = (fargs[0], 'in', invoice_obj.search(cursor, user,
382                     [(fargs[1], args[i][1], args[i][2])]))
383                 i += 1
384                 continue
385             if isinstance(args[i][2], basestring):
386                 res_ids = invoice_obj.name_search(cursor, user, args[i][2], [],
387                         args[i][1])
388                 args[i] = (args[i][0], 'in', [x[0] for x in res_ids])
389             i += 1
390         qu1, qu2 = [], []
391         for x in args:
392             if x[1] != 'in':
393                 if (x[2] is False) and (x[1] == '='):
394                     qu1.append('(i.id IS NULL)')
395                 elif (x[2] is False) and (x[1] == '<>' or x[1] == '!='):
396                     qu1.append('(i.id IS NOT NULL)')
397                 else:
398                     qu1.append('(i.id %s %s)' % (x[1], '%s'))
399                     qu2.append(x[2])
400             elif x[1] == 'in':
401                 if len(x[2]) > 0:
402                     qu1.append('(i.id IN (%s))' % (','.join(['%s'] * len(x[2]))))
403                     qu2 += x[2]
404                 else:
405                     qu1.append(' (False)')
406         if qu1:
407             qu1 = ' AND' + ' AND'.join(qu1)
408         else:
409             qu1 = ''
410         cursor.execute('SELECT l.id ' \
411                 'FROM account_move_line l, account_invoice i ' \
412                 'WHERE l.move_id = i.move_id ' + qu1, qu2)
413         res = cursor.fetchall()
414         if not res:
415             return [('id', '=', '0')]
416         return [('id', 'in', [x[0] for x in res])]
417
418     def _get_move_lines(self, cr, uid, ids, context=None):
419         result = []
420         for move in self.pool.get('account.move').browse(cr, uid, ids, context=context):
421             for line in move.line_id:
422                 result.append(line.id)
423         return result
424
425     def _get_reconcile(self, cr, uid, ids,name, unknow_none, context=None):
426         res = dict.fromkeys(ids, False)
427         for line in self.browse(cr, uid, ids, context=context):
428             if line.reconcile_id:
429                 res[line.id] = str(line.reconcile_id.name)
430             elif line.reconcile_partial_id:
431                 res[line.id] = str(line.reconcile_partial_id.name)
432         return res
433
434     _columns = {
435         'name': fields.char('Name', size=64, required=True),
436         'quantity': fields.float('Quantity', digits=(16,2), help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports."),
437         'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'),
438         'product_id': fields.many2one('product.product', 'Product'),
439         'debit': fields.float('Debit', digits_compute=dp.get_precision('Account')),
440         'credit': fields.float('Credit', digits_compute=dp.get_precision('Account')),
441         'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", domain=[('type','<>','view'), ('type', '<>', 'closed')], select=2),
442         'move_id': fields.many2one('account.move', 'Journal Entry', ondelete="cascade", help="The move of this entry line.", select=2, required=True),
443         'narration': fields.related('move_id','narration', type='text', relation='account.move', string='Internal Note'),
444         'ref': fields.related('move_id', 'ref', string='Reference', type='char', size=64, store=True),
445         'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1),
446         'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2),
447         'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2),
448         'reconcile': fields.function(_get_reconcile, type='char', string='Reconcile Ref'),
449         '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')),
450         'amount_residual_currency': fields.function(_amount_residual, string='Residual Amount in Currency', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in its currency (maybe different of the company currency)."),
451         'amount_residual': fields.function(_amount_residual, string='Residual Amount', multi="residual", help="The residual amount on a receivable or payable of a journal entry expressed in the company currency."),
452         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
453         'journal_id': fields.related('move_id', 'journal_id', string='Journal', type='many2one', relation='account.journal', required=True, select=True,
454                                 store = {
455                                     'account.move': (_get_move_lines, ['journal_id'], 20)
456                                 }),
457         'period_id': fields.related('move_id', 'period_id', string='Period', type='many2one', relation='account.period', required=True, select=True,
458                                 store = {
459                                     'account.move': (_get_move_lines, ['period_id'], 20)
460                                 }),
461         'blocked': fields.boolean('No Follow-up', help="You can check this box to mark this journal item as a litigation with the associated partner"),
462         'partner_id': fields.many2one('res.partner', 'Partner', select=1, ondelete='restrict'),
463         'date_maturity': fields.date('Due date', select=True ,help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line."),
464         'date': fields.related('move_id','date', string='Effective date', type='date', required=True, select=True,
465                                 store = {
466                                     'account.move': (_get_move_lines, ['date'], 20)
467                                 }),
468         'date_created': fields.date('Creation date', select=True),
469         'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'),
470         'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation'),('currency','Currency Adjustment')], 'Centralisation', size=8),
471         'balance': fields.function(_balance, fnct_search=_balance_search, string='Balance'),
472         'state': fields.selection([('draft','Unbalanced'), ('valid','Balanced')], 'Status', readonly=True),
473         'tax_code_id': fields.many2one('account.tax.code', 'Tax Account', help="The Account can either be a base tax code or a tax code account."),
474         'tax_amount': fields.float('Tax/Base Amount', digits_compute=dp.get_precision('Account'), select=True, help="If the Tax account is a tax code account, this field will contain the taxed amount.If the tax account is base tax code, "\
475                     "this field will contain the basic amount(without tax)."),
476         'invoice': fields.function(_invoice, string='Invoice',
477             type='many2one', relation='account.invoice', fnct_search=_invoice_search),
478         'account_tax_id':fields.many2one('account.tax', 'Tax'),
479         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
480         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', 
481                             string='Company', store=True, readonly=True)
482     }
483
484     def _get_date(self, cr, uid, context=None):
485         if context is None:
486             context or {}
487         period_obj = self.pool.get('account.period')
488         dt = time.strftime('%Y-%m-%d')
489         if context.get('journal_id') and context.get('period_id'):
490             cr.execute('SELECT date FROM account_move_line ' \
491                     'WHERE journal_id = %s AND period_id = %s ' \
492                     'ORDER BY id DESC limit 1',
493                     (context['journal_id'], context['period_id']))
494             res = cr.fetchone()
495             if res:
496                 dt = res[0]
497             else:
498                 period = period_obj.browse(cr, uid, context['period_id'], context=context)
499                 dt = period.date_start
500         return dt
501
502     def _get_currency(self, cr, uid, context=None):
503         if context is None:
504             context = {}
505         if not context.get('journal_id', False):
506             return False
507         cur = self.pool.get('account.journal').browse(cr, uid, context['journal_id']).currency
508         return cur and cur.id or False
509
510     def _get_period(self, cr, uid, context=None):
511         """
512         Return  default account period value
513         """
514         context = context or {}
515         if context.get('period_id', False):
516             return context['period_id']
517         account_period_obj = self.pool.get('account.period')
518         ctx = dict(context, account_period_prefer_normal=True)
519         ids = account_period_obj.find(cr, uid, context=ctx)
520         period_id = False
521         if ids:
522             period_id = ids[0]
523         return period_id
524
525     def _get_journal(self, cr, uid, context=None):
526         """
527         Return journal based on the journal type
528         """
529         context = context or {}
530         if context.get('journal_id', False):
531             return context['journal_id']
532         journal_id = False
533
534         journal_pool = self.pool.get('account.journal')
535         if context.get('journal_type', False):
536             jids = journal_pool.search(cr, uid, [('type','=', context.get('journal_type'))])
537             if not jids:
538                 raise osv.except_osv(_('Configuration Error!'), _('Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration/Journals/Journals.') % context.get('journal_type'))
539             journal_id = jids[0]
540         return journal_id
541
542
543     _defaults = {
544         'blocked': False,
545         'centralisation': 'normal',
546         'date': _get_date,
547         'date_created': fields.date.context_today,
548         'state': 'draft',
549         'currency_id': _get_currency,
550         'journal_id': _get_journal,
551         'credit': 0.0,
552         'debit': 0.0,
553         'amount_currency': 0.0,
554         'account_id': lambda self, cr, uid, c: c.get('account_id', False),
555         'period_id': _get_period,
556         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.move.line', context=c)
557     }
558     _order = "date desc, id desc"
559     _sql_constraints = [
560         ('credit_debit1', 'CHECK (credit*debit=0)',  'Wrong credit or debit value in accounting entry !'),
561         ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
562     ]
563
564     def _auto_init(self, cr, context=None):
565         res = super(account_move_line, self)._auto_init(cr, context=context)
566         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'')
567         if not cr.fetchone():
568             cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)')
569         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('account_move_line_date_id_index',))
570         if not cr.fetchone():
571             cr.execute('CREATE INDEX account_move_line_date_id_index ON account_move_line (date DESC, id desc)')
572         return res
573
574     def _check_no_view(self, cr, uid, ids, context=None):
575         lines = self.browse(cr, uid, ids, context=context)
576         for l in lines:
577             if l.account_id.type in ('view', 'consolidation'):
578                 return False
579         return True
580
581     def _check_no_closed(self, cr, uid, ids, context=None):
582         lines = self.browse(cr, uid, ids, context=context)
583         for l in lines:
584             if l.account_id.type == 'closed':
585                 raise osv.except_osv(_('Error!'), _('You cannot create journal items on a closed account %s %s.') % (l.account_id.code, l.account_id.name))
586         return True
587
588     def _check_company_id(self, cr, uid, ids, context=None):
589         lines = self.browse(cr, uid, ids, context=context)
590         for l in lines:
591             if l.company_id != l.account_id.company_id or l.company_id != l.period_id.company_id:
592                 return False
593         return True
594
595     def _check_date(self, cr, uid, ids, context=None):
596         for l in self.browse(cr, uid, ids, context=context):
597             if l.journal_id.allow_date:
598                 if not time.strptime(l.date[:10],'%Y-%m-%d') >= time.strptime(l.period_id.date_start, '%Y-%m-%d') or not time.strptime(l.date[:10], '%Y-%m-%d') <= time.strptime(l.period_id.date_stop, '%Y-%m-%d'):
599                     return False
600         return True
601
602     def _check_currency(self, cr, uid, ids, context=None):
603         for l in self.browse(cr, uid, ids, context=context):
604             if l.account_id.currency_id:
605                 if not l.currency_id or not l.currency_id.id == l.account_id.currency_id.id:
606                     return False
607         return True
608
609     def _check_currency_and_amount(self, cr, uid, ids, context=None):
610         for l in self.browse(cr, uid, ids, context=context):
611             if (l.amount_currency and not l.currency_id):
612                 return False
613         return True
614
615     def _check_currency_amount(self, cr, uid, ids, context=None):
616         for l in self.browse(cr, uid, ids, context=context):
617             if l.amount_currency:
618                 if (l.amount_currency > 0.0 and l.credit > 0.0) or (l.amount_currency < 0.0 and l.debit > 0.0):
619                     return False
620         return True
621
622     def _check_currency_company(self, cr, uid, ids, context=None):
623         for l in self.browse(cr, uid, ids, context=context):
624             if l.currency_id.id == l.company_id.currency_id.id:
625                 return False
626         return True
627
628     _constraints = [
629         (_check_no_view, 'You cannot create journal items on an account of type view or consolidation.', ['account_id']),
630         (_check_no_closed, 'You cannot create journal items on closed account.', ['account_id']),
631         (_check_company_id, 'Account and Period must belong to the same company.', ['company_id']),
632         (_check_date, 'The date of your Journal Entry is not in the defined period! You should change the date or remove this constraint from the journal.', ['date']),
633         (_check_currency, 'The selected account of your Journal Entry forces to provide a secondary currency. You should remove the secondary currency on the account or select a multi-currency view on the journal.', ['currency_id']),
634         (_check_currency_and_amount, "You cannot create journal items with a secondary currency without recording both 'currency' and 'amount currency' field.", ['currency_id','amount_currency']),
635         (_check_currency_amount, 'The amount expressed in the secondary currency must be positive when the journal item is a debit and negative when if it is a credit.', ['amount_currency']),
636         (_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']),
637     ]
638
639     #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id
640     def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False, context=None):
641         if context is None:
642             context = {}
643         account_obj = self.pool.get('account.account')
644         journal_obj = self.pool.get('account.journal')
645         currency_obj = self.pool.get('res.currency')
646         if (not currency_id) or (not account_id):
647             return {}
648         result = {}
649         acc = account_obj.browse(cr, uid, account_id, context=context)
650         if (amount>0) and journal:
651             x = journal_obj.browse(cr, uid, journal).default_credit_account_id
652             if x: acc = x
653         context.update({
654                 'date': date,
655                 'res.currency.compute.account': acc,
656             })
657         v = currency_obj.compute(cr, uid, currency_id, acc.company_id.currency_id.id, amount, context=context)
658         result['value'] = {
659             'debit': v > 0 and v or 0.0,
660             'credit': v < 0 and -v or 0.0
661         }
662         return result
663
664     def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False, context=None):
665         partner_obj = self.pool.get('res.partner')
666         payment_term_obj = self.pool.get('account.payment.term')
667         journal_obj = self.pool.get('account.journal')
668         fiscal_pos_obj = self.pool.get('account.fiscal.position')
669         val = {}
670         val['date_maturity'] = False
671
672         if not partner_id:
673             return {'value':val}
674         if not date:
675             date = datetime.now().strftime('%Y-%m-%d')
676         jt = False
677         if journal:
678             jt = journal_obj.browse(cr, uid, journal, context=context).type
679         part = partner_obj.browse(cr, uid, partner_id, context=context)
680
681         payment_term_id = False
682         if jt and jt in ('purchase', 'purchase_refund') and part.property_supplier_payment_term:
683             payment_term_id = part.property_supplier_payment_term.id
684         elif jt and part.property_payment_term:
685             payment_term_id = part.property_payment_term.id
686         if payment_term_id:
687             res = payment_term_obj.compute(cr, uid, payment_term_id, 100, date)
688             if res:
689                 val['date_maturity'] = res[0][0]
690         if not account_id:
691             id1 = part.property_account_payable.id
692             id2 =  part.property_account_receivable.id
693             if jt:
694                 if jt in ('sale', 'purchase_refund'):
695                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
696                 elif jt in ('purchase', 'sale_refund'):
697                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
698                 elif jt in ('general', 'bank', 'cash'):
699                     if part.customer:
700                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
701                     elif part.supplier:
702                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
703                 if val.get('account_id', False):
704                     d = self.onchange_account_id(cr, uid, ids, account_id=val['account_id'], partner_id=part.id, context=context)
705                     val.update(d['value'])
706         return {'value':val}
707
708     def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False, context=None):
709         account_obj = self.pool.get('account.account')
710         partner_obj = self.pool.get('res.partner')
711         fiscal_pos_obj = self.pool.get('account.fiscal.position')
712         val = {}
713         if account_id:
714             res = account_obj.browse(cr, uid, account_id, context=context)
715             tax_ids = res.tax_ids
716             if tax_ids and partner_id:
717                 part = partner_obj.browse(cr, uid, partner_id, context=context)
718                 tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
719             else:
720                 tax_id = tax_ids and tax_ids[0].id or False
721             val['account_tax_id'] = tax_id
722         return {'value': val}
723     #
724     # type: the type if reconciliation (no logic behind this field, for info)
725     #
726     # writeoff; entry generated for the difference between the lines
727     #
728     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
729         if context is None:
730             context = {}
731         if context and context.get('next_partner_only', False):
732             if not context.get('partner_id', False):
733                 partner = self.list_partners_to_reconcile(cr, uid, context=context)
734                 if partner:
735                     partner = partner[0]
736             else:
737                 partner = context.get('partner_id', False)
738             if not partner:
739                 return []
740             args.append(('partner_id', '=', partner[0]))
741         return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
742
743     def list_partners_to_reconcile(self, cr, uid, context=None):
744         cr.execute(
745              """SELECT partner_id FROM (
746                 SELECT l.partner_id, p.last_reconciliation_date, SUM(l.debit) AS debit, SUM(l.credit) AS credit, MAX(l.create_date) AS max_date
747                 FROM account_move_line l
748                 RIGHT JOIN account_account a ON (a.id = l.account_id)
749                 RIGHT JOIN res_partner p ON (l.partner_id = p.id)
750                     WHERE a.reconcile IS TRUE
751                     AND l.reconcile_id IS NULL
752                     AND l.state <> 'draft'
753                     GROUP BY l.partner_id, p.last_reconciliation_date
754                 ) AS s
755                 WHERE debit > 0 AND credit > 0 AND (last_reconciliation_date IS NULL OR max_date > last_reconciliation_date)
756                 ORDER BY last_reconciliation_date""")
757         ids = [x[0] for x in cr.fetchall()]
758         if not ids: 
759             return []
760
761         # To apply the ir_rules
762         partner_obj = self.pool.get('res.partner')
763         ids = partner_obj.search(cr, uid, [('id', 'in', ids)], context=context)
764         return partner_obj.name_get(cr, uid, ids, context=context)
765
766     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
767         move_rec_obj = self.pool.get('account.move.reconcile')
768         merges = []
769         unmerge = []
770         total = 0.0
771         merges_rec = []
772         company_list = []
773         if context is None:
774             context = {}
775         for line in self.browse(cr, uid, ids, context=context):
776             if company_list and not line.company_id.id in company_list:
777                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
778             company_list.append(line.company_id.id)
779
780         for line in self.browse(cr, uid, ids, context=context):
781             if line.account_id.currency_id:
782                 currency_id = line.account_id.currency_id
783             else:
784                 currency_id = line.company_id.currency_id
785             if line.reconcile_id:
786                 raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name)) 
787             if line.reconcile_partial_id:
788                 for line2 in line.reconcile_partial_id.line_partial_ids:
789                     if not line2.reconcile_id:
790                         if line2.id not in merges:
791                             merges.append(line2.id)
792                         if line2.account_id.currency_id:
793                             total += line2.amount_currency
794                         else:
795                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
796                 merges_rec.append(line.reconcile_partial_id.id)
797             else:
798                 unmerge.append(line.id)
799                 if line.account_id.currency_id:
800                     total += line.amount_currency
801                 else:
802                     total += (line.debit or 0.0) - (line.credit or 0.0)
803         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
804             res = self.reconcile(cr, uid, merges+unmerge, context=context, writeoff_acc_id=writeoff_acc_id, writeoff_period_id=writeoff_period_id, writeoff_journal_id=writeoff_journal_id)
805             return res
806         # marking the lines as reconciled does not change their validity, so there is no need
807         # to revalidate their moves completely.
808         reconcile_context = dict(context, novalidate=True)
809         r_id = move_rec_obj.create(cr, uid, {
810             'type': type,
811             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
812         }, context=reconcile_context)
813         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=reconcile_context)
814         return True
815
816     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
817         account_obj = self.pool.get('account.account')
818         move_obj = self.pool.get('account.move')
819         move_rec_obj = self.pool.get('account.move.reconcile')
820         partner_obj = self.pool.get('res.partner')
821         currency_obj = self.pool.get('res.currency')
822         lines = self.browse(cr, uid, ids, context=context)
823         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
824         credit = debit = 0.0
825         currency = 0.0
826         account_id = False
827         partner_id = False
828         if context is None:
829             context = {}
830         company_list = []
831         for line in self.browse(cr, uid, ids, context=context):
832             if company_list and not line.company_id.id in company_list:
833                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
834             company_list.append(line.company_id.id)
835         for line in unrec_lines:
836             if line.state <> 'valid':
837                 raise osv.except_osv(_('Error!'),
838                         _('Entry "%s" is not valid !') % line.name)
839             credit += line['credit']
840             debit += line['debit']
841             currency += line['amount_currency'] or 0.0
842             account_id = line['account_id']['id']
843             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
844         writeoff = debit - credit
845
846         # Ifdate_p in context => take this date
847         if context.has_key('date_p') and context['date_p']:
848             date=context['date_p']
849         else:
850             date = time.strftime('%Y-%m-%d')
851
852         cr.execute('SELECT account_id, reconcile_id '\
853                    'FROM account_move_line '\
854                    'WHERE id IN %s '\
855                    'GROUP BY account_id,reconcile_id',
856                    (tuple(ids), ))
857         r = cr.fetchall()
858         #TODO: move this check to a constraint in the account_move_reconcile object
859         if len(r) != 1:
860             raise osv.except_osv(_('Error'), _('Entries are not of the same account or already reconciled ! '))
861         if not unrec_lines:
862             raise osv.except_osv(_('Error!'), _('Entry is already reconciled.'))
863         account = account_obj.browse(cr, uid, account_id, context=context)
864         if not account.reconcile:
865             raise osv.except_osv(_('Error'), _('The account is not defined to be reconciled !'))
866         if r[0][1] != None:
867             raise osv.except_osv(_('Error!'), _('Some entries are already reconciled.'))
868
869         if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
870            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
871             if not writeoff_acc_id:
872                 raise osv.except_osv(_('Warning!'), _('You have to provide an account for the write off/exchange difference entry.'))
873             if writeoff > 0:
874                 debit = writeoff
875                 credit = 0.0
876                 self_credit = writeoff
877                 self_debit = 0.0
878             else:
879                 debit = 0.0
880                 credit = -writeoff
881                 self_credit = 0.0
882                 self_debit = -writeoff
883             # If comment exist in context, take it
884             if 'comment' in context and context['comment']:
885                 libelle = context['comment']
886             else:
887                 libelle = _('Write-Off')
888
889             cur_obj = self.pool.get('res.currency')
890             cur_id = False
891             amount_currency_writeoff = 0.0
892             if context.get('company_currency_id',False) != context.get('currency_id',False):
893                 cur_id = context.get('currency_id',False)
894                 for line in unrec_lines:
895                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
896                         amount_currency_writeoff += line.amount_currency
897                     else:
898                         tmp_amount = cur_obj.compute(cr, uid, line.account_id.company_id.currency_id.id, context.get('currency_id',False), abs(line.debit-line.credit), context={'date': line.date})
899                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
900
901             writeoff_lines = [
902                 (0, 0, {
903                     'name': libelle,
904                     'debit': self_debit,
905                     'credit': self_credit,
906                     'account_id': account_id,
907                     'date': date,
908                     'partner_id': partner_id,
909                     'currency_id': cur_id or (account.currency_id.id or False),
910                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
911                 }),
912                 (0, 0, {
913                     'name': libelle,
914                     'debit': debit,
915                     'credit': credit,
916                     'account_id': writeoff_acc_id,
917                     'analytic_account_id': context.get('analytic_id', False),
918                     'date': date,
919                     'partner_id': partner_id,
920                     'currency_id': cur_id or (account.currency_id.id or False),
921                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
922                 })
923             ]
924
925             writeoff_move_id = move_obj.create(cr, uid, {
926                 'period_id': writeoff_period_id,
927                 'journal_id': writeoff_journal_id,
928                 'date':date,
929                 'state': 'draft',
930                 'line_id': writeoff_lines
931             })
932
933             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
934             if account_id == writeoff_acc_id:
935                 writeoff_line_ids = [writeoff_line_ids[1]]
936             ids += writeoff_line_ids
937
938         # marking the lines as reconciled does not change their validity, so there is no need
939         # to revalidate their moves completely.
940         reconcile_context = dict(context, novalidate=True)
941         r_id = move_rec_obj.create(cr, uid, {
942             'type': type,
943             'line_id': map(lambda x: (4, x, False), ids),
944             'line_partial_ids': map(lambda x: (3, x, False), ids)
945         }, context=reconcile_context)
946         wf_service = netsvc.LocalService("workflow")
947         # the id of the move.reconcile is written in the move.line (self) by the create method above
948         # because of the way the line_id are defined: (4, x, False)
949         for id in ids:
950             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
951
952         if lines and lines[0]:
953             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
954             if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context):
955                 partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context)
956         return r_id
957
958     def view_header_get(self, cr, user, view_id, view_type, context=None):
959         if context is None:
960             context = {}
961         context = self.convert_to_period(cr, user, context=context)
962         if context.get('account_id', False):
963             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
964             res = cr.fetchone()
965             if res:
966                 res = _('Entries: ')+ (res[0] or '')
967             return res
968         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
969             return False
970         if context.get('search_default_journal_id', False):
971             context['journal_id'] = context.get('search_default_journal_id')
972         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
973         j = cr.fetchone()[0] or ''
974         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
975         p = cr.fetchone()[0] or ''
976         if j or p:
977             return j + (p and (':' + p) or '')
978         return False
979
980     def onchange_date(self, cr, user, ids, date, context=None):
981         """
982         Returns a dict that contains new values and context
983         @param cr: A database cursor
984         @param user: ID of the user currently logged in
985         @param date: latest value from user input for field date
986         @param args: other arguments
987         @param context: context arguments, like lang, time zone
988         @return: Returns a dict which contains new values, and context
989         """
990         res = {}
991         if context is None:
992             context = {}
993         period_pool = self.pool.get('account.period')
994         ctx = dict(context, account_period_prefer_normal=True)
995         pids = period_pool.find(cr, user, date, context=ctx)
996         if pids:
997             res.update({
998                 'period_id':pids[0]
999             })
1000             context.update({
1001                 'period_id':pids[0]
1002             })
1003         return {
1004             'value':res,
1005             'context':context,
1006         }
1007
1008     def _check_moves(self, cr, uid, context=None):
1009         # use the first move ever created for this journal and period
1010         if context is None:
1011             context = {}
1012         cr.execute('SELECT id, state, name FROM account_move WHERE journal_id = %s AND period_id = %s ORDER BY id limit 1', (context['journal_id'],context['period_id']))
1013         res = cr.fetchone()
1014         if res:
1015             if res[1] != 'draft':
1016                 raise osv.except_osv(_('User Error!'),
1017                        _('The account move (%s) for centralisation ' \
1018                                 'has been confirmed.') % res[2])
1019         return res
1020
1021     def _remove_move_reconcile(self, cr, uid, move_ids=None, opening_reconciliation=False, context=None):
1022         # Function remove move rencocile ids related with moves
1023         obj_move_line = self.pool.get('account.move.line')
1024         obj_move_rec = self.pool.get('account.move.reconcile')
1025         unlink_ids = []
1026         if not move_ids:
1027             return True
1028         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1029         full_recs = filter(lambda x: x['reconcile_id'], recs)
1030         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1031         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1032         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1033         unlink_ids += rec_ids
1034         unlink_ids += part_rec_ids
1035         all_moves = obj_move_line.search(cr, uid, ['|',('reconcile_id', 'in', unlink_ids),('reconcile_partial_id', 'in', unlink_ids)])
1036         all_moves = list(set(all_moves) - set(move_ids))
1037         if unlink_ids:
1038             if opening_reconciliation:
1039                 raise osv.except_osv(_('Warning!'),
1040                     _('Opening Entries have already been generated.  Please run "Cancel Closing Entries" wizard to cancel those entries and then run this wizard.'))
1041                 obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
1042             obj_move_rec.unlink(cr, uid, unlink_ids)
1043             if len(all_moves) >= 2:
1044                 obj_move_line.reconcile_partial(cr, uid, all_moves, 'auto',context=context)
1045         return True
1046
1047     def unlink(self, cr, uid, ids, context=None, check=True):
1048         if context is None:
1049             context = {}
1050         move_obj = self.pool.get('account.move')
1051         self._update_check(cr, uid, ids, context)
1052         result = False
1053         move_ids = set()
1054         for line in self.browse(cr, uid, ids, context=context):
1055             move_ids.add(line.move_id.id)
1056             context['journal_id'] = line.journal_id.id
1057             context['period_id'] = line.period_id.id
1058             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1059         move_ids = list(move_ids)
1060         if check and move_ids:
1061             move_obj.validate(cr, uid, move_ids, context=context)
1062         return result
1063
1064     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1065         if context is None:
1066             context={}
1067         move_obj = self.pool.get('account.move')
1068         account_obj = self.pool.get('account.account')
1069         journal_obj = self.pool.get('account.journal')
1070         if isinstance(ids, (int, long)):
1071             ids = [ids]
1072         if vals.get('account_tax_id', False):
1073             raise osv.except_osv(_('Unable to change tax!'), _('You cannot change the tax, you should remove and recreate lines.'))
1074         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1075             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1076         if update_check:
1077             if ('account_id' in vals) or ('journal_id' in vals) or ('period_id' in vals) or ('move_id' in vals) or ('debit' in vals) or ('credit' in vals) or ('date' in vals):
1078                 self._update_check(cr, uid, ids, context)
1079
1080         todo_date = None
1081         if vals.get('date', False):
1082             todo_date = vals['date']
1083             del vals['date']
1084
1085         for line in self.browse(cr, uid, ids, context=context):
1086             ctx = context.copy()
1087             if not ctx.get('journal_id'):
1088                 if line.move_id:
1089                    ctx['journal_id'] = line.move_id.journal_id.id
1090                 else:
1091                     ctx['journal_id'] = line.journal_id.id
1092             if not ctx.get('period_id'):
1093                 if line.move_id:
1094                     ctx['period_id'] = line.move_id.period_id.id
1095                 else:
1096                     ctx['period_id'] = line.period_id.id
1097             #Check for centralisation
1098             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1099             if journal.centralisation:
1100                 self._check_moves(cr, uid, context=ctx)
1101         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1102         if check:
1103             done = []
1104             for line in self.browse(cr, uid, ids):
1105                 if line.move_id.id not in done:
1106                     done.append(line.move_id.id)
1107                     move_obj.validate(cr, uid, [line.move_id.id], context)
1108                     if todo_date:
1109                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1110         return result
1111
1112     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1113         journal_obj = self.pool.get('account.journal')
1114         period_obj = self.pool.get('account.period')
1115         jour_period_obj = self.pool.get('account.journal.period')
1116         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1117         result = cr.fetchall()
1118         journal = journal_obj.browse(cr, uid, journal_id, context=context)
1119         period = period_obj.browse(cr, uid, period_id, context=context)
1120         for (state,) in result:
1121             if state == 'done':
1122                 raise osv.except_osv(_('Error!'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
1123         if not result:
1124             jour_period_obj.create(cr, uid, {
1125                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1126                 'journal_id': journal.id,
1127                 'period_id': period.id
1128             })
1129         return True
1130
1131     def _update_check(self, cr, uid, ids, context=None):
1132         done = {}
1133         for line in self.browse(cr, uid, ids, context=context):
1134             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1135             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1136                 raise osv.except_osv(_('Error!'), _('You cannot do this modification on a confirmed entry. You can just change some non legal fields or you must unconfirm the journal entry first.\n%s.') % err_msg)
1137             if line.reconcile_id:
1138                 raise osv.except_osv(_('Error!'), _('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg)
1139             t = (line.journal_id.id, line.period_id.id)
1140             if t not in done:
1141                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1142                 done[t] = True
1143         return True
1144
1145     def create(self, cr, uid, vals, context=None, check=True):
1146         account_obj = self.pool.get('account.account')
1147         tax_obj = self.pool.get('account.tax')
1148         move_obj = self.pool.get('account.move')
1149         cur_obj = self.pool.get('res.currency')
1150         journal_obj = self.pool.get('account.journal')
1151         if context is None:
1152             context = {}
1153         if vals.get('move_id', False):
1154             move = self.pool.get('account.move').browse(cr, uid, vals['move_id'], context=context)
1155             if move.company_id:
1156                 vals['company_id'] = move.company_id.id
1157             if move.date and not vals.get('date'):
1158                 vals['date'] = move.date
1159         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1160             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1161         if 'journal_id' in vals and vals['journal_id']:
1162             context['journal_id'] = vals['journal_id']
1163         if 'period_id' in vals and vals['period_id']:
1164             context['period_id'] = vals['period_id']
1165         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1166             m = move_obj.browse(cr, uid, vals['move_id'])
1167             context['journal_id'] = m.journal_id.id
1168             context['period_id'] = m.period_id.id
1169         #we need to treat the case where a value is given in the context for period_id as a string
1170         if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)):
1171             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1172             if len(period_candidate_ids) != 1:
1173                 raise osv.except_osv(_('Error!'), _('No period found or more than one period found for the given date.'))
1174             context['period_id'] = period_candidate_ids[0][0]
1175         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1176             context['journal_id'] = context.get('search_default_journal_id')
1177         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1178         move_id = vals.get('move_id', False)
1179         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1180         vals['journal_id'] = vals.get('journal_id') or context.get('journal_id')
1181         vals['period_id'] = vals.get('period_id') or context.get('period_id')
1182         vals['date'] = vals.get('date') or context.get('date')
1183         if not move_id:
1184             if journal.centralisation:
1185                 #Check for centralisation
1186                 res = self._check_moves(cr, uid, context)
1187                 if res:
1188                     vals['move_id'] = res[0]
1189             if not vals.get('move_id', False):
1190                 if journal.sequence_id:
1191                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1192                     v = {
1193                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1194                         'period_id': context['period_id'],
1195                         'journal_id': context['journal_id']
1196                     }
1197                     if vals.get('ref', ''):
1198                         v.update({'ref': vals['ref']})
1199                     move_id = move_obj.create(cr, uid, v, context)
1200                     vals['move_id'] = move_id
1201                 else:
1202                     raise osv.except_osv(_('No Piece Number!'), _('Cannot create an automatic sequence for this piece.\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.'))
1203         ok = not (journal.type_control_ids or journal.account_control_ids)
1204         if ('account_id' in vals):
1205             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1206             if journal.type_control_ids:
1207                 type = account.user_type
1208                 for t in journal.type_control_ids:
1209                     if type.code == t.code:
1210                         ok = True
1211                         break
1212             if journal.account_control_ids and not ok:
1213                 for a in journal.account_control_ids:
1214                     if a.id == vals['account_id']:
1215                         ok = True
1216                         break
1217             # Automatically convert in the account's secondary currency if there is one and
1218             # the provided values were not already multi-currency
1219             if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
1220                 vals['currency_id'] = account.currency_id.id
1221                 ctx = {}
1222                 if 'date' in vals:
1223                     ctx['date'] = vals['date']
1224                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1225                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1226         if not ok:
1227             raise osv.except_osv(_('Bad Account!'), _('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
1228
1229         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1230         # CREATE Taxes
1231         if vals.get('account_tax_id', False):
1232             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1233             total = vals['debit'] - vals['credit']
1234             if journal.type in ('purchase_refund', 'sale_refund'):
1235                 base_code = 'ref_base_code_id'
1236                 tax_code = 'ref_tax_code_id'
1237                 account_id = 'account_paid_id'
1238                 base_sign = 'ref_base_sign'
1239                 tax_sign = 'ref_tax_sign'
1240             else:
1241                 base_code = 'base_code_id'
1242                 tax_code = 'tax_code_id'
1243                 account_id = 'account_collected_id'
1244                 base_sign = 'base_sign'
1245                 tax_sign = 'tax_sign'
1246             tmp_cnt = 0
1247             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1248                 #create the base movement
1249                 if tmp_cnt == 0:
1250                     if tax[base_code]:
1251                         tmp_cnt += 1
1252                         self.write(cr, uid,[result], {
1253                             'tax_code_id': tax[base_code],
1254                             'tax_amount': tax[base_sign] * abs(total)
1255                         })
1256                 else:
1257                     data = {
1258                         'move_id': vals['move_id'],
1259                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1260                         'date': vals['date'],
1261                         'partner_id': vals.get('partner_id',False),
1262                         'ref': vals.get('ref',False),
1263                         'account_tax_id': False,
1264                         'tax_code_id': tax[base_code],
1265                         'tax_amount': tax[base_sign] * abs(total),
1266                         'account_id': vals['account_id'],
1267                         'credit': 0.0,
1268                         'debit': 0.0,
1269                     }
1270                     if data['tax_code_id']:
1271                         self.create(cr, uid, data, context)
1272                 #create the Tax movement
1273                 data = {
1274                     'move_id': vals['move_id'],
1275                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1276                     'date': vals['date'],
1277                     'partner_id': vals.get('partner_id',False),
1278                     'ref': vals.get('ref',False),
1279                     'account_tax_id': False,
1280                     'tax_code_id': tax[tax_code],
1281                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1282                     'account_id': tax[account_id] or vals['account_id'],
1283                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1284                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1285                 }
1286                 if data['tax_code_id']:
1287                     self.create(cr, uid, data, context)
1288             del vals['account_tax_id']
1289
1290         if check and not context.get('novalidate') and ((not context.get('no_store_function')) or journal.entry_posted):
1291             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1292             if journal.entry_posted and tmp:
1293                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1294         return result
1295
1296     def list_periods(self, cr, uid, context=None):
1297         ids = self.pool.get('account.period').search(cr,uid,[])
1298         return self.pool.get('account.period').name_get(cr, uid, ids, context=context)
1299
1300     def list_journals(self, cr, uid, context=None):
1301         ng = dict(self.pool.get('account.journal').name_search(cr,uid,'',[]))
1302         ids = ng.keys()
1303         result = []
1304         for journal in self.pool.get('account.journal').browse(cr, uid, ids, context=context):
1305             result.append((journal.id,ng[journal.id],journal.type,
1306                 bool(journal.currency),bool(journal.analytic_journal_id)))
1307         return result
1308
1309 account_move_line()
1310
1311 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: