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