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