[IMP]Replace class flow
[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', 'Unit of Measure'),
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')], 'Status', 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         if context is None:
702             context = {}
703         if context and context.get('next_partner_only', False):
704             if not context.get('partner_id', False):
705                 partner = self.get_next_partner_only(cr, uid, offset, context)
706             else:
707                 partner = context.get('partner_id', False)
708             if not partner:
709                 return []
710             args.append(('partner_id', '=', partner[0]))
711         return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
712
713     def get_next_partner_only(self, cr, uid, offset=0, context=None):
714         cr.execute(
715              """
716              SELECT p.id
717              FROM res_partner p
718              RIGHT JOIN (
719                 SELECT l.partner_id AS partner_id, SUM(l.debit) AS debit, SUM(l.credit) AS credit
720                 FROM account_move_line l
721                 LEFT JOIN account_account a ON (a.id = l.account_id)
722                     LEFT JOIN res_partner p ON (l.partner_id = p.id)
723                     WHERE a.reconcile IS TRUE
724                     AND l.reconcile_id IS NULL
725                     AND (p.last_reconciliation_date IS NULL OR l.date > p.last_reconciliation_date)
726                     AND l.state <> 'draft'
727                     GROUP BY l.partner_id
728                 ) AS s ON (p.id = s.partner_id)
729                 WHERE debit > 0 AND credit > 0
730                 ORDER BY p.last_reconciliation_date LIMIT 1 OFFSET %s""", (offset, )
731             )
732         return cr.fetchone()
733
734     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
735         move_rec_obj = self.pool.get('account.move.reconcile')
736         merges = []
737         unmerge = []
738         total = 0.0
739         merges_rec = []
740         company_list = []
741         if context is None:
742             context = {}
743         for line in self.browse(cr, uid, ids, context=context):
744             if company_list and not line.company_id.id in company_list:
745                 raise osv.except_osv(_('Warning !'), _('To reconcile the entries company should be the same for all entries'))
746             company_list.append(line.company_id.id)
747
748         for line in self.browse(cr, uid, ids, context=context):
749             if line.account_id.currency_id:
750                 currency_id = line.account_id.currency_id
751             else:
752                 currency_id = line.company_id.currency_id
753             if line.reconcile_id:
754                 raise osv.except_osv(_('Warning'), _('Already Reconciled!'))
755             if line.reconcile_partial_id:
756                 for line2 in line.reconcile_partial_id.line_partial_ids:
757                     if not line2.reconcile_id:
758                         if line2.id not in merges:
759                             merges.append(line2.id)
760                         if line2.account_id.currency_id:
761                             total += line2.amount_currency
762                         else:
763                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
764                 merges_rec.append(line.reconcile_partial_id.id)
765             else:
766                 unmerge.append(line.id)
767                 if line.account_id.currency_id:
768                     total += line.amount_currency
769                 else:
770                     total += (line.debit or 0.0) - (line.credit or 0.0)
771         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
772             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)
773             return res
774         r_id = move_rec_obj.create(cr, uid, {
775             'type': type,
776             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
777         })
778         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
779         return True
780
781     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
782         account_obj = self.pool.get('account.account')
783         move_obj = self.pool.get('account.move')
784         move_rec_obj = self.pool.get('account.move.reconcile')
785         partner_obj = self.pool.get('res.partner')
786         currency_obj = self.pool.get('res.currency')
787         lines = self.browse(cr, uid, ids, context=context)
788         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
789         credit = debit = 0.0
790         currency = 0.0
791         account_id = False
792         partner_id = False
793         if context is None:
794             context = {}
795         company_list = []
796         for line in self.browse(cr, uid, ids, context=context):
797             if company_list and not line.company_id.id in company_list:
798                 raise osv.except_osv(_('Warning !'), _('To reconcile the entries company should be the same for all entries'))
799             company_list.append(line.company_id.id)
800         for line in unrec_lines:
801             if line.state <> 'valid':
802                 raise osv.except_osv(_('Error'),
803                         _('Entry "%s" is not valid !') % line.name)
804             credit += line['credit']
805             debit += line['debit']
806             currency += line['amount_currency'] or 0.0
807             account_id = line['account_id']['id']
808             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
809         writeoff = debit - credit
810
811         # Ifdate_p in context => take this date
812         if context.has_key('date_p') and context['date_p']:
813             date=context['date_p']
814         else:
815             date = time.strftime('%Y-%m-%d')
816
817         cr.execute('SELECT account_id, reconcile_id '\
818                    'FROM account_move_line '\
819                    'WHERE id IN %s '\
820                    'GROUP BY account_id,reconcile_id',
821                    (tuple(ids), ))
822         r = cr.fetchall()
823         #TODO: move this check to a constraint in the account_move_reconcile object
824         if not unrec_lines:
825             raise osv.except_osv(_('Error'), _('Entry is already reconciled'))
826         account = account_obj.browse(cr, uid, account_id, context=context)
827         if r[0][1] != None:
828             raise osv.except_osv(_('Error'), _('Some entries are already reconciled !'))
829
830         if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
831            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
832             if not writeoff_acc_id:
833                 raise osv.except_osv(_('Warning'), _('You have to provide an account for the write off/exchange difference entry !'))
834             if writeoff > 0:
835                 debit = writeoff
836                 credit = 0.0
837                 self_credit = writeoff
838                 self_debit = 0.0
839             else:
840                 debit = 0.0
841                 credit = -writeoff
842                 self_credit = 0.0
843                 self_debit = -writeoff
844             # If comment exist in context, take it
845             if 'comment' in context and context['comment']:
846                 libelle = context['comment']
847             else:
848                 libelle = _('Write-Off')
849
850             cur_obj = self.pool.get('res.currency')
851             cur_id = False
852             amount_currency_writeoff = 0.0
853             if context.get('company_currency_id',False) != context.get('currency_id',False):
854                 cur_id = context.get('currency_id',False)
855                 for line in unrec_lines:
856                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
857                         amount_currency_writeoff += line.amount_currency
858                     else:
859                         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})
860                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
861
862             writeoff_lines = [
863                 (0, 0, {
864                     'name': libelle,
865                     'debit': self_debit,
866                     'credit': self_credit,
867                     'account_id': account_id,
868                     'date': date,
869                     'partner_id': partner_id,
870                     'currency_id': cur_id or (account.currency_id.id or False),
871                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
872                 }),
873                 (0, 0, {
874                     'name': libelle,
875                     'debit': debit,
876                     'credit': credit,
877                     'account_id': writeoff_acc_id,
878                     'analytic_account_id': context.get('analytic_id', False),
879                     'date': date,
880                     'partner_id': partner_id,
881                     'currency_id': cur_id or (account.currency_id.id or False),
882                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
883                 })
884             ]
885
886             writeoff_move_id = move_obj.create(cr, uid, {
887                 'period_id': writeoff_period_id,
888                 'journal_id': writeoff_journal_id,
889                 'date':date,
890                 'state': 'draft',
891                 'line_id': writeoff_lines
892             })
893
894             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
895             if account_id == writeoff_acc_id:
896                 writeoff_line_ids = [writeoff_line_ids[1]]
897             ids += writeoff_line_ids
898
899         r_id = move_rec_obj.create(cr, uid, {
900             'type': type,
901             'line_id': map(lambda x: (4, x, False), ids),
902             'line_partial_ids': map(lambda x: (3, x, False), ids)
903         })
904         wf_service = netsvc.LocalService("workflow")
905         # the id of the move.reconcile is written in the move.line (self) by the create method above
906         # because of the way the line_id are defined: (4, x, False)
907         for id in ids:
908             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
909
910         if lines and lines[0]:
911             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
912             if partner_id and context and context.get('stop_reconcile', False):
913                 partner_obj.write(cr, uid, [partner_id], {'last_reconciliation_date': time.strftime('%Y-%m-%d %H:%M:%S')})
914         return r_id
915
916     def view_header_get(self, cr, user, view_id, view_type, context=None):
917         if context is None:
918             context = {}
919         context = self.convert_to_period(cr, user, context=context)
920         if context.get('account_id', False):
921             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
922             res = cr.fetchone()
923             if res:
924                 res = _('Entries: ')+ (res[0] or '')
925             return res
926         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
927             return False
928         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
929         j = cr.fetchone()[0] or ''
930         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
931         p = cr.fetchone()[0] or ''
932         if j or p:
933             return j + (p and (':' + p) or '')
934         return False
935
936     def onchange_date(self, cr, user, ids, date, context=None):
937         """
938         Returns a dict that contains new values and context
939         @param cr: A database cursor
940         @param user: ID of the user currently logged in
941         @param date: latest value from user input for field date
942         @param args: other arguments
943         @param context: context arguments, like lang, time zone
944         @return: Returns a dict which contains new values, and context
945         """
946         res = {}
947         if context is None:
948             context = {}
949         period_pool = self.pool.get('account.period')
950         pids = period_pool.search(cr, user, [('date_start','<=',date), ('date_stop','>=',date)])
951         if pids:
952             res.update({
953                 'period_id':pids[0]
954             })
955             context.update({
956                 'period_id':pids[0]
957             })
958         return {
959             'value':res,
960             'context':context,
961         }
962
963     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
964         journal_pool = self.pool.get('account.journal')
965         if context is None:
966             context = {}
967         result = super(account_move_line, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
968         if view_type != 'tree':
969             #Remove the toolbar from the form view
970             if view_type == 'form':
971                 if result.get('toolbar', False):
972                     result['toolbar']['action'] = []
973             #Restrict the list of journal view in search view
974             if view_type == 'search' and result['fields'].get('journal_id', False):
975                 result['fields']['journal_id']['selection'] = journal_pool.name_search(cr, uid, '', [], context=context)
976                 ctx = context.copy()
977                 #we add the refunds journal in the selection field of journal
978                 if context.get('journal_type', False) == 'sale':
979                     ctx.update({'journal_type': 'sale_refund'})
980                     result['fields']['journal_id']['selection'] += journal_pool.name_search(cr, uid, '', [], context=ctx)
981                 elif context.get('journal_type', False) == 'purchase':
982                     ctx.update({'journal_type': 'purchase_refund'})
983                     result['fields']['journal_id']['selection'] += journal_pool.name_search(cr, uid, '', [], context=ctx)
984             return result
985         if context.get('view_mode', False):
986             return result
987         fld = []
988         fields = {}
989         flds = []
990         title = _("Accounting Entries") #self.view_header_get(cr, uid, view_id, view_type, context)
991
992         ids = journal_pool.search(cr, uid, [])
993         journals = journal_pool.browse(cr, uid, ids, context=context)
994         all_journal = [None]
995         common_fields = {}
996         total = len(journals)
997         for journal in journals:
998             all_journal.append(journal.id)
999             for field in journal.view_id.columns_id:
1000                 # sometimes, it's possible that a defined column is not loaded (the module containing 
1001                 # this field is not loaded) when we make an update.
1002                 if field.name not in self._columns:
1003                     continue
1004
1005                 if not field.field in fields:
1006                     fields[field.field] = [journal.id]
1007                     fld.append((field.field, field.sequence))
1008                     flds.append(field.field)
1009                     common_fields[field.field] = 1
1010                 else:
1011                     fields.get(field.field).append(journal.id)
1012                     common_fields[field.field] = common_fields[field.field] + 1
1013         fld.append(('period_id', 3))
1014         fld.append(('journal_id', 10))
1015         flds.append('period_id')
1016         flds.append('journal_id')
1017         fields['period_id'] = all_journal
1018         fields['journal_id'] = all_journal
1019         fld = sorted(fld, key=itemgetter(1))
1020         widths = {
1021             'statement_id': 50,
1022             'state': 60,
1023             'tax_code_id': 50,
1024             'move_id': 40,
1025         }
1026
1027         document = etree.Element('tree', string=title, editable="top",
1028                                  on_write="on_create_write",
1029                                  colors="red:state=='draft';black:state=='valid'")
1030         fields_get = self.fields_get(cr, uid, flds, context)
1031         for field, _seq in fld:
1032             if common_fields.get(field) == total:
1033                 fields.get(field).append(None)
1034             # if field=='state':
1035             #     state = 'colors="red:state==\'draft\'"'
1036             f = etree.SubElement(document, 'field', name=field)
1037
1038             if field == 'debit':
1039                 f.set('sum', _("Total debit"))
1040
1041             elif field == 'credit':
1042                 f.set('sum', _("Total credit"))
1043
1044             elif field == 'move_id':
1045                 f.set('required', 'False')
1046
1047             elif field == 'account_tax_id':
1048                 f.set('domain', "[('parent_id', '=' ,False)]")
1049                 f.set('context', "{'journal_id': journal_id}")
1050
1051             elif field == 'account_id' and journal.id:
1052                 f.set('domain', "[('journal_id', '=', journal_id),('type','!=','view'), ('type','!=','closed')]")
1053                 f.set('on_change', 'onchange_account_id(account_id, partner_id)')
1054
1055             elif field == 'partner_id':
1056                 f.set('on_change', 'onchange_partner_id(move_id, partner_id, account_id, debit, credit, date, journal_id)')
1057
1058             elif field == 'journal_id':
1059                 f.set('context', "{'journal_id': journal_id}")
1060
1061             elif field == 'statement_id':
1062                 f.set('domain', "[('state', '!=', 'confirm'),('journal_id.type', '=', 'bank')]")
1063                 f.set('invisible', 'True')
1064
1065             elif field == 'date':
1066                 f.set('on_change', 'onchange_date(date)')
1067
1068             elif field == 'analytic_account_id':
1069                 # Currently it is not working due to being executed by superclass's fields_view_get
1070                 # f.set('groups', 'analytic.group_analytic_accounting')
1071                 pass
1072
1073             if field in ('amount_currency', 'currency_id'):
1074                 f.set('on_change', 'onchange_currency(account_id, amount_currency, currency_id, date, journal_id)')
1075                 f.set('attrs', "{'readonly': [('state', '=', 'valid')]}")
1076
1077             if field in widths:
1078                 f.set('width', str(widths[field]))
1079
1080             if field in ('journal_id',):
1081                 f.set("invisible", "context.get('journal_id', False)")
1082             elif field in ('period_id',):
1083                 f.set("invisible", "context.get('period_id', False)")
1084
1085             orm.setup_modifiers(f, fields_get[field], context=context,
1086                                 in_tree_view=True)
1087
1088         result['arch'] = etree.tostring(document, pretty_print=True)
1089         result['fields'] = fields_get
1090         return result
1091
1092     def _check_moves(self, cr, uid, context=None):
1093         # use the first move ever created for this journal and period
1094         if context is None:
1095             context = {}
1096         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']))
1097         res = cr.fetchone()
1098         if res:
1099             if res[1] != 'draft':
1100                 raise osv.except_osv(_('UserError'),
1101                        _('The account move (%s) for centralisation ' \
1102                                 'has been confirmed!') % res[2])
1103         return res
1104
1105     def _remove_move_reconcile(self, cr, uid, move_ids=[], context=None):
1106         # Function remove move rencocile ids related with moves
1107         obj_move_line = self.pool.get('account.move.line')
1108         obj_move_rec = self.pool.get('account.move.reconcile')
1109         unlink_ids = []
1110         if not move_ids:
1111             return True
1112         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1113         full_recs = filter(lambda x: x['reconcile_id'], recs)
1114         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1115         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1116         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1117         unlink_ids += rec_ids
1118         unlink_ids += part_rec_ids
1119         if unlink_ids:
1120             obj_move_rec.unlink(cr, uid, unlink_ids)
1121         return True
1122
1123     def unlink(self, cr, uid, ids, context=None, check=True):
1124         if context is None:
1125             context = {}
1126         move_obj = self.pool.get('account.move')
1127         self._update_check(cr, uid, ids, context)
1128         result = False
1129         move_ids = set()
1130         for line in self.browse(cr, uid, ids, context=context):
1131             move_ids.add(line.move_id.id)
1132             context['journal_id'] = line.journal_id.id
1133             context['period_id'] = line.period_id.id
1134             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1135         move_ids = list(move_ids)
1136         if check and move_ids:
1137             move_obj.validate(cr, uid, move_ids, context=context)
1138         return result
1139
1140     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1141         if context is None:
1142             context={}
1143         move_obj = self.pool.get('account.move')
1144         account_obj = self.pool.get('account.account')
1145         journal_obj = self.pool.get('account.journal')
1146         if isinstance(ids, (int, long)):
1147             ids = [ids]
1148         if vals.get('account_tax_id', False):
1149             raise osv.except_osv(_('Unable to change tax !'), _('You can not change the tax, you should remove and recreate lines !'))
1150         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1151             raise osv.except_osv(_('Bad account!'), _('You can not use an inactive account!'))
1152         if update_check:
1153             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):
1154                 self._update_check(cr, uid, ids, context)
1155
1156         todo_date = None
1157         if vals.get('date', False):
1158             todo_date = vals['date']
1159             del vals['date']
1160
1161         for line in self.browse(cr, uid, ids, context=context):
1162             ctx = context.copy()
1163             if ('journal_id' not in ctx):
1164                 if line.move_id:
1165                    ctx['journal_id'] = line.move_id.journal_id.id
1166                 else:
1167                     ctx['journal_id'] = line.journal_id.id
1168             if ('period_id' not in ctx):
1169                 if line.move_id:
1170                     ctx['period_id'] = line.move_id.period_id.id
1171                 else:
1172                     ctx['period_id'] = line.period_id.id
1173             #Check for centralisation
1174             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1175             if journal.centralisation:
1176                 self._check_moves(cr, uid, context=ctx)
1177         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1178         if check:
1179             done = []
1180             for line in self.browse(cr, uid, ids):
1181                 if line.move_id.id not in done:
1182                     done.append(line.move_id.id)
1183                     move_obj.validate(cr, uid, [line.move_id.id], context)
1184                     if todo_date:
1185                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1186         return result
1187
1188     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1189         journal_obj = self.pool.get('account.journal')
1190         period_obj = self.pool.get('account.period')
1191         jour_period_obj = self.pool.get('account.journal.period')
1192         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1193         result = cr.fetchall()
1194         for (state,) in result:
1195             if state == 'done':
1196                 raise osv.except_osv(_('Error !'), _('You can not add/modify entries in a closed journal.'))
1197         if not result:
1198             journal = journal_obj.browse(cr, uid, journal_id, context=context)
1199             period = period_obj.browse(cr, uid, period_id, context=context)
1200             jour_period_obj.create(cr, uid, {
1201                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1202                 'journal_id': journal.id,
1203                 'period_id': period.id
1204             })
1205         return True
1206
1207     def _update_check(self, cr, uid, ids, context=None):
1208         done = {}
1209         for line in self.browse(cr, uid, ids, context=context):
1210             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1211             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1212                 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)
1213             if line.reconcile_id:
1214                 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)
1215             t = (line.journal_id.id, line.period_id.id)
1216             if t not in done:
1217                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1218                 done[t] = True
1219         return True
1220
1221     def create(self, cr, uid, vals, context=None, check=True):
1222         account_obj = self.pool.get('account.account')
1223         tax_obj = self.pool.get('account.tax')
1224         move_obj = self.pool.get('account.move')
1225         cur_obj = self.pool.get('res.currency')
1226         journal_obj = self.pool.get('account.journal')
1227         if context is None:
1228             context = {}
1229         if vals.get('move_id', False):
1230             company_id = self.pool.get('account.move').read(cr, uid, vals['move_id'], ['company_id']).get('company_id', False)
1231             if company_id:
1232                 vals['company_id'] = company_id[0]
1233         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1234             raise osv.except_osv(_('Bad account!'), _('You can not use an inactive account!'))
1235         if 'journal_id' in vals:
1236             context['journal_id'] = vals['journal_id']
1237         if 'period_id' in vals:
1238             context['period_id'] = vals['period_id']
1239         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1240             m = move_obj.browse(cr, uid, vals['move_id'])
1241             context['journal_id'] = m.journal_id.id
1242             context['period_id'] = m.period_id.id
1243         #we need to treat the case where a value is given in the context for period_id as a string
1244         if 'period_id' not in context or not isinstance(context.get('period_id', ''), (int, long)):
1245             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1246             if len(period_candidate_ids) != 1:
1247                 raise osv.except_osv(_('Encoding error'), _('No period found or more than one period found for the given date.'))
1248             context['period_id'] = period_candidate_ids[0][0]
1249         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1250             context['journal_id'] = context.get('search_default_journal_id')            
1251         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1252         move_id = vals.get('move_id', False)
1253         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1254         if not move_id:
1255             if journal.centralisation:
1256                 #Check for centralisation
1257                 res = self._check_moves(cr, uid, context)
1258                 if res:
1259                     vals['move_id'] = res[0]
1260             if not vals.get('move_id', False):
1261                 if journal.sequence_id:
1262                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1263                     v = {
1264                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1265                         'period_id': context['period_id'],
1266                         'journal_id': context['journal_id']
1267                     }
1268                     if vals.get('ref', ''):
1269                         v.update({'ref': vals['ref']})
1270                     move_id = move_obj.create(cr, uid, v, context)
1271                     vals['move_id'] = move_id
1272                 else:
1273                     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.'))
1274         ok = not (journal.type_control_ids or journal.account_control_ids)
1275         if ('account_id' in vals):
1276             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1277             if journal.type_control_ids:
1278                 type = account.user_type
1279                 for t in journal.type_control_ids:
1280                     if type.code == t.code:
1281                         ok = True
1282                         break
1283             if journal.account_control_ids and not ok:
1284                 for a in journal.account_control_ids:
1285                     if a.id == vals['account_id']:
1286                         ok = True
1287                         break
1288             # Automatically convert in the account's secondary currency if there is one and
1289             # the provided values were not already multi-currency
1290             if account.currency_id and (vals.get('amount_currency', False) is False) and account.currency_id.id != account.company_id.currency_id.id:
1291                 vals['currency_id'] = account.currency_id.id
1292                 ctx = {}
1293                 if 'date' in vals:
1294                     ctx['date'] = vals['date']
1295                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1296                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1297         if not ok:
1298             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 !'))
1299
1300         if vals.get('analytic_account_id',False):
1301             if journal.analytic_journal_id:
1302                 vals['analytic_lines'] = [(0,0, {
1303                         'name': vals['name'],
1304                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1305                         'account_id': vals.get('analytic_account_id', False),
1306                         'unit_amount': vals.get('quantity', 1.0),
1307                         'amount': vals.get('debit', 0.0) or vals.get('credit', 0.0),
1308                         'general_account_id': vals.get('account_id', False),
1309                         'journal_id': journal.analytic_journal_id.id,
1310                         'ref': vals.get('ref', False),
1311                         'user_id': uid
1312             })]
1313
1314         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1315         # CREATE Taxes
1316         if vals.get('account_tax_id', False):
1317             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1318             total = vals['debit'] - vals['credit']
1319             if journal.type in ('purchase_refund', 'sale_refund'):
1320                 base_code = 'ref_base_code_id'
1321                 tax_code = 'ref_tax_code_id'
1322                 account_id = 'account_paid_id'
1323                 base_sign = 'ref_base_sign'
1324                 tax_sign = 'ref_tax_sign'
1325             else:
1326                 base_code = 'base_code_id'
1327                 tax_code = 'tax_code_id'
1328                 account_id = 'account_collected_id'
1329                 base_sign = 'base_sign'
1330                 tax_sign = 'tax_sign'
1331             tmp_cnt = 0
1332             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1333                 #create the base movement
1334                 if tmp_cnt == 0:
1335                     if tax[base_code]:
1336                         tmp_cnt += 1
1337                         self.write(cr, uid,[result], {
1338                             'tax_code_id': tax[base_code],
1339                             'tax_amount': tax[base_sign] * abs(total)
1340                         })
1341                 else:
1342                     data = {
1343                         'move_id': vals['move_id'],
1344                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1345                         'date': vals['date'],
1346                         'partner_id': vals.get('partner_id',False),
1347                         'ref': vals.get('ref',False),
1348                         'account_tax_id': False,
1349                         'tax_code_id': tax[base_code],
1350                         'tax_amount': tax[base_sign] * abs(total),
1351                         'account_id': vals['account_id'],
1352                         'credit': 0.0,
1353                         'debit': 0.0,
1354                     }
1355                     if data['tax_code_id']:
1356                         self.create(cr, uid, data, context)
1357                 #create the VAT movement
1358                 data = {
1359                     'move_id': vals['move_id'],
1360                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1361                     'date': vals['date'],
1362                     'partner_id': vals.get('partner_id',False),
1363                     'ref': vals.get('ref',False),
1364                     'account_tax_id': False,
1365                     'tax_code_id': tax[tax_code],
1366                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1367                     'account_id': tax[account_id] or vals['account_id'],
1368                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1369                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1370                 }
1371                 if data['tax_code_id']:
1372                     self.create(cr, uid, data, context)
1373             del vals['account_tax_id']
1374
1375         if check and ((not context.get('no_store_function')) or journal.entry_posted):
1376             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1377             if journal.entry_posted and tmp:
1378                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1379         return result
1380
1381 account_move_line()
1382
1383 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: