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