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