Merge with addons/trunk revno 8168.
[odoo/odoo.git] / addons / account / account_move_line.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import sys
23 import time
24 from datetime import datetime
25 from operator import itemgetter
26
27 from lxml import etree
28
29 import netsvc
30 from osv import fields, osv, orm
31 from tools.translate import _
32 import decimal_precision as dp
33 import tools
34
35 class account_move_line(osv.osv):
36     _name = "account.move.line"
37     _description = "Journal Items"
38
39     def _query_get(self, cr, uid, obj='l', context=None):
40         fiscalyear_obj = self.pool.get('account.fiscalyear')
41         fiscalperiod_obj = self.pool.get('account.period')
42         account_obj = self.pool.get('account.account')
43         fiscalyear_ids = []
44         if context is None:
45             context = {}
46         initial_bal = context.get('initial_bal', False)
47         company_clause = " "
48         if context.get('company_id', False):
49             company_clause = " AND " +obj+".company_id = %s" % context.get('company_id', False)
50         if not context.get('fiscalyear', False):
51             if context.get('all_fiscalyear', False):
52                 #this option is needed by the aged balance report because otherwise, if we search only the draft ones, an open invoice of a closed fiscalyear won't be displayed
53                 fiscalyear_ids = fiscalyear_obj.search(cr, uid, [])
54             else:
55                 fiscalyear_ids = fiscalyear_obj.search(cr, uid, [('state', '=', 'draft')])
56         else:
57             #for initial balance as well as for normal query, we check only the selected FY because the best practice is to generate the FY opening entries
58             fiscalyear_ids = [context['fiscalyear']]
59
60         fiscalyear_clause = (','.join([str(x) for x in fiscalyear_ids])) or '0'
61         state = context.get('state', False)
62         where_move_state = ''
63         where_move_lines_by_date = ''
64
65         if context.get('date_from', False) and context.get('date_to', False):
66             if initial_bal:
67                 where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date < '" +context['date_from']+"')"
68             else:
69                 where_move_lines_by_date = " AND " +obj+".move_id IN (SELECT id FROM account_move WHERE date >= '" +context['date_from']+"' AND date <= '"+context['date_to']+"')"
70
71         if state:
72             if state.lower() not in ['all']:
73                 where_move_state= " AND "+obj+".move_id IN (SELECT id FROM account_move WHERE account_move.state = '"+state+"')"
74         if context.get('period_from', False) and context.get('period_to', False) and not context.get('periods', False):
75             if initial_bal:
76                 period_company_id = fiscalperiod_obj.browse(cr, uid, context['period_from'], context=context).company_id.id
77                 first_period = fiscalperiod_obj.search(cr, uid, [('company_id', '=', period_company_id)], order='date_start', limit=1)[0]
78                 context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, first_period, context['period_from'])
79             else:
80                 context['periods'] = fiscalperiod_obj.build_ctx_periods(cr, uid, context['period_from'], context['period_to'])
81         if context.get('periods', False):
82             if initial_bal:
83                 query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date)
84                 period_ids = fiscalperiod_obj.search(cr, uid, [('id', 'in', context['periods'])], order='date_start', limit=1)
85                 if period_ids and period_ids[0]:
86                     first_period = fiscalperiod_obj.browse(cr, uid, period_ids[0], context=context)
87                     ids = ','.join([str(x) for x in context['periods']])
88                     query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND date_start <= '%s' AND id NOT IN (%s)) %s %s" % (fiscalyear_clause, first_period.date_start, ids, where_move_state, where_move_lines_by_date)
89             else:
90                 ids = ','.join([str(x) for x in context['periods']])
91                 query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s) AND id IN (%s)) %s %s" % (fiscalyear_clause, ids, where_move_state, where_move_lines_by_date)
92         else:
93             query = obj+".state <> 'draft' AND "+obj+".period_id IN (SELECT id FROM account_period WHERE fiscalyear_id IN (%s)) %s %s" % (fiscalyear_clause, where_move_state, where_move_lines_by_date)
94
95         if initial_bal and not context.get('periods', False) and not where_move_lines_by_date:
96             #we didn't pass any filter in the context, and the initial balance can't be computed using only the fiscalyear otherwise entries will be summed twice
97             #so we have to invalidate this query
98             raise osv.except_osv(_('Warning!'),_("You have not supplied enough arguments to compute the initial balance, please select a period and a journal in the context."))
99
100
101         if context.get('journal_ids', False):
102             query += ' AND '+obj+'.journal_id IN (%s)' % ','.join(map(str, context['journal_ids']))
103
104         if context.get('chart_account_id', False):
105             child_ids = account_obj._get_children_and_consol(cr, uid, [context['chart_account_id']], context=context)
106             query += ' AND '+obj+'.account_id IN (%s)' % ','.join(map(str, child_ids))
107
108         query += company_clause
109         return query
110
111     def _amount_residual(self, cr, uid, ids, field_names, args, context=None):
112         """
113            This function returns the residual amount on a receivable or payable account.move.line.
114            By default, it returns an amount in the currency of this journal entry (maybe different
115            of the company currency), but if you pass 'residual_in_company_currency' = True in the
116            context then the returned amount will be in company currency.
117         """
118         res = {}
119         if context is None:
120             context = {}
121         cur_obj = self.pool.get('res.currency')
122         for move_line in self.browse(cr, uid, ids, context=context):
123             res[move_line.id] = {
124                 'amount_residual': 0.0,
125                 'amount_residual_currency': 0.0,
126             }
127
128             if move_line.reconcile_id:
129                 continue
130             if not move_line.account_id.type in ('payable', 'receivable'):
131                 #this function does not suport to be used on move lines not related to payable or receivable accounts
132                 continue
133
134             if move_line.currency_id:
135                 move_line_total = move_line.amount_currency
136                 sign = move_line.amount_currency < 0 and -1 or 1
137             else:
138                 move_line_total = move_line.debit - move_line.credit
139                 sign = (move_line.debit - move_line.credit) < 0 and -1 or 1
140             line_total_in_company_currency =  move_line.debit - move_line.credit
141             context_unreconciled = context.copy()
142             if move_line.reconcile_partial_id:
143                 for payment_line in move_line.reconcile_partial_id.line_partial_ids:
144                     if payment_line.id == move_line.id:
145                         continue
146                     if payment_line.currency_id and move_line.currency_id and payment_line.currency_id.id == move_line.currency_id.id:
147                             move_line_total += payment_line.amount_currency
148                     else:
149                         if move_line.currency_id:
150                             context_unreconciled.update({'date': payment_line.date})
151                             amount_in_foreign_currency = cur_obj.compute(cr, uid, move_line.company_id.currency_id.id, move_line.currency_id.id, (payment_line.debit - payment_line.credit), round=False, context=context_unreconciled)
152                             move_line_total += amount_in_foreign_currency
153                         else:
154                             move_line_total += (payment_line.debit - payment_line.credit)
155                     line_total_in_company_currency += (payment_line.debit - payment_line.credit)
156
157             result = move_line_total
158             res[move_line.id]['amount_residual_currency'] =  sign * (move_line.currency_id and self.pool.get('res.currency').round(cr, uid, move_line.currency_id, result) or result)
159             res[move_line.id]['amount_residual'] = sign * line_total_in_company_currency
160         return res
161
162     def default_get(self, cr, uid, fields, context=None):
163         data = self._default_get(cr, uid, fields, context=context)
164         for f in data.keys():
165             if f not in fields:
166                 del data[f]
167         return data
168
169     def _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'),
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', 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('Litigation', 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', string='Company', store=True, readonly=True)
479     }
480
481     def _get_date(self, cr, uid, context=None):
482         if context is None:
483             context or {}
484         period_obj = self.pool.get('account.period')
485         dt = time.strftime('%Y-%m-%d')
486         if context.get('journal_id') and context.get('period_id'):
487             cr.execute('SELECT date FROM account_move_line ' \
488                     'WHERE journal_id = %s AND period_id = %s ' \
489                     'ORDER BY id DESC limit 1',
490                     (context['journal_id'], context['period_id']))
491             res = cr.fetchone()
492             if res:
493                 dt = res[0]
494             else:
495                 period = period_obj.browse(cr, uid, context['period_id'], context=context)
496                 dt = period.date_start
497         return dt
498
499     def _get_currency(self, cr, uid, context=None):
500         if context is None:
501             context = {}
502         if not context.get('journal_id', False):
503             return False
504         cur = self.pool.get('account.journal').browse(cr, uid, context['journal_id']).currency
505         return cur and cur.id or False
506
507     def _get_period(self, cr, uid, context=None):
508         """
509         Return  default account period value
510         """
511         context = context or {}
512         if context.get('period_id', False):
513             return context['period_id']
514         account_period_obj = self.pool.get('account.period')
515         ids = account_period_obj.find(cr, uid, context=context)
516         period_id = False
517         if ids:
518             period_id = ids[0]
519         return period_id
520
521     def _get_journal(self, cr, uid, context=None):
522         """
523         Return journal based on the journal type
524         """
525         context = context or {}
526         if context.get('journal_id', False):
527             return context['journal_id']
528         journal_id = False
529
530         journal_pool = self.pool.get('account.journal')
531         if context.get('journal_type', False):
532             jids = journal_pool.search(cr, uid, [('type','=', context.get('journal_type'))])
533             if not jids:
534                 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'))
535             journal_id = jids[0]
536         return journal_id
537
538
539     _defaults = {
540         'blocked': False,
541         'centralisation': 'normal',
542         'date': _get_date,
543         'date_created': fields.date.context_today,
544         'state': 'draft',
545         'currency_id': _get_currency,
546         'journal_id': _get_journal,
547         'credit': 0.0,
548         'debit': 0.0,
549         'amount_currency': 0.0,
550         'account_id': lambda self, cr, uid, c: c.get('account_id', False),
551         'period_id': _get_period,
552         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.move.line', context=c)
553     }
554     _order = "date desc, id desc"
555     _sql_constraints = [
556         ('credit_debit1', 'CHECK (credit*debit=0)',  'Wrong credit or debit value in accounting entry !'),
557         ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
558     ]
559
560     def _auto_init(self, cr, context=None):
561         super(account_move_line, self)._auto_init(cr, context=context)
562         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'')
563         if not cr.fetchone():
564             cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)')
565
566     def _check_no_view(self, cr, uid, ids, context=None):
567         lines = self.browse(cr, uid, ids, context=context)
568         for l in lines:
569             if l.account_id.type == 'view':
570                 return False
571         return True
572
573     def _check_no_closed(self, cr, uid, ids, context=None):
574         lines = self.browse(cr, uid, ids, context=context)
575         for l in lines:
576             if l.account_id.type == 'closed':
577                 raise osv.except_osv(_('Error!'), _('You cannot create journal items on a closed account %s %s.') % (l.account_id.code, l.account_id.name))
578         return True
579
580     def _check_company_id(self, cr, uid, ids, context=None):
581         lines = self.browse(cr, uid, ids, context=context)
582         for l in lines:
583             if l.company_id != l.account_id.company_id or l.company_id != l.period_id.company_id:
584                 return False
585         return True
586
587     def _check_date(self, cr, uid, ids, context=None):
588         for l in self.browse(cr, uid, ids, context=context):
589             if l.journal_id.allow_date:
590                 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'):
591                     return False
592         return True
593
594     def _check_currency(self, cr, uid, ids, context=None):
595         for l in self.browse(cr, uid, ids, context=context):
596             if l.account_id.currency_id:
597                 if not l.currency_id or not l.currency_id.id == l.account_id.currency_id.id:
598                     return False
599         return True
600
601     def _check_currency_and_amount(self, cr, uid, ids, context=None):
602         for l in self.browse(cr, uid, ids, context=context):
603             if (l.amount_currency and not l.currency_id):
604                 return False
605         return True
606
607     def _check_currency_amount(self, cr, uid, ids, context=None):
608         for l in self.browse(cr, uid, ids, context=context):
609             if l.amount_currency:
610                 if (l.amount_currency > 0.0 and l.credit > 0.0) or (l.amount_currency < 0.0 and l.debit > 0.0):
611                     return False
612         return True
613
614     def _check_currency_company(self, cr, uid, ids, context=None):
615         for l in self.browse(cr, uid, ids, context=context):
616             if l.currency_id.id == l.company_id.currency_id.id:
617                 return False
618         return True
619
620     _constraints = [
621         (_check_no_view, 'You cannot create journal items on an account of type view.', ['account_id']),
622         (_check_no_closed, 'You cannot create journal items on closed account.', ['account_id']),
623         (_check_company_id, 'Account and Period must belong to the same company.', ['company_id']),
624         (_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']),
625         (_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']),
626         (_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']),
627         (_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']),
628         (_check_currency_company, "You cannot provide a secondary currency if it is the same than the company one." , ['currency_id']),
629     ]
630
631     #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id
632     def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False, context=None):
633         if context is None:
634             context = {}
635         account_obj = self.pool.get('account.account')
636         journal_obj = self.pool.get('account.journal')
637         currency_obj = self.pool.get('res.currency')
638         if (not currency_id) or (not account_id):
639             return {}
640         result = {}
641         acc = account_obj.browse(cr, uid, account_id, context=context)
642         if (amount>0) and journal:
643             x = journal_obj.browse(cr, uid, journal).default_credit_account_id
644             if x: acc = x
645         context.update({
646                 'date': date,
647                 'res.currency.compute.account': acc,
648             })
649         v = currency_obj.compute(cr, uid, currency_id, acc.company_id.currency_id.id, amount, context=context)
650         result['value'] = {
651             'debit': v > 0 and v or 0.0,
652             'credit': v < 0 and -v or 0.0
653         }
654         return result
655
656     def onchange_account_id(self, cr, uid, ids, account_id, context=None):
657         res = {'value': {}}
658         if account_id:
659             res['value']['account_tax_id'] = [x.id for x in self.pool.get('account.account').browse(cr, uid, account_id, context=context).tax_ids]
660         return res
661
662     def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False):
663         partner_obj = self.pool.get('res.partner')
664         payment_term_obj = self.pool.get('account.payment.term')
665         journal_obj = self.pool.get('account.journal')
666         fiscal_pos_obj = self.pool.get('account.fiscal.position')
667         val = {}
668         val['date_maturity'] = False
669
670         if not partner_id:
671             return {'value':val}
672         if not date:
673             date = datetime.now().strftime('%Y-%m-%d')
674         jt = False
675         if journal:
676             jt = journal_obj.browse(cr, uid, journal).type
677         part = partner_obj.browse(cr, uid, partner_id)
678
679         payment_term_id = False
680         if jt and jt in ('purchase', 'purchase_refund') and part.property_supplier_payment_term:
681             payment_term_id = part.property_supplier_payment_term.id
682         elif jt and part.property_payment_term:
683             payment_term_id = part.property_payment_term.id
684         if payment_term_id:
685             res = payment_term_obj.compute(cr, uid, payment_term_id, 100, date)
686             if res:
687                 val['date_maturity'] = res[0][0]
688         if not account_id:
689             id1 = part.property_account_payable.id
690             id2 =  part.property_account_receivable.id
691             if jt:
692                 if jt in ('sale', 'purchase_refund'):
693                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
694                 elif jt in ('purchase', 'sale_refund'):
695                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
696                 elif jt in ('general', 'bank', 'cash'):
697                     if part.customer:
698                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
699                     elif part.supplier:
700                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
701                 if val.get('account_id', False):
702                     d = self.onchange_account_id(cr, uid, ids, val['account_id'])
703                     val.update(d['value'])
704         return {'value':val}
705
706     def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False):
707         account_obj = self.pool.get('account.account')
708         partner_obj = self.pool.get('res.partner')
709         fiscal_pos_obj = self.pool.get('account.fiscal.position')
710         val = {}
711         if account_id:
712             res = account_obj.browse(cr, uid, account_id)
713             tax_ids = res.tax_ids
714             if tax_ids and partner_id:
715                 part = partner_obj.browse(cr, uid, partner_id)
716                 tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
717             else:
718                 tax_id = tax_ids and tax_ids[0].id or False
719             val['account_tax_id'] = tax_id
720         return {'value': val}
721     #
722     # type: the type if reconciliation (no logic behind this field, for info)
723     #
724     # writeoff; entry generated for the difference between the lines
725     #
726     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
727         if context is None:
728             context = {}
729         if context and context.get('next_partner_only', False):
730             if not context.get('partner_id', False):
731                 partner = self.list_partners_to_reconcile(cr, uid, context=context)
732                 if partner:
733                     partner = partner[0]
734             else:
735                 partner = context.get('partner_id', False)
736             if not partner:
737                 return []
738             args.append(('partner_id', '=', partner[0]))
739         return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
740
741     def list_partners_to_reconcile(self, cr, uid, context=None):
742         cr.execute(
743              """
744              SELECT partner_id
745              FROM (
746                 SELECT l.partner_id, p.last_reconciliation_date, SUM(l.debit) AS debit, SUM(l.credit) AS credit
747                 FROM account_move_line l
748                 RIGHT JOIN account_account a ON (a.id = l.account_id)
749                 RIGHT JOIN res_partner p ON (l.partner_id = p.id)
750                     WHERE a.reconcile IS TRUE
751                     AND l.reconcile_id IS NULL
752                     AND (p.last_reconciliation_date IS NULL OR l.date > p.last_reconciliation_date)
753                     AND l.state <> 'draft'
754                     GROUP BY l.partner_id, p.last_reconciliation_date
755                 ) AS s
756                 WHERE debit > 0 AND credit > 0
757                 ORDER BY last_reconciliation_date""")
758         ids = cr.fetchall()
759         ids = len(ids) and [x[0] for x in ids] or []
760         return self.pool.get('res.partner').name_get(cr, uid, ids, context=context)
761
762     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
763         move_rec_obj = self.pool.get('account.move.reconcile')
764         merges = []
765         unmerge = []
766         total = 0.0
767         merges_rec = []
768         company_list = []
769         if context is None:
770             context = {}
771         for line in self.browse(cr, uid, ids, context=context):
772             if company_list and not line.company_id.id in company_list:
773                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
774             company_list.append(line.company_id.id)
775
776         for line in self.browse(cr, uid, ids, context=context):
777             if line.account_id.currency_id:
778                 currency_id = line.account_id.currency_id
779             else:
780                 currency_id = line.company_id.currency_id
781             if line.reconcile_id:
782                 raise osv.except_osv(_('Warning!'), _('Already reconciled.'))
783             if line.reconcile_partial_id:
784                 for line2 in line.reconcile_partial_id.line_partial_ids:
785                     if not line2.reconcile_id:
786                         if line2.id not in merges:
787                             merges.append(line2.id)
788                         if line2.account_id.currency_id:
789                             total += line2.amount_currency
790                         else:
791                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
792                 merges_rec.append(line.reconcile_partial_id.id)
793             else:
794                 unmerge.append(line.id)
795                 if line.account_id.currency_id:
796                     total += line.amount_currency
797                 else:
798                     total += (line.debit or 0.0) - (line.credit or 0.0)
799         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
800             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)
801             return res
802         r_id = move_rec_obj.create(cr, uid, {
803             'type': type,
804             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
805         })
806         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
807         return True
808
809     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
810         account_obj = self.pool.get('account.account')
811         move_obj = self.pool.get('account.move')
812         move_rec_obj = self.pool.get('account.move.reconcile')
813         partner_obj = self.pool.get('res.partner')
814         currency_obj = self.pool.get('res.currency')
815         lines = self.browse(cr, uid, ids, context=context)
816         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
817         credit = debit = 0.0
818         currency = 0.0
819         account_id = False
820         partner_id = False
821         if context is None:
822             context = {}
823         company_list = []
824         for line in self.browse(cr, uid, ids, context=context):
825             if company_list and not line.company_id.id in company_list:
826                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
827             company_list.append(line.company_id.id)
828         for line in unrec_lines:
829             if line.state <> 'valid':
830                 raise osv.except_osv(_('Error!'),
831                         _('Entry "%s" is not valid !') % line.name)
832             credit += line['credit']
833             debit += line['debit']
834             currency += line['amount_currency'] or 0.0
835             account_id = line['account_id']['id']
836             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
837         writeoff = debit - credit
838
839         # Ifdate_p in context => take this date
840         if context.has_key('date_p') and context['date_p']:
841             date=context['date_p']
842         else:
843             date = time.strftime('%Y-%m-%d')
844
845         cr.execute('SELECT account_id, reconcile_id '\
846                    'FROM account_move_line '\
847                    'WHERE id IN %s '\
848                    'GROUP BY account_id,reconcile_id',
849                    (tuple(ids), ))
850         r = cr.fetchall()
851         #TODO: move this check to a constraint in the account_move_reconcile object
852         if not unrec_lines:
853             raise osv.except_osv(_('Error!'), _('Entry is already reconciled.'))
854         account = account_obj.browse(cr, uid, account_id, context=context)
855         if r[0][1] != None:
856             raise osv.except_osv(_('Error!'), _('Some entries are already reconciled.'))
857
858         if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
859            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
860             if not writeoff_acc_id:
861                 raise osv.except_osv(_('Warning!'), _('You have to provide an account for the write off/exchange difference entry.'))
862             if writeoff > 0:
863                 debit = writeoff
864                 credit = 0.0
865                 self_credit = writeoff
866                 self_debit = 0.0
867             else:
868                 debit = 0.0
869                 credit = -writeoff
870                 self_credit = 0.0
871                 self_debit = -writeoff
872             # If comment exist in context, take it
873             if 'comment' in context and context['comment']:
874                 libelle = context['comment']
875             else:
876                 libelle = _('Write-Off')
877
878             cur_obj = self.pool.get('res.currency')
879             cur_id = False
880             amount_currency_writeoff = 0.0
881             if context.get('company_currency_id',False) != context.get('currency_id',False):
882                 cur_id = context.get('currency_id',False)
883                 for line in unrec_lines:
884                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
885                         amount_currency_writeoff += line.amount_currency
886                     else:
887                         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})
888                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
889
890             writeoff_lines = [
891                 (0, 0, {
892                     'name': libelle,
893                     'debit': self_debit,
894                     'credit': self_credit,
895                     'account_id': account_id,
896                     'date': date,
897                     'partner_id': partner_id,
898                     'currency_id': cur_id or (account.currency_id.id or False),
899                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
900                 }),
901                 (0, 0, {
902                     'name': libelle,
903                     'debit': debit,
904                     'credit': credit,
905                     'account_id': writeoff_acc_id,
906                     'analytic_account_id': context.get('analytic_id', False),
907                     'date': date,
908                     'partner_id': partner_id,
909                     'currency_id': cur_id or (account.currency_id.id or False),
910                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
911                 })
912             ]
913
914             writeoff_move_id = move_obj.create(cr, uid, {
915                 'period_id': writeoff_period_id,
916                 'journal_id': writeoff_journal_id,
917                 'date':date,
918                 'state': 'draft',
919                 'line_id': writeoff_lines
920             })
921
922             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
923             if account_id == writeoff_acc_id:
924                 writeoff_line_ids = [writeoff_line_ids[1]]
925             ids += writeoff_line_ids
926
927         r_id = move_rec_obj.create(cr, uid, {
928             'type': type,
929             'line_id': map(lambda x: (4, x, False), ids),
930             'line_partial_ids': map(lambda x: (3, x, False), ids)
931         })
932         wf_service = netsvc.LocalService("workflow")
933         # the id of the move.reconcile is written in the move.line (self) by the create method above
934         # because of the way the line_id are defined: (4, x, False)
935         for id in ids:
936             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
937
938         if lines and lines[0]:
939             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
940             if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context):
941                 partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context)
942         return r_id
943
944     def view_header_get(self, cr, user, view_id, view_type, context=None):
945         if context is None:
946             context = {}
947         context = self.convert_to_period(cr, user, context=context)
948         if context.get('account_id', False):
949             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
950             res = cr.fetchone()
951             if res:
952                 res = _('Entries: ')+ (res[0] or '')
953             return res
954         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
955             return False
956         if context.get('search_default_journal_id', False):
957             context['journal_id'] = context.get('search_default_journal_id')
958         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
959         j = cr.fetchone()[0] or ''
960         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
961         p = cr.fetchone()[0] or ''
962         if j or p:
963             return j + (p and (':' + p) or '')
964         return False
965
966     def onchange_date(self, cr, user, ids, date, context=None):
967         """
968         Returns a dict that contains new values and context
969         @param cr: A database cursor
970         @param user: ID of the user currently logged in
971         @param date: latest value from user input for field date
972         @param args: other arguments
973         @param context: context arguments, like lang, time zone
974         @return: Returns a dict which contains new values, and context
975         """
976         res = {}
977         if context is None:
978             context = {}
979         period_pool = self.pool.get('account.period')
980         pids = period_pool.search(cr, user, [('date_start','<=',date), ('date_stop','>=',date)])
981         if pids:
982             res.update({
983                 'period_id':pids[0]
984             })
985             context.update({
986                 'period_id':pids[0]
987             })
988         return {
989             'value':res,
990             'context':context,
991         }
992
993     def _check_moves(self, cr, uid, context=None):
994         # use the first move ever created for this journal and period
995         if context is None:
996             context = {}
997         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']))
998         res = cr.fetchone()
999         if res:
1000             if res[1] != 'draft':
1001                 raise osv.except_osv(_('User Error!'),
1002                        _('The account move (%s) for centralisation ' \
1003                                 'has been confirmed.') % res[2])
1004         return res
1005
1006     def _remove_move_reconcile(self, cr, uid, move_ids=None, opening_reconciliation=False, context=None):
1007         # Function remove move rencocile ids related with moves
1008         obj_move_line = self.pool.get('account.move.line')
1009         obj_move_rec = self.pool.get('account.move.reconcile')
1010         unlink_ids = []
1011         if not move_ids:
1012             return True
1013         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1014         full_recs = filter(lambda x: x['reconcile_id'], recs)
1015         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1016         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1017         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1018         unlink_ids += rec_ids
1019         unlink_ids += part_rec_ids
1020         if unlink_ids:
1021             if opening_reconciliation:
1022                 obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
1023             obj_move_rec.unlink(cr, uid, unlink_ids)
1024         return True
1025
1026     def unlink(self, cr, uid, ids, context=None, check=True):
1027         if context is None:
1028             context = {}
1029         move_obj = self.pool.get('account.move')
1030         self._update_check(cr, uid, ids, context)
1031         result = False
1032         move_ids = set()
1033         for line in self.browse(cr, uid, ids, context=context):
1034             move_ids.add(line.move_id.id)
1035             context['journal_id'] = line.journal_id.id
1036             context['period_id'] = line.period_id.id
1037             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1038         move_ids = list(move_ids)
1039         if check and move_ids:
1040             move_obj.validate(cr, uid, move_ids, context=context)
1041         return result
1042
1043     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1044         if context is None:
1045             context={}
1046         move_obj = self.pool.get('account.move')
1047         account_obj = self.pool.get('account.account')
1048         journal_obj = self.pool.get('account.journal')
1049         if isinstance(ids, (int, long)):
1050             ids = [ids]
1051         if vals.get('account_tax_id', False):
1052             raise osv.except_osv(_('Unable to change tax!'), _('You cannot change the tax, you should remove and recreate lines.'))
1053         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1054             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1055         if update_check:
1056             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):
1057                 self._update_check(cr, uid, ids, context)
1058
1059         todo_date = None
1060         if vals.get('date', False):
1061             todo_date = vals['date']
1062             del vals['date']
1063
1064         for line in self.browse(cr, uid, ids, context=context):
1065             ctx = context.copy()
1066             if ('journal_id' not in ctx):
1067                 if line.move_id:
1068                    ctx['journal_id'] = line.move_id.journal_id.id
1069                 else:
1070                     ctx['journal_id'] = line.journal_id.id
1071             if ('period_id' not in ctx):
1072                 if line.move_id:
1073                     ctx['period_id'] = line.move_id.period_id.id
1074                 else:
1075                     ctx['period_id'] = line.period_id.id
1076             #Check for centralisation
1077             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1078             if journal.centralisation:
1079                 self._check_moves(cr, uid, context=ctx)
1080         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1081         if check:
1082             done = []
1083             for line in self.browse(cr, uid, ids):
1084                 if line.move_id.id not in done:
1085                     done.append(line.move_id.id)
1086                     move_obj.validate(cr, uid, [line.move_id.id], context)
1087                     if todo_date:
1088                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1089         return result
1090
1091     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1092         journal_obj = self.pool.get('account.journal')
1093         period_obj = self.pool.get('account.period')
1094         jour_period_obj = self.pool.get('account.journal.period')
1095         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1096         result = cr.fetchall()
1097         journal = journal_obj.browse(cr, uid, journal_id, context=context)
1098         period = period_obj.browse(cr, uid, period_id, context=context)
1099         for (state,) in result:
1100             if state == 'done':
1101                 raise osv.except_osv(_('Error !'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
1102         if not result:
1103             jour_period_obj.create(cr, uid, {
1104                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1105                 'journal_id': journal.id,
1106                 'period_id': period.id
1107             })
1108         return True
1109
1110     def _update_check(self, cr, uid, ids, context=None):
1111         done = {}
1112         for line in self.browse(cr, uid, ids, context=context):
1113             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1114             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1115                 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)
1116             if line.reconcile_id:
1117                 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)
1118             t = (line.journal_id.id, line.period_id.id)
1119             if t not in done:
1120                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1121                 done[t] = True
1122         return True
1123
1124     def create(self, cr, uid, vals, context=None, check=True):
1125         account_obj = self.pool.get('account.account')
1126         tax_obj = self.pool.get('account.tax')
1127         move_obj = self.pool.get('account.move')
1128         cur_obj = self.pool.get('res.currency')
1129         journal_obj = self.pool.get('account.journal')
1130         if context is None:
1131             context = {}
1132         if vals.get('move_id', False):
1133             company_id = self.pool.get('account.move').read(cr, uid, vals['move_id'], ['company_id']).get('company_id', False)
1134             if company_id:
1135                 vals['company_id'] = company_id[0]
1136         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1137             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1138         if 'journal_id' in vals and vals['journal_id']:
1139             context['journal_id'] = vals['journal_id']
1140         if 'period_id' in vals and vals['period_id']:
1141             context['period_id'] = vals['period_id']
1142         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1143             m = move_obj.browse(cr, uid, vals['move_id'])
1144             context['journal_id'] = m.journal_id.id
1145             context['period_id'] = m.period_id.id
1146         #we need to treat the case where a value is given in the context for period_id as a string
1147         if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)):
1148             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1149             if len(period_candidate_ids) != 1:
1150                 raise osv.except_osv(_('Error!'), _('No period found or more than one period found for the given date.'))
1151             context['period_id'] = period_candidate_ids[0][0]
1152         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1153             context['journal_id'] = context.get('search_default_journal_id')
1154         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1155         move_id = vals.get('move_id', False)
1156         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1157         vals['journal_id'] = vals.get('journal_id') or context.get('journal_id')
1158         vals['period_id'] = vals.get('period_id') or context.get('period_id')
1159         vals['date'] = vals.get('date') or context.get('date')
1160         if not move_id:
1161             if journal.centralisation:
1162                 #Check for centralisation
1163                 res = self._check_moves(cr, uid, context)
1164                 if res:
1165                     vals['move_id'] = res[0]
1166             if not vals.get('move_id', False):
1167                 if journal.sequence_id:
1168                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1169                     v = {
1170                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1171                         'period_id': context['period_id'],
1172                         'journal_id': context['journal_id']
1173                     }
1174                     if vals.get('ref', ''):
1175                         v.update({'ref': vals['ref']})
1176                     move_id = move_obj.create(cr, uid, v, context)
1177                     vals['move_id'] = move_id
1178                 else:
1179                     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.'))
1180         ok = not (journal.type_control_ids or journal.account_control_ids)
1181         if ('account_id' in vals):
1182             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1183             if journal.type_control_ids:
1184                 type = account.user_type
1185                 for t in journal.type_control_ids:
1186                     if type.code == t.code:
1187                         ok = True
1188                         break
1189             if journal.account_control_ids and not ok:
1190                 for a in journal.account_control_ids:
1191                     if a.id == vals['account_id']:
1192                         ok = True
1193                         break
1194             # Automatically convert in the account's secondary currency if there is one and
1195             # the provided values were not already multi-currency
1196             if account.currency_id and (vals.get('amount_currency', False) is False) and account.currency_id.id != account.company_id.currency_id.id:
1197                 vals['currency_id'] = account.currency_id.id
1198                 ctx = {}
1199                 if 'date' in vals:
1200                     ctx['date'] = vals['date']
1201                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1202                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1203         if not ok:
1204             raise osv.except_osv(_('Bad Account!'), _('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
1205
1206         if vals.get('analytic_account_id',False):
1207             if journal.analytic_journal_id:
1208                 vals['analytic_lines'] = [(0,0, {
1209                         'name': vals['name'],
1210                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1211                         'account_id': vals.get('analytic_account_id', False),
1212                         'unit_amount': vals.get('quantity', 1.0),
1213                         'amount': vals.get('debit', 0.0) or vals.get('credit', 0.0),
1214                         'general_account_id': vals.get('account_id', False),
1215                         'journal_id': journal.analytic_journal_id.id,
1216                         'ref': vals.get('ref', False),
1217                         'user_id': uid
1218             })]
1219
1220         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1221         # CREATE Taxes
1222         if vals.get('account_tax_id', False):
1223             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1224             total = vals['debit'] - vals['credit']
1225             if journal.type in ('purchase_refund', 'sale_refund'):
1226                 base_code = 'ref_base_code_id'
1227                 tax_code = 'ref_tax_code_id'
1228                 account_id = 'account_paid_id'
1229                 base_sign = 'ref_base_sign'
1230                 tax_sign = 'ref_tax_sign'
1231             else:
1232                 base_code = 'base_code_id'
1233                 tax_code = 'tax_code_id'
1234                 account_id = 'account_collected_id'
1235                 base_sign = 'base_sign'
1236                 tax_sign = 'tax_sign'
1237             tmp_cnt = 0
1238             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1239                 #create the base movement
1240                 if tmp_cnt == 0:
1241                     if tax[base_code]:
1242                         tmp_cnt += 1
1243                         self.write(cr, uid,[result], {
1244                             'tax_code_id': tax[base_code],
1245                             'tax_amount': tax[base_sign] * abs(total)
1246                         })
1247                 else:
1248                     data = {
1249                         'move_id': vals['move_id'],
1250                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1251                         'date': vals['date'],
1252                         'partner_id': vals.get('partner_id',False),
1253                         'ref': vals.get('ref',False),
1254                         'account_tax_id': False,
1255                         'tax_code_id': tax[base_code],
1256                         'tax_amount': tax[base_sign] * abs(total),
1257                         'account_id': vals['account_id'],
1258                         'credit': 0.0,
1259                         'debit': 0.0,
1260                     }
1261                     if data['tax_code_id']:
1262                         self.create(cr, uid, data, context)
1263                 #create the Tax movement
1264                 data = {
1265                     'move_id': vals['move_id'],
1266                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1267                     'date': vals['date'],
1268                     'partner_id': vals.get('partner_id',False),
1269                     'ref': vals.get('ref',False),
1270                     'account_tax_id': False,
1271                     'tax_code_id': tax[tax_code],
1272                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1273                     'account_id': tax[account_id] or vals['account_id'],
1274                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1275                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1276                 }
1277                 if data['tax_code_id']:
1278                     self.create(cr, uid, data, context)
1279             del vals['account_tax_id']
1280
1281         if check and ((not context.get('no_store_function')) or journal.entry_posted):
1282             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1283             if journal.entry_posted and tmp:
1284                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1285         return result
1286
1287     def list_periods(self, cr, uid, context=None):
1288         ids = self.pool.get('account.period').search(cr,uid,[])
1289         return self.pool.get('account.period').name_get(cr, uid, ids, context=context)
1290
1291     def list_journals(self, cr, uid, context=None):
1292         ng = dict(self.pool.get('account.journal').name_search(cr,uid,'',[]))
1293         ids = ng.keys()
1294         result = []
1295         for journal in self.pool.get('account.journal').browse(cr, uid, ids, context=context):
1296             result.append((journal.id,ng[journal.id],journal.type,
1297                 bool(journal.currency),bool(journal.analytic_journal_id)))
1298         return result
1299
1300 account_move_line()
1301
1302 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: