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