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