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