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