[REF] removed explicit model instanciations.
[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              """SELECT partner_id FROM (
745                 SELECT l.partner_id, p.last_reconciliation_date, SUM(l.debit) AS debit, SUM(l.credit) AS credit, MAX(l.create_date) AS max_date
746                 FROM account_move_line l
747                 RIGHT JOIN account_account a ON (a.id = l.account_id)
748                 RIGHT JOIN res_partner p ON (l.partner_id = p.id)
749                     WHERE a.reconcile IS TRUE
750                     AND l.reconcile_id IS NULL
751                     AND l.state <> 'draft'
752                     GROUP BY l.partner_id, p.last_reconciliation_date
753                 ) AS s
754                 WHERE debit > 0 AND credit > 0 AND (last_reconciliation_date IS NULL OR max_date > last_reconciliation_date)
755                 ORDER BY last_reconciliation_date""")
756         ids = [x[0] for x in cr.fetchall()]
757         if not ids: 
758             return []
759
760         # To apply the ir_rules
761         partner_obj = self.pool.get('res.partner')
762         ids = partner_obj.search(cr, uid, [('id', 'in', ids)], context=context)
763         return partner_obj.name_get(cr, uid, ids, context=context)
764
765     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
766         move_rec_obj = self.pool.get('account.move.reconcile')
767         merges = []
768         unmerge = []
769         total = 0.0
770         merges_rec = []
771         company_list = []
772         if context is None:
773             context = {}
774         for line in self.browse(cr, uid, ids, context=context):
775             if company_list and not line.company_id.id in company_list:
776                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
777             company_list.append(line.company_id.id)
778
779         for line in self.browse(cr, uid, ids, context=context):
780             if line.account_id.currency_id:
781                 currency_id = line.account_id.currency_id
782             else:
783                 currency_id = line.company_id.currency_id
784             if line.reconcile_id:
785                 raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name)) 
786             if line.reconcile_partial_id:
787                 for line2 in line.reconcile_partial_id.line_partial_ids:
788                     if not line2.reconcile_id:
789                         if line2.id not in merges:
790                             merges.append(line2.id)
791                         if line2.account_id.currency_id:
792                             total += line2.amount_currency
793                         else:
794                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
795                 merges_rec.append(line.reconcile_partial_id.id)
796             else:
797                 unmerge.append(line.id)
798                 if line.account_id.currency_id:
799                     total += line.amount_currency
800                 else:
801                     total += (line.debit or 0.0) - (line.credit or 0.0)
802         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
803             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)
804             return res
805         r_id = move_rec_obj.create(cr, uid, {
806             'type': type,
807             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
808         })
809         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
810         return True
811
812     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
813         account_obj = self.pool.get('account.account')
814         move_obj = self.pool.get('account.move')
815         move_rec_obj = self.pool.get('account.move.reconcile')
816         partner_obj = self.pool.get('res.partner')
817         currency_obj = self.pool.get('res.currency')
818         lines = self.browse(cr, uid, ids, context=context)
819         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
820         credit = debit = 0.0
821         currency = 0.0
822         account_id = False
823         partner_id = False
824         if context is None:
825             context = {}
826         company_list = []
827         for line in self.browse(cr, uid, ids, context=context):
828             if company_list and not line.company_id.id in company_list:
829                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
830             company_list.append(line.company_id.id)
831         for line in unrec_lines:
832             if line.state <> 'valid':
833                 raise osv.except_osv(_('Error!'),
834                         _('Entry "%s" is not valid !') % line.name)
835             credit += line['credit']
836             debit += line['debit']
837             currency += line['amount_currency'] or 0.0
838             account_id = line['account_id']['id']
839             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
840         writeoff = debit - credit
841
842         # Ifdate_p in context => take this date
843         if context.has_key('date_p') and context['date_p']:
844             date=context['date_p']
845         else:
846             date = time.strftime('%Y-%m-%d')
847
848         cr.execute('SELECT account_id, reconcile_id '\
849                    'FROM account_move_line '\
850                    'WHERE id IN %s '\
851                    'GROUP BY account_id,reconcile_id',
852                    (tuple(ids), ))
853         r = cr.fetchall()
854         #TODO: move this check to a constraint in the account_move_reconcile object
855         if not unrec_lines:
856             raise osv.except_osv(_('Error!'), _('Entry is already reconciled.'))
857         account = account_obj.browse(cr, uid, account_id, context=context)
858         if r[0][1] != None:
859             raise osv.except_osv(_('Error!'), _('Some entries are already reconciled.'))
860
861         if context.get('fy_closing'):
862             # We don't want to generate any write-off when being called from the
863             # wizard used to close a fiscal year (and it doesn't give us any
864             # writeoff_acc_id).
865             pass
866         elif (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
867            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
868             if not writeoff_acc_id:
869                 raise osv.except_osv(_('Warning!'), _('You have to provide an account for the write off/exchange difference entry.'))
870             if writeoff > 0:
871                 debit = writeoff
872                 credit = 0.0
873                 self_credit = writeoff
874                 self_debit = 0.0
875             else:
876                 debit = 0.0
877                 credit = -writeoff
878                 self_credit = 0.0
879                 self_debit = -writeoff
880             # If comment exist in context, take it
881             if 'comment' in context and context['comment']:
882                 libelle = context['comment']
883             else:
884                 libelle = _('Write-Off')
885
886             cur_obj = self.pool.get('res.currency')
887             cur_id = False
888             amount_currency_writeoff = 0.0
889             if context.get('company_currency_id',False) != context.get('currency_id',False):
890                 cur_id = context.get('currency_id',False)
891                 for line in unrec_lines:
892                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
893                         amount_currency_writeoff += line.amount_currency
894                     else:
895                         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})
896                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
897
898             writeoff_lines = [
899                 (0, 0, {
900                     'name': libelle,
901                     'debit': self_debit,
902                     'credit': self_credit,
903                     'account_id': account_id,
904                     'date': date,
905                     'partner_id': partner_id,
906                     'currency_id': cur_id or (account.currency_id.id or False),
907                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
908                 }),
909                 (0, 0, {
910                     'name': libelle,
911                     'debit': debit,
912                     'credit': credit,
913                     'account_id': writeoff_acc_id,
914                     'analytic_account_id': context.get('analytic_id', False),
915                     'date': date,
916                     'partner_id': partner_id,
917                     'currency_id': cur_id or (account.currency_id.id or False),
918                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
919                 })
920             ]
921
922             writeoff_move_id = move_obj.create(cr, uid, {
923                 'period_id': writeoff_period_id,
924                 'journal_id': writeoff_journal_id,
925                 'date':date,
926                 'state': 'draft',
927                 'line_id': writeoff_lines
928             })
929
930             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
931             if account_id == writeoff_acc_id:
932                 writeoff_line_ids = [writeoff_line_ids[1]]
933             ids += writeoff_line_ids
934
935         r_id = move_rec_obj.create(cr, uid, {
936             'type': type,
937             'line_id': map(lambda x: (4, x, False), ids),
938             'line_partial_ids': map(lambda x: (3, x, False), ids)
939         })
940         wf_service = netsvc.LocalService("workflow")
941         # the id of the move.reconcile is written in the move.line (self) by the create method above
942         # because of the way the line_id are defined: (4, x, False)
943         for id in ids:
944             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
945
946         if lines and lines[0]:
947             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
948             if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context):
949                 partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context)
950         return r_id
951
952     def view_header_get(self, cr, user, view_id, view_type, context=None):
953         if context is None:
954             context = {}
955         context = self.convert_to_period(cr, user, context=context)
956         if context.get('account_id', False):
957             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
958             res = cr.fetchone()
959             if res:
960                 res = _('Entries: ')+ (res[0] or '')
961             return res
962         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
963             return False
964         if context.get('search_default_journal_id', False):
965             context['journal_id'] = context.get('search_default_journal_id')
966         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
967         j = cr.fetchone()[0] or ''
968         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
969         p = cr.fetchone()[0] or ''
970         if j or p:
971             return j + (p and (':' + p) or '')
972         return False
973
974     def onchange_date(self, cr, user, ids, date, context=None):
975         """
976         Returns a dict that contains new values and context
977         @param cr: A database cursor
978         @param user: ID of the user currently logged in
979         @param date: latest value from user input for field date
980         @param args: other arguments
981         @param context: context arguments, like lang, time zone
982         @return: Returns a dict which contains new values, and context
983         """
984         res = {}
985         if context is None:
986             context = {}
987         period_pool = self.pool.get('account.period')
988         ctx = dict(context, account_period_prefer_normal=True)
989         pids = period_pool.find(cr, user, date, context=ctx)
990         if pids:
991             res.update({
992                 'period_id':pids[0]
993             })
994             context.update({
995                 'period_id':pids[0]
996             })
997         return {
998             'value':res,
999             'context':context,
1000         }
1001
1002     def _check_moves(self, cr, uid, context=None):
1003         # use the first move ever created for this journal and period
1004         if context is None:
1005             context = {}
1006         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']))
1007         res = cr.fetchone()
1008         if res:
1009             if res[1] != 'draft':
1010                 raise osv.except_osv(_('User Error!'),
1011                        _('The account move (%s) for centralisation ' \
1012                                 'has been confirmed.') % res[2])
1013         return res
1014
1015     def _remove_move_reconcile(self, cr, uid, move_ids=None, opening_reconciliation=False, context=None):
1016         # Function remove move rencocile ids related with moves
1017         obj_move_line = self.pool.get('account.move.line')
1018         obj_move_rec = self.pool.get('account.move.reconcile')
1019         unlink_ids = []
1020         if not move_ids:
1021             return True
1022         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1023         full_recs = filter(lambda x: x['reconcile_id'], recs)
1024         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1025         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1026         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1027         unlink_ids += rec_ids
1028         unlink_ids += part_rec_ids
1029         if unlink_ids:
1030             if opening_reconciliation:
1031                 obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
1032             obj_move_rec.unlink(cr, uid, unlink_ids)
1033         return True
1034
1035     def unlink(self, cr, uid, ids, context=None, check=True):
1036         if context is None:
1037             context = {}
1038         move_obj = self.pool.get('account.move')
1039         self._update_check(cr, uid, ids, context)
1040         result = False
1041         move_ids = set()
1042         for line in self.browse(cr, uid, ids, context=context):
1043             move_ids.add(line.move_id.id)
1044             context['journal_id'] = line.journal_id.id
1045             context['period_id'] = line.period_id.id
1046             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1047         move_ids = list(move_ids)
1048         if check and move_ids:
1049             move_obj.validate(cr, uid, move_ids, context=context)
1050         return result
1051
1052     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1053         if context is None:
1054             context={}
1055         move_obj = self.pool.get('account.move')
1056         account_obj = self.pool.get('account.account')
1057         journal_obj = self.pool.get('account.journal')
1058         if isinstance(ids, (int, long)):
1059             ids = [ids]
1060         if vals.get('account_tax_id', False):
1061             raise osv.except_osv(_('Unable to change tax!'), _('You cannot change the tax, you should remove and recreate lines.'))
1062         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1063             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1064         if update_check:
1065             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):
1066                 self._update_check(cr, uid, ids, context)
1067
1068         todo_date = None
1069         if vals.get('date', False):
1070             todo_date = vals['date']
1071             del vals['date']
1072
1073         for line in self.browse(cr, uid, ids, context=context):
1074             ctx = context.copy()
1075             if ('journal_id' not in ctx):
1076                 if line.move_id:
1077                    ctx['journal_id'] = line.move_id.journal_id.id
1078                 else:
1079                     ctx['journal_id'] = line.journal_id.id
1080             if ('period_id' not in ctx):
1081                 if line.move_id:
1082                     ctx['period_id'] = line.move_id.period_id.id
1083                 else:
1084                     ctx['period_id'] = line.period_id.id
1085             #Check for centralisation
1086             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1087             if journal.centralisation:
1088                 self._check_moves(cr, uid, context=ctx)
1089         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1090         if check:
1091             done = []
1092             for line in self.browse(cr, uid, ids):
1093                 if line.move_id.id not in done:
1094                     done.append(line.move_id.id)
1095                     move_obj.validate(cr, uid, [line.move_id.id], context)
1096                     if todo_date:
1097                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1098         return result
1099
1100     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1101         journal_obj = self.pool.get('account.journal')
1102         period_obj = self.pool.get('account.period')
1103         jour_period_obj = self.pool.get('account.journal.period')
1104         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1105         result = cr.fetchall()
1106         journal = journal_obj.browse(cr, uid, journal_id, context=context)
1107         period = period_obj.browse(cr, uid, period_id, context=context)
1108         for (state,) in result:
1109             if state == 'done':
1110                 raise osv.except_osv(_('Error !'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
1111         if not result:
1112             jour_period_obj.create(cr, uid, {
1113                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1114                 'journal_id': journal.id,
1115                 'period_id': period.id
1116             })
1117         return True
1118
1119     def _update_check(self, cr, uid, ids, context=None):
1120         done = {}
1121         for line in self.browse(cr, uid, ids, context=context):
1122             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1123             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1124                 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)
1125             if line.reconcile_id:
1126                 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)
1127             t = (line.journal_id.id, line.period_id.id)
1128             if t not in done:
1129                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1130                 done[t] = True
1131         return True
1132
1133     def create(self, cr, uid, vals, context=None, check=True):
1134         account_obj = self.pool.get('account.account')
1135         tax_obj = self.pool.get('account.tax')
1136         move_obj = self.pool.get('account.move')
1137         cur_obj = self.pool.get('res.currency')
1138         journal_obj = self.pool.get('account.journal')
1139         if context is None:
1140             context = {}
1141         if vals.get('move_id', False):
1142             move = self.pool.get('account.move').browse(cr, uid, vals['move_id'], context=context)
1143             if move.company_id:
1144                 vals['company_id'] = move.company_id.id
1145             if move.date and not vals.get('date'):
1146                 vals['date'] = move.date
1147         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1148             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1149         if 'journal_id' in vals and vals['journal_id']:
1150             context['journal_id'] = vals['journal_id']
1151         if 'period_id' in vals and vals['period_id']:
1152             context['period_id'] = vals['period_id']
1153         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1154             m = move_obj.browse(cr, uid, vals['move_id'])
1155             context['journal_id'] = m.journal_id.id
1156             context['period_id'] = m.period_id.id
1157         #we need to treat the case where a value is given in the context for period_id as a string
1158         if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)):
1159             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1160             if len(period_candidate_ids) != 1:
1161                 raise osv.except_osv(_('Error!'), _('No period found or more than one period found for the given date.'))
1162             context['period_id'] = period_candidate_ids[0][0]
1163         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1164             context['journal_id'] = context.get('search_default_journal_id')
1165         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1166         move_id = vals.get('move_id', False)
1167         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1168         vals['journal_id'] = vals.get('journal_id') or context.get('journal_id')
1169         vals['period_id'] = vals.get('period_id') or context.get('period_id')
1170         vals['date'] = vals.get('date') or context.get('date')
1171         if not move_id:
1172             if journal.centralisation:
1173                 #Check for centralisation
1174                 res = self._check_moves(cr, uid, context)
1175                 if res:
1176                     vals['move_id'] = res[0]
1177             if not vals.get('move_id', False):
1178                 if journal.sequence_id:
1179                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1180                     v = {
1181                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1182                         'period_id': context['period_id'],
1183                         'journal_id': context['journal_id']
1184                     }
1185                     if vals.get('ref', ''):
1186                         v.update({'ref': vals['ref']})
1187                     move_id = move_obj.create(cr, uid, v, context)
1188                     vals['move_id'] = move_id
1189                 else:
1190                     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.'))
1191         ok = not (journal.type_control_ids or journal.account_control_ids)
1192         if ('account_id' in vals):
1193             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1194             if journal.type_control_ids:
1195                 type = account.user_type
1196                 for t in journal.type_control_ids:
1197                     if type.code == t.code:
1198                         ok = True
1199                         break
1200             if journal.account_control_ids and not ok:
1201                 for a in journal.account_control_ids:
1202                     if a.id == vals['account_id']:
1203                         ok = True
1204                         break
1205             # Automatically convert in the account's secondary currency if there is one and
1206             # the provided values were not already multi-currency
1207             if account.currency_id and (vals.get('amount_currency', False) is False) and account.currency_id.id != account.company_id.currency_id.id:
1208                 vals['currency_id'] = account.currency_id.id
1209                 ctx = {}
1210                 if 'date' in vals:
1211                     ctx['date'] = vals['date']
1212                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1213                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1214         if not ok:
1215             raise osv.except_osv(_('Bad Account!'), _('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
1216
1217         if vals.get('analytic_account_id',False):
1218             if journal.analytic_journal_id:
1219                 vals['analytic_lines'] = [(0,0, {
1220                         'name': vals['name'],
1221                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1222                         'account_id': vals.get('analytic_account_id', False),
1223                         'unit_amount': vals.get('quantity', 1.0),
1224                         'amount': vals.get('debit', 0.0) or vals.get('credit', 0.0),
1225                         'general_account_id': vals.get('account_id', False),
1226                         'journal_id': journal.analytic_journal_id.id,
1227                         'ref': vals.get('ref', False),
1228                         'user_id': uid
1229             })]
1230
1231         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1232         # CREATE Taxes
1233         if vals.get('account_tax_id', False):
1234             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1235             total = vals['debit'] - vals['credit']
1236             if journal.type in ('purchase_refund', 'sale_refund'):
1237                 base_code = 'ref_base_code_id'
1238                 tax_code = 'ref_tax_code_id'
1239                 account_id = 'account_paid_id'
1240                 base_sign = 'ref_base_sign'
1241                 tax_sign = 'ref_tax_sign'
1242             else:
1243                 base_code = 'base_code_id'
1244                 tax_code = 'tax_code_id'
1245                 account_id = 'account_collected_id'
1246                 base_sign = 'base_sign'
1247                 tax_sign = 'tax_sign'
1248             tmp_cnt = 0
1249             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1250                 #create the base movement
1251                 if tmp_cnt == 0:
1252                     if tax[base_code]:
1253                         tmp_cnt += 1
1254                         self.write(cr, uid,[result], {
1255                             'tax_code_id': tax[base_code],
1256                             'tax_amount': tax[base_sign] * abs(total)
1257                         })
1258                 else:
1259                     data = {
1260                         'move_id': vals['move_id'],
1261                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1262                         'date': vals['date'],
1263                         'partner_id': vals.get('partner_id',False),
1264                         'ref': vals.get('ref',False),
1265                         'account_tax_id': False,
1266                         'tax_code_id': tax[base_code],
1267                         'tax_amount': tax[base_sign] * abs(total),
1268                         'account_id': vals['account_id'],
1269                         'credit': 0.0,
1270                         'debit': 0.0,
1271                     }
1272                     if data['tax_code_id']:
1273                         self.create(cr, uid, data, context)
1274                 #create the Tax movement
1275                 data = {
1276                     'move_id': vals['move_id'],
1277                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1278                     'date': vals['date'],
1279                     'partner_id': vals.get('partner_id',False),
1280                     'ref': vals.get('ref',False),
1281                     'account_tax_id': False,
1282                     'tax_code_id': tax[tax_code],
1283                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1284                     'account_id': tax[account_id] or vals['account_id'],
1285                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1286                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1287                 }
1288                 if data['tax_code_id']:
1289                     self.create(cr, uid, data, context)
1290             del vals['account_tax_id']
1291
1292         if check and ((not context.get('no_store_function')) or journal.entry_posted):
1293             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1294             if journal.entry_posted and tmp:
1295                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1296         return result
1297
1298     def list_periods(self, cr, uid, context=None):
1299         ids = self.pool.get('account.period').search(cr,uid,[])
1300         return self.pool.get('account.period').name_get(cr, uid, ids, context=context)
1301
1302     def list_journals(self, cr, uid, context=None):
1303         ng = dict(self.pool.get('account.journal').name_search(cr,uid,'',[]))
1304         ids = ng.keys()
1305         result = []
1306         for journal in self.pool.get('account.journal').browse(cr, uid, ids, context=context):
1307             result.append((journal.id,ng[journal.id],journal.type,
1308                 bool(journal.currency),bool(journal.analytic_journal_id)))
1309         return result
1310
1311
1312 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: