Launchpad automatic translations update.
[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         return res
570
571     def _check_no_view(self, cr, uid, ids, context=None):
572         lines = self.browse(cr, uid, ids, context=context)
573         for l in lines:
574             if l.account_id.type == 'view':
575                 return False
576         return True
577
578     def _check_no_closed(self, cr, uid, ids, context=None):
579         lines = self.browse(cr, uid, ids, context=context)
580         for l in lines:
581             if l.account_id.type == 'closed':
582                 raise osv.except_osv(_('Error!'), _('You cannot create journal items on a closed account %s %s.') % (l.account_id.code, l.account_id.name))
583         return True
584
585     def _check_company_id(self, cr, uid, ids, context=None):
586         lines = self.browse(cr, uid, ids, context=context)
587         for l in lines:
588             if l.company_id != l.account_id.company_id or l.company_id != l.period_id.company_id:
589                 return False
590         return True
591
592     def _check_date(self, cr, uid, ids, context=None):
593         for l in self.browse(cr, uid, ids, context=context):
594             if l.journal_id.allow_date:
595                 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'):
596                     return False
597         return True
598
599     def _check_currency(self, cr, uid, ids, context=None):
600         for l in self.browse(cr, uid, ids, context=context):
601             if l.account_id.currency_id:
602                 if not l.currency_id or not l.currency_id.id == l.account_id.currency_id.id:
603                     return False
604         return True
605
606     def _check_currency_and_amount(self, cr, uid, ids, context=None):
607         for l in self.browse(cr, uid, ids, context=context):
608             if (l.amount_currency and not l.currency_id):
609                 return False
610         return True
611
612     def _check_currency_amount(self, cr, uid, ids, context=None):
613         for l in self.browse(cr, uid, ids, context=context):
614             if l.amount_currency:
615                 if (l.amount_currency > 0.0 and l.credit > 0.0) or (l.amount_currency < 0.0 and l.debit > 0.0):
616                     return False
617         return True
618
619     def _check_currency_company(self, cr, uid, ids, context=None):
620         for l in self.browse(cr, uid, ids, context=context):
621             if l.currency_id.id == l.company_id.currency_id.id:
622                 return False
623         return True
624
625     _constraints = [
626         (_check_no_view, 'You cannot create journal items on an account of type view.', ['account_id']),
627         (_check_no_closed, 'You cannot create journal items on closed account.', ['account_id']),
628         (_check_company_id, 'Account and Period must belong to the same company.', ['company_id']),
629         (_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']),
630         (_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']),
631         (_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']),
632         (_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']),
633         (_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']),
634     ]
635
636     #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id
637     def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False, context=None):
638         if context is None:
639             context = {}
640         account_obj = self.pool.get('account.account')
641         journal_obj = self.pool.get('account.journal')
642         currency_obj = self.pool.get('res.currency')
643         if (not currency_id) or (not account_id):
644             return {}
645         result = {}
646         acc = account_obj.browse(cr, uid, account_id, context=context)
647         if (amount>0) and journal:
648             x = journal_obj.browse(cr, uid, journal).default_credit_account_id
649             if x: acc = x
650         context.update({
651                 'date': date,
652                 'res.currency.compute.account': acc,
653             })
654         v = currency_obj.compute(cr, uid, currency_id, acc.company_id.currency_id.id, amount, context=context)
655         result['value'] = {
656             'debit': v > 0 and v or 0.0,
657             'credit': v < 0 and -v or 0.0
658         }
659         return result
660
661     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):
662         partner_obj = self.pool.get('res.partner')
663         payment_term_obj = self.pool.get('account.payment.term')
664         journal_obj = self.pool.get('account.journal')
665         fiscal_pos_obj = self.pool.get('account.fiscal.position')
666         val = {}
667         val['date_maturity'] = False
668
669         if not partner_id:
670             return {'value':val}
671         if not date:
672             date = datetime.now().strftime('%Y-%m-%d')
673         jt = False
674         if journal:
675             jt = journal_obj.browse(cr, uid, journal, context=context).type
676         part = partner_obj.browse(cr, uid, partner_id, context=context)
677
678         payment_term_id = False
679         if jt and jt in ('purchase', 'purchase_refund') and part.property_supplier_payment_term:
680             payment_term_id = part.property_supplier_payment_term.id
681         elif jt and part.property_payment_term:
682             payment_term_id = part.property_payment_term.id
683         if payment_term_id:
684             res = payment_term_obj.compute(cr, uid, payment_term_id, 100, date)
685             if res:
686                 val['date_maturity'] = res[0][0]
687         if not account_id:
688             id1 = part.property_account_payable.id
689             id2 =  part.property_account_receivable.id
690             if jt:
691                 if jt in ('sale', 'purchase_refund'):
692                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
693                 elif jt in ('purchase', 'sale_refund'):
694                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
695                 elif jt in ('general', 'bank', 'cash'):
696                     if part.customer:
697                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
698                     elif part.supplier:
699                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
700                 if val.get('account_id', False):
701                     d = self.onchange_account_id(cr, uid, ids, account_id=val['account_id'], partner_id=part.id, context=context)
702                     val.update(d['value'])
703         return {'value':val}
704
705     def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False, context=None):
706         account_obj = self.pool.get('account.account')
707         partner_obj = self.pool.get('res.partner')
708         fiscal_pos_obj = self.pool.get('account.fiscal.position')
709         val = {}
710         if account_id:
711             res = account_obj.browse(cr, uid, account_id, context=context)
712             tax_ids = res.tax_ids
713             if tax_ids and partner_id:
714                 part = partner_obj.browse(cr, uid, partner_id, context=context)
715                 tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
716             else:
717                 tax_id = tax_ids and tax_ids[0].id or False
718             val['account_tax_id'] = tax_id
719         return {'value': val}
720     #
721     # type: the type if reconciliation (no logic behind this field, for info)
722     #
723     # writeoff; entry generated for the difference between the lines
724     #
725     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
726         if context is None:
727             context = {}
728         if context and context.get('next_partner_only', False):
729             if not context.get('partner_id', False):
730                 partner = self.list_partners_to_reconcile(cr, uid, context=context)
731                 if partner:
732                     partner = partner[0]
733             else:
734                 partner = context.get('partner_id', False)
735             if not partner:
736                 return []
737             args.append(('partner_id', '=', partner[0]))
738         return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
739
740     def list_partners_to_reconcile(self, cr, uid, context=None):
741         cr.execute(
742              """SELECT partner_id FROM (
743                 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
744                 FROM account_move_line l
745                 RIGHT JOIN account_account a ON (a.id = l.account_id)
746                 RIGHT JOIN res_partner p ON (l.partner_id = p.id)
747                     WHERE a.reconcile IS TRUE
748                     AND l.reconcile_id IS NULL
749                     AND l.state <> 'draft'
750                     GROUP BY l.partner_id, p.last_reconciliation_date
751                 ) AS s
752                 WHERE debit > 0 AND credit > 0 AND (last_reconciliation_date IS NULL OR max_date > last_reconciliation_date)
753                 ORDER BY last_reconciliation_date""")
754         ids = [x[0] for x in cr.fetchall()]
755         if not ids: 
756             return []
757
758         # To apply the ir_rules
759         partner_obj = self.pool.get('res.partner')
760         ids = partner_obj.search(cr, uid, [('id', 'in', ids)], context=context)
761         return partner_obj.name_get(cr, uid, ids, context=context)
762
763     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
764         move_rec_obj = self.pool.get('account.move.reconcile')
765         merges = []
766         unmerge = []
767         total = 0.0
768         merges_rec = []
769         company_list = []
770         if context is None:
771             context = {}
772         for line in self.browse(cr, uid, ids, context=context):
773             if company_list and not line.company_id.id in company_list:
774                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
775             company_list.append(line.company_id.id)
776
777         for line in self.browse(cr, uid, ids, context=context):
778             if line.account_id.currency_id:
779                 currency_id = line.account_id.currency_id
780             else:
781                 currency_id = line.company_id.currency_id
782             if line.reconcile_id:
783                 raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name)) 
784             if line.reconcile_partial_id:
785                 for line2 in line.reconcile_partial_id.line_partial_ids:
786                     if not line2.reconcile_id:
787                         if line2.id not in merges:
788                             merges.append(line2.id)
789                         if line2.account_id.currency_id:
790                             total += line2.amount_currency
791                         else:
792                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
793                 merges_rec.append(line.reconcile_partial_id.id)
794             else:
795                 unmerge.append(line.id)
796                 if line.account_id.currency_id:
797                     total += line.amount_currency
798                 else:
799                     total += (line.debit or 0.0) - (line.credit or 0.0)
800         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
801             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)
802             return res
803         r_id = move_rec_obj.create(cr, uid, {
804             'type': type,
805             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
806         }, context=context)
807         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
808         return True
809
810     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
811         account_obj = self.pool.get('account.account')
812         move_obj = self.pool.get('account.move')
813         move_rec_obj = self.pool.get('account.move.reconcile')
814         partner_obj = self.pool.get('res.partner')
815         currency_obj = self.pool.get('res.currency')
816         lines = self.browse(cr, uid, ids, context=context)
817         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
818         credit = debit = 0.0
819         currency = 0.0
820         account_id = False
821         partner_id = False
822         if context is None:
823             context = {}
824         company_list = []
825         for line in self.browse(cr, uid, ids, context=context):
826             if company_list and not line.company_id.id in company_list:
827                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
828             company_list.append(line.company_id.id)
829         for line in unrec_lines:
830             if line.state <> 'valid':
831                 raise osv.except_osv(_('Error!'),
832                         _('Entry "%s" is not valid !') % line.name)
833             credit += line['credit']
834             debit += line['debit']
835             currency += line['amount_currency'] or 0.0
836             account_id = line['account_id']['id']
837             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
838         writeoff = debit - credit
839
840         # Ifdate_p in context => take this date
841         if context.has_key('date_p') and context['date_p']:
842             date=context['date_p']
843         else:
844             date = time.strftime('%Y-%m-%d')
845
846         cr.execute('SELECT account_id, reconcile_id '\
847                    'FROM account_move_line '\
848                    'WHERE id IN %s '\
849                    'GROUP BY account_id,reconcile_id',
850                    (tuple(ids), ))
851         r = cr.fetchall()
852         #TODO: move this check to a constraint in the account_move_reconcile object
853         if len(r) != 1:
854             raise osv.except_osv(_('Error'), _('Entries are not of the same account or already reconciled ! '))
855         if not unrec_lines:
856             raise osv.except_osv(_('Error!'), _('Entry is already reconciled.'))
857         account = account_obj.browse(cr, uid, account_id, context=context)
858         if not account.reconcile:
859             raise osv.except_osv(_('Error'), _('The account is not defined to be reconciled !'))
860         if r[0][1] != None:
861             raise osv.except_osv(_('Error!'), _('Some entries are already reconciled.'))
862
863         if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
864            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
865             if not writeoff_acc_id:
866                 raise osv.except_osv(_('Warning!'), _('You have to provide an account for the write off/exchange difference entry.'))
867             if writeoff > 0:
868                 debit = writeoff
869                 credit = 0.0
870                 self_credit = writeoff
871                 self_debit = 0.0
872             else:
873                 debit = 0.0
874                 credit = -writeoff
875                 self_credit = 0.0
876                 self_debit = -writeoff
877             # If comment exist in context, take it
878             if 'comment' in context and context['comment']:
879                 libelle = context['comment']
880             else:
881                 libelle = _('Write-Off')
882
883             cur_obj = self.pool.get('res.currency')
884             cur_id = False
885             amount_currency_writeoff = 0.0
886             if context.get('company_currency_id',False) != context.get('currency_id',False):
887                 cur_id = context.get('currency_id',False)
888                 for line in unrec_lines:
889                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
890                         amount_currency_writeoff += line.amount_currency
891                     else:
892                         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})
893                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
894
895             writeoff_lines = [
896                 (0, 0, {
897                     'name': libelle,
898                     'debit': self_debit,
899                     'credit': self_credit,
900                     'account_id': account_id,
901                     'date': date,
902                     'partner_id': partner_id,
903                     'currency_id': cur_id or (account.currency_id.id or False),
904                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
905                 }),
906                 (0, 0, {
907                     'name': libelle,
908                     'debit': debit,
909                     'credit': credit,
910                     'account_id': writeoff_acc_id,
911                     'analytic_account_id': context.get('analytic_id', False),
912                     'date': date,
913                     'partner_id': partner_id,
914                     'currency_id': cur_id or (account.currency_id.id or False),
915                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
916                 })
917             ]
918
919             writeoff_move_id = move_obj.create(cr, uid, {
920                 'period_id': writeoff_period_id,
921                 'journal_id': writeoff_journal_id,
922                 'date':date,
923                 'state': 'draft',
924                 'line_id': writeoff_lines
925             })
926
927             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
928             if account_id == writeoff_acc_id:
929                 writeoff_line_ids = [writeoff_line_ids[1]]
930             ids += writeoff_line_ids
931
932         r_id = move_rec_obj.create(cr, uid, {
933             'type': type,
934             'line_id': map(lambda x: (4, x, False), ids),
935             'line_partial_ids': map(lambda x: (3, x, False), ids)
936         })
937         wf_service = netsvc.LocalService("workflow")
938         # the id of the move.reconcile is written in the move.line (self) by the create method above
939         # because of the way the line_id are defined: (4, x, False)
940         for id in ids:
941             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
942
943         if lines and lines[0]:
944             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
945             if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context):
946                 partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context)
947         return r_id
948
949     def view_header_get(self, cr, user, view_id, view_type, context=None):
950         if context is None:
951             context = {}
952         context = self.convert_to_period(cr, user, context=context)
953         if context.get('account_id', False):
954             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
955             res = cr.fetchone()
956             if res:
957                 res = _('Entries: ')+ (res[0] or '')
958             return res
959         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
960             return False
961         if context.get('search_default_journal_id', False):
962             context['journal_id'] = context.get('search_default_journal_id')
963         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
964         j = cr.fetchone()[0] or ''
965         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
966         p = cr.fetchone()[0] or ''
967         if j or p:
968             return j + (p and (':' + p) or '')
969         return False
970
971     def onchange_date(self, cr, user, ids, date, context=None):
972         """
973         Returns a dict that contains new values and context
974         @param cr: A database cursor
975         @param user: ID of the user currently logged in
976         @param date: latest value from user input for field date
977         @param args: other arguments
978         @param context: context arguments, like lang, time zone
979         @return: Returns a dict which contains new values, and context
980         """
981         res = {}
982         if context is None:
983             context = {}
984         period_pool = self.pool.get('account.period')
985         ctx = dict(context, account_period_prefer_normal=True)
986         pids = period_pool.find(cr, user, date, context=ctx)
987         if pids:
988             res.update({
989                 'period_id':pids[0]
990             })
991             context.update({
992                 'period_id':pids[0]
993             })
994         return {
995             'value':res,
996             'context':context,
997         }
998
999     def _check_moves(self, cr, uid, context=None):
1000         # use the first move ever created for this journal and period
1001         if context is None:
1002             context = {}
1003         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']))
1004         res = cr.fetchone()
1005         if res:
1006             if res[1] != 'draft':
1007                 raise osv.except_osv(_('User Error!'),
1008                        _('The account move (%s) for centralisation ' \
1009                                 'has been confirmed.') % res[2])
1010         return res
1011
1012     def _remove_move_reconcile(self, cr, uid, move_ids=None, opening_reconciliation=False, context=None):
1013         # Function remove move rencocile ids related with moves
1014         obj_move_line = self.pool.get('account.move.line')
1015         obj_move_rec = self.pool.get('account.move.reconcile')
1016         unlink_ids = []
1017         if not move_ids:
1018             return True
1019         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1020         full_recs = filter(lambda x: x['reconcile_id'], recs)
1021         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1022         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1023         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1024         unlink_ids += rec_ids
1025         unlink_ids += part_rec_ids
1026         if unlink_ids:
1027             if opening_reconciliation:
1028                 obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
1029             obj_move_rec.unlink(cr, uid, unlink_ids)
1030         return True
1031
1032     def unlink(self, cr, uid, ids, context=None, check=True):
1033         if context is None:
1034             context = {}
1035         move_obj = self.pool.get('account.move')
1036         self._update_check(cr, uid, ids, context)
1037         result = False
1038         move_ids = set()
1039         for line in self.browse(cr, uid, ids, context=context):
1040             move_ids.add(line.move_id.id)
1041             context['journal_id'] = line.journal_id.id
1042             context['period_id'] = line.period_id.id
1043             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1044         move_ids = list(move_ids)
1045         if check and move_ids:
1046             move_obj.validate(cr, uid, move_ids, context=context)
1047         return result
1048
1049     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1050         if context is None:
1051             context={}
1052         move_obj = self.pool.get('account.move')
1053         account_obj = self.pool.get('account.account')
1054         journal_obj = self.pool.get('account.journal')
1055         if isinstance(ids, (int, long)):
1056             ids = [ids]
1057         if vals.get('account_tax_id', False):
1058             raise osv.except_osv(_('Unable to change tax!'), _('You cannot change the tax, you should remove and recreate lines.'))
1059         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1060             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1061         if update_check:
1062             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):
1063                 self._update_check(cr, uid, ids, context)
1064
1065         todo_date = None
1066         if vals.get('date', False):
1067             todo_date = vals['date']
1068             del vals['date']
1069
1070         for line in self.browse(cr, uid, ids, context=context):
1071             ctx = context.copy()
1072             if not ctx.get('journal_id'):
1073                 if line.move_id:
1074                    ctx['journal_id'] = line.move_id.journal_id.id
1075                 else:
1076                     ctx['journal_id'] = line.journal_id.id
1077             if not ctx.get('period_id'):
1078                 if line.move_id:
1079                     ctx['period_id'] = line.move_id.period_id.id
1080                 else:
1081                     ctx['period_id'] = line.period_id.id
1082             #Check for centralisation
1083             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1084             if journal.centralisation:
1085                 self._check_moves(cr, uid, context=ctx)
1086         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1087         if check:
1088             done = []
1089             for line in self.browse(cr, uid, ids):
1090                 if line.move_id.id not in done:
1091                     done.append(line.move_id.id)
1092                     move_obj.validate(cr, uid, [line.move_id.id], context)
1093                     if todo_date:
1094                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1095         return result
1096
1097     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1098         journal_obj = self.pool.get('account.journal')
1099         period_obj = self.pool.get('account.period')
1100         jour_period_obj = self.pool.get('account.journal.period')
1101         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1102         result = cr.fetchall()
1103         journal = journal_obj.browse(cr, uid, journal_id, context=context)
1104         period = period_obj.browse(cr, uid, period_id, context=context)
1105         for (state,) in result:
1106             if state == 'done':
1107                 raise osv.except_osv(_('Error!'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
1108         if not result:
1109             jour_period_obj.create(cr, uid, {
1110                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1111                 'journal_id': journal.id,
1112                 'period_id': period.id
1113             })
1114         return True
1115
1116     def _update_check(self, cr, uid, ids, context=None):
1117         done = {}
1118         for line in self.browse(cr, uid, ids, context=context):
1119             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1120             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1121                 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)
1122             if line.reconcile_id:
1123                 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)
1124             t = (line.journal_id.id, line.period_id.id)
1125             if t not in done:
1126                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1127                 done[t] = True
1128         return True
1129
1130     def create(self, cr, uid, vals, context=None, check=True):
1131         account_obj = self.pool.get('account.account')
1132         tax_obj = self.pool.get('account.tax')
1133         move_obj = self.pool.get('account.move')
1134         cur_obj = self.pool.get('res.currency')
1135         journal_obj = self.pool.get('account.journal')
1136         if context is None:
1137             context = {}
1138         if vals.get('move_id', False):
1139             move = self.pool.get('account.move').browse(cr, uid, vals['move_id'], context=context)
1140             if move.company_id:
1141                 vals['company_id'] = move.company_id.id
1142             if move.date and not vals.get('date'):
1143                 vals['date'] = move.date
1144         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1145             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1146         if 'journal_id' in vals and vals['journal_id']:
1147             context['journal_id'] = vals['journal_id']
1148         if 'period_id' in vals and vals['period_id']:
1149             context['period_id'] = vals['period_id']
1150         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1151             m = move_obj.browse(cr, uid, vals['move_id'])
1152             context['journal_id'] = m.journal_id.id
1153             context['period_id'] = m.period_id.id
1154         #we need to treat the case where a value is given in the context for period_id as a string
1155         if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)):
1156             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1157             if len(period_candidate_ids) != 1:
1158                 raise osv.except_osv(_('Error!'), _('No period found or more than one period found for the given date.'))
1159             context['period_id'] = period_candidate_ids[0][0]
1160         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1161             context['journal_id'] = context.get('search_default_journal_id')
1162         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1163         move_id = vals.get('move_id', False)
1164         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1165         vals['journal_id'] = vals.get('journal_id') or context.get('journal_id')
1166         vals['period_id'] = vals.get('period_id') or context.get('period_id')
1167         vals['date'] = vals.get('date') or context.get('date')
1168         if not move_id:
1169             if journal.centralisation:
1170                 #Check for centralisation
1171                 res = self._check_moves(cr, uid, context)
1172                 if res:
1173                     vals['move_id'] = res[0]
1174             if not vals.get('move_id', False):
1175                 if journal.sequence_id:
1176                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1177                     v = {
1178                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1179                         'period_id': context['period_id'],
1180                         'journal_id': context['journal_id']
1181                     }
1182                     if vals.get('ref', ''):
1183                         v.update({'ref': vals['ref']})
1184                     move_id = move_obj.create(cr, uid, v, context)
1185                     vals['move_id'] = move_id
1186                 else:
1187                     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.'))
1188         ok = not (journal.type_control_ids or journal.account_control_ids)
1189         if ('account_id' in vals):
1190             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1191             if journal.type_control_ids:
1192                 type = account.user_type
1193                 for t in journal.type_control_ids:
1194                     if type.code == t.code:
1195                         ok = True
1196                         break
1197             if journal.account_control_ids and not ok:
1198                 for a in journal.account_control_ids:
1199                     if a.id == vals['account_id']:
1200                         ok = True
1201                         break
1202             # Automatically convert in the account's secondary currency if there is one and
1203             # the provided values were not already multi-currency
1204             if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
1205                 vals['currency_id'] = account.currency_id.id
1206                 ctx = {}
1207                 if 'date' in vals:
1208                     ctx['date'] = vals['date']
1209                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1210                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1211         if not ok:
1212             raise osv.except_osv(_('Bad Account!'), _('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
1213
1214         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1215         # CREATE Taxes
1216         if vals.get('account_tax_id', False):
1217             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1218             total = vals['debit'] - vals['credit']
1219             if journal.type in ('purchase_refund', 'sale_refund'):
1220                 base_code = 'ref_base_code_id'
1221                 tax_code = 'ref_tax_code_id'
1222                 account_id = 'account_paid_id'
1223                 base_sign = 'ref_base_sign'
1224                 tax_sign = 'ref_tax_sign'
1225             else:
1226                 base_code = 'base_code_id'
1227                 tax_code = 'tax_code_id'
1228                 account_id = 'account_collected_id'
1229                 base_sign = 'base_sign'
1230                 tax_sign = 'tax_sign'
1231             tmp_cnt = 0
1232             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1233                 #create the base movement
1234                 if tmp_cnt == 0:
1235                     if tax[base_code]:
1236                         tmp_cnt += 1
1237                         self.write(cr, uid,[result], {
1238                             'tax_code_id': tax[base_code],
1239                             'tax_amount': tax[base_sign] * abs(total)
1240                         })
1241                 else:
1242                     data = {
1243                         'move_id': vals['move_id'],
1244                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1245                         'date': vals['date'],
1246                         'partner_id': vals.get('partner_id',False),
1247                         'ref': vals.get('ref',False),
1248                         'account_tax_id': False,
1249                         'tax_code_id': tax[base_code],
1250                         'tax_amount': tax[base_sign] * abs(total),
1251                         'account_id': vals['account_id'],
1252                         'credit': 0.0,
1253                         'debit': 0.0,
1254                     }
1255                     if data['tax_code_id']:
1256                         self.create(cr, uid, data, context)
1257                 #create the Tax movement
1258                 data = {
1259                     'move_id': vals['move_id'],
1260                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1261                     'date': vals['date'],
1262                     'partner_id': vals.get('partner_id',False),
1263                     'ref': vals.get('ref',False),
1264                     'account_tax_id': False,
1265                     'tax_code_id': tax[tax_code],
1266                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1267                     'account_id': tax[account_id] or vals['account_id'],
1268                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1269                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1270                 }
1271                 if data['tax_code_id']:
1272                     self.create(cr, uid, data, context)
1273             del vals['account_tax_id']
1274
1275         if check and not context.get('novalidate') and ((not context.get('no_store_function')) or journal.entry_posted):
1276             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1277             if journal.entry_posted and tmp:
1278                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1279         return result
1280
1281     def list_periods(self, cr, uid, context=None):
1282         ids = self.pool.get('account.period').search(cr,uid,[])
1283         return self.pool.get('account.period').name_get(cr, uid, ids, context=context)
1284
1285     def list_journals(self, cr, uid, context=None):
1286         ng = dict(self.pool.get('account.journal').name_search(cr,uid,'',[]))
1287         ids = ng.keys()
1288         result = []
1289         for journal in self.pool.get('account.journal').browse(cr, uid, ids, context=context):
1290             result.append((journal.id,ng[journal.id],journal.type,
1291                 bool(journal.currency),bool(journal.analytic_journal_id)))
1292         return result
1293
1294 account_move_line()
1295
1296 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: