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