[MERGE]Latest trunk
[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 and ids[0] or False
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     def _get_period(self, cr, uid, context=None):
555         """
556         Return  default account period value
557         """
558         account_period_obj = self.pool.get('account.period')
559         ids = account_period_obj.find(cr, uid, context=context)
560         period_id = False
561         if ids:
562             period_id = ids[0]
563         return period_id
564
565     def _get_journal(self, cr, uid, context=None):
566         """
567         Return journal based on the journal type
568         """
569
570         journal_id = False
571
572         journal_pool = self.pool.get('account.journal')
573         if context.get('journal_type', False):
574             jids = journal_pool.search(cr, uid, [('type','=', context.get('journal_type'))])
575             if not jids:
576                 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'))
577             journal_id = jids[0]
578         return journal_id
579
580
581     _defaults = {
582         'blocked': False,
583         'centralisation': 'normal',
584         'date': _get_date,
585         'date_created': fields.date.context_today,
586         'state': 'draft',
587         'currency_id': _get_currency,
588         'journal_id': _get_journal,
589         'credit': 0.0,
590         'debit': 0.0,
591         'amount_currency': 0.0,
592         'account_id': lambda self, cr, uid, c: c.get('account_id', False),
593         'period_id': _get_period,
594         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.move.line', context=c)
595     }
596     _order = "date desc, id desc"
597     _sql_constraints = [
598         ('credit_debit1', 'CHECK (credit*debit=0)',  'Wrong credit or debit value in accounting entry !'),
599         ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
600     ]
601
602     def _auto_init(self, cr, context=None):
603         super(account_move_line, self)._auto_init(cr, context=context)
604         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'')
605         if not cr.fetchone():
606             cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)')
607
608     def _check_no_view(self, cr, uid, ids, context=None):
609         lines = self.browse(cr, uid, ids, context=context)
610         for l in lines:
611             if l.account_id.type == 'view':
612                 return False
613         return True
614
615     def _check_no_closed(self, cr, uid, ids, context=None):
616         lines = self.browse(cr, uid, ids, context=context)
617         for l in lines:
618             if l.account_id.type == 'closed':
619                 raise osv.except_osv(_('Error!'), _('You cannot create journal items on a closed account %s %s.') % (l.account_id.code, l.account_id.name))
620         return True
621
622     def _check_company_id(self, cr, uid, ids, context=None):
623         lines = self.browse(cr, uid, ids, context=context)
624         for l in lines:
625             if l.company_id != l.account_id.company_id or l.company_id != l.period_id.company_id:
626                 return False
627         return True
628
629     def _check_date(self, cr, uid, ids, context=None):
630         for l in self.browse(cr, uid, ids, context=context):
631             if l.journal_id.allow_date:
632                 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'):
633                     return False
634         return True
635
636     def _check_currency(self, cr, uid, ids, context=None):
637         for l in self.browse(cr, uid, ids, context=context):
638             if l.account_id.currency_id:
639                 if not l.currency_id or not l.currency_id.id == l.account_id.currency_id.id:
640                     return False
641         return True
642
643     _constraints = [
644         (_check_no_view, 'You cannot create journal items on an account of type view.', ['account_id']),
645         (_check_no_closed, 'You cannot create journal items on closed account.', ['account_id']),
646         (_check_company_id, 'Account and Period must belong to the same company.', ['company_id']),
647         (_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']),
648         (_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']),
649     ]
650
651     #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id
652     def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False, context=None):
653         if context is None:
654             context = {}
655         account_obj = self.pool.get('account.account')
656         journal_obj = self.pool.get('account.journal')
657         currency_obj = self.pool.get('res.currency')
658         if (not currency_id) or (not account_id):
659             return {}
660         result = {}
661         acc = account_obj.browse(cr, uid, account_id, context=context)
662         if (amount>0) and journal:
663             x = journal_obj.browse(cr, uid, journal).default_credit_account_id
664             if x: acc = x
665         context.update({
666                 'date': date,
667                 'res.currency.compute.account': acc,
668             })
669         v = currency_obj.compute(cr, uid, currency_id, acc.company_id.currency_id.id, amount, context=context)
670         result['value'] = {
671             'debit': v > 0 and v or 0.0,
672             'credit': v < 0 and -v or 0.0
673         }
674         return result
675
676     def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False):
677         partner_obj = self.pool.get('res.partner')
678         payment_term_obj = self.pool.get('account.payment.term')
679         journal_obj = self.pool.get('account.journal')
680         fiscal_pos_obj = self.pool.get('account.fiscal.position')
681         val = {}
682         val['date_maturity'] = False
683
684         if not partner_id:
685             return {'value':val}
686         if not date:
687             date = datetime.now().strftime('%Y-%m-%d')
688         part = partner_obj.browse(cr, uid, partner_id)
689
690         if part.property_payment_term:
691             res = payment_term_obj.compute(cr, uid, part.property_payment_term.id, 100, date)
692             if res:
693                 val['date_maturity'] = res[0][0]
694         if not account_id:
695             id1 = part.property_account_payable.id
696             id2 =  part.property_account_receivable.id
697             if journal:
698                 jt = journal_obj.browse(cr, uid, journal).type
699                 if jt in ('sale', 'purchase_refund'):
700                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
701                 elif jt in ('purchase', 'sale_refund'):
702                     val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
703                 elif jt in ('general', 'bank', 'cash'):
704                     if part.customer:
705                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id2)
706                     elif part.supplier:
707                         val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
708                 if val.get('account_id', False):
709                     d = self.onchange_account_id(cr, uid, ids, val['account_id'])
710                     val.update(d['value'])
711         return {'value':val}
712
713     def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False):
714         account_obj = self.pool.get('account.account')
715         partner_obj = self.pool.get('res.partner')
716         fiscal_pos_obj = self.pool.get('account.fiscal.position')
717         val = {}
718         if account_id:
719             res = account_obj.browse(cr, uid, account_id)
720             tax_ids = res.tax_ids
721             if tax_ids and partner_id:
722                 part = partner_obj.browse(cr, uid, partner_id)
723                 tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
724             else:
725                 tax_id = tax_ids and tax_ids[0].id or False
726             val['account_tax_id'] = tax_id
727         return {'value': val}
728     #
729     # type: the type if reconciliation (no logic behind this field, for info)
730     #
731     # writeoff; entry generated for the difference between the lines
732     #
733     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
734         if context is None:
735             context = {}
736         if context and context.get('next_partner_only', False):
737             if not context.get('partner_id', False):
738                 partner = self.list_partners_to_reconcile(cr, uid, context=context)
739                 if partner:
740                     partner = partner[0]
741             else:
742                 partner = context.get('partner_id', False)
743             if not partner:
744                 return []
745             args.append(('partner_id', '=', partner[0]))
746         return super(account_move_line, self).search(cr, uid, args, offset, limit, order, context, count)
747
748     def list_partners_to_reconcile(self, cr, uid, context=None):
749         cr.execute(
750              """
751              SELECT partner_id
752              FROM (
753                 SELECT l.partner_id, p.last_reconciliation_date, SUM(l.debit) AS debit, SUM(l.credit) AS credit
754                 FROM account_move_line l
755                 RIGHT JOIN account_account a ON (a.id = l.account_id)
756                 RIGHT JOIN res_partner p ON (l.partner_id = p.id)
757                     WHERE a.reconcile IS TRUE
758                     AND l.reconcile_id IS NULL
759                     AND (p.last_reconciliation_date IS NULL OR l.date > p.last_reconciliation_date)
760                     AND l.state <> 'draft'
761                     GROUP BY l.partner_id, p.last_reconciliation_date
762                 ) AS s
763                 WHERE debit > 0 AND credit > 0
764                 ORDER BY last_reconciliation_date""")
765         ids = cr.fetchall()
766         ids = len(ids) and [x[0] for x in ids] or []
767         return self.pool.get('res.partner').name_get(cr, uid, ids, context=context)
768
769     def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False):
770         move_rec_obj = self.pool.get('account.move.reconcile')
771         merges = []
772         unmerge = []
773         total = 0.0
774         merges_rec = []
775         company_list = []
776         if context is None:
777             context = {}
778         for line in self.browse(cr, uid, ids, context=context):
779             if company_list and not line.company_id.id in company_list:
780                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
781             company_list.append(line.company_id.id)
782
783         for line in self.browse(cr, uid, ids, context=context):
784             if line.account_id.currency_id:
785                 currency_id = line.account_id.currency_id
786             else:
787                 currency_id = line.company_id.currency_id
788             if line.reconcile_id:
789                 raise osv.except_osv(_('Warning!'), _('Already reconciled.'))
790             if line.reconcile_partial_id:
791                 for line2 in line.reconcile_partial_id.line_partial_ids:
792                     if not line2.reconcile_id:
793                         if line2.id not in merges:
794                             merges.append(line2.id)
795                         if line2.account_id.currency_id:
796                             total += line2.amount_currency
797                         else:
798                             total += (line2.debit or 0.0) - (line2.credit or 0.0)
799                 merges_rec.append(line.reconcile_partial_id.id)
800             else:
801                 unmerge.append(line.id)
802                 if line.account_id.currency_id:
803                     total += line.amount_currency
804                 else:
805                     total += (line.debit or 0.0) - (line.credit or 0.0)
806         if self.pool.get('res.currency').is_zero(cr, uid, currency_id, total):
807             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)
808             return res
809         r_id = move_rec_obj.create(cr, uid, {
810             'type': type,
811             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
812         })
813         move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
814         return True
815
816     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
817         account_obj = self.pool.get('account.account')
818         move_obj = self.pool.get('account.move')
819         move_rec_obj = self.pool.get('account.move.reconcile')
820         partner_obj = self.pool.get('res.partner')
821         currency_obj = self.pool.get('res.currency')
822         lines = self.browse(cr, uid, ids, context=context)
823         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
824         credit = debit = 0.0
825         currency = 0.0
826         account_id = False
827         partner_id = False
828         if context is None:
829             context = {}
830         company_list = []
831         for line in self.browse(cr, uid, ids, context=context):
832             if company_list and not line.company_id.id in company_list:
833                 raise osv.except_osv(_('Warning!'), _('To reconcile the entries company should be the same for all entries.'))
834             company_list.append(line.company_id.id)
835         for line in unrec_lines:
836             if line.state <> 'valid':
837                 raise osv.except_osv(_('Error!'),
838                         _('Entry "%s" is not valid !') % line.name)
839             credit += line['credit']
840             debit += line['debit']
841             currency += line['amount_currency'] or 0.0
842             account_id = line['account_id']['id']
843             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
844         writeoff = debit - credit
845
846         # Ifdate_p in context => take this date
847         if context.has_key('date_p') and context['date_p']:
848             date=context['date_p']
849         else:
850             date = time.strftime('%Y-%m-%d')
851
852         cr.execute('SELECT account_id, reconcile_id '\
853                    'FROM account_move_line '\
854                    'WHERE id IN %s '\
855                    'GROUP BY account_id,reconcile_id',
856                    (tuple(ids), ))
857         r = cr.fetchall()
858         #TODO: move this check to a constraint in the account_move_reconcile object
859         if not unrec_lines:
860             raise osv.except_osv(_('Error!'), _('Entry is already reconciled.'))
861         account = account_obj.browse(cr, uid, account_id, context=context)
862         if r[0][1] != None:
863             raise osv.except_osv(_('Error!'), _('Some entries are already reconciled.'))
864
865         if (not currency_obj.is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
866            (account.currency_id and (not currency_obj.is_zero(cr, uid, account.currency_id, currency))):
867             if not writeoff_acc_id:
868                 raise osv.except_osv(_('Warning!'), _('You have to provide an account for the write off/exchange difference entry.'))
869             if writeoff > 0:
870                 debit = writeoff
871                 credit = 0.0
872                 self_credit = writeoff
873                 self_debit = 0.0
874             else:
875                 debit = 0.0
876                 credit = -writeoff
877                 self_credit = 0.0
878                 self_debit = -writeoff
879             # If comment exist in context, take it
880             if 'comment' in context and context['comment']:
881                 libelle = context['comment']
882             else:
883                 libelle = _('Write-Off')
884
885             cur_obj = self.pool.get('res.currency')
886             cur_id = False
887             amount_currency_writeoff = 0.0
888             if context.get('company_currency_id',False) != context.get('currency_id',False):
889                 cur_id = context.get('currency_id',False)
890                 for line in unrec_lines:
891                     if line.currency_id and line.currency_id.id == context.get('currency_id',False):
892                         amount_currency_writeoff += line.amount_currency
893                     else:
894                         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})
895                         amount_currency_writeoff += (line.debit > 0) and tmp_amount or -tmp_amount
896
897             writeoff_lines = [
898                 (0, 0, {
899                     'name': libelle,
900                     'debit': self_debit,
901                     'credit': self_credit,
902                     'account_id': account_id,
903                     'date': date,
904                     'partner_id': partner_id,
905                     'currency_id': cur_id or (account.currency_id.id or False),
906                     'amount_currency': amount_currency_writeoff and -1 * amount_currency_writeoff or (account.currency_id.id and -1 * currency or 0.0)
907                 }),
908                 (0, 0, {
909                     'name': libelle,
910                     'debit': debit,
911                     'credit': credit,
912                     'account_id': writeoff_acc_id,
913                     'analytic_account_id': context.get('analytic_id', False),
914                     'date': date,
915                     'partner_id': partner_id,
916                     'currency_id': cur_id or (account.currency_id.id or False),
917                     'amount_currency': amount_currency_writeoff and amount_currency_writeoff or (account.currency_id.id and currency or 0.0)
918                 })
919             ]
920
921             writeoff_move_id = move_obj.create(cr, uid, {
922                 'period_id': writeoff_period_id,
923                 'journal_id': writeoff_journal_id,
924                 'date':date,
925                 'state': 'draft',
926                 'line_id': writeoff_lines
927             })
928
929             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
930             if account_id == writeoff_acc_id:
931                 writeoff_line_ids = [writeoff_line_ids[1]]
932             ids += writeoff_line_ids
933
934         r_id = move_rec_obj.create(cr, uid, {
935             'type': type,
936             'line_id': map(lambda x: (4, x, False), ids),
937             'line_partial_ids': map(lambda x: (3, x, False), ids)
938         })
939         wf_service = netsvc.LocalService("workflow")
940         # the id of the move.reconcile is written in the move.line (self) by the create method above
941         # because of the way the line_id are defined: (4, x, False)
942         for id in ids:
943             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
944
945         if lines and lines[0]:
946             partner_id = lines[0].partner_id and lines[0].partner_id.id or False
947             if partner_id and not partner_obj.has_something_to_reconcile(cr, uid, partner_id, context=context):
948                 partner_obj.mark_as_reconciled(cr, uid, [partner_id], context=context)
949         return r_id
950
951     def view_header_get(self, cr, user, view_id, view_type, context=None):
952         if context is None:
953             context = {}
954         context = self.convert_to_period(cr, user, context=context)
955         if context.get('account_id', False):
956             cr.execute('SELECT code FROM account_account WHERE id = %s', (context['account_id'], ))
957             res = cr.fetchone()
958             if res:
959                 res = _('Entries: ')+ (res[0] or '')
960             return res
961         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
962             return False
963         if context.get('search_default_journal_id', False):
964             context['journal_id'] = context.get('search_default_journal_id')
965         cr.execute('SELECT code FROM account_journal WHERE id = %s', (context['journal_id'], ))
966         j = cr.fetchone()[0] or ''
967         cr.execute('SELECT code FROM account_period WHERE id = %s', (context['period_id'], ))
968         p = cr.fetchone()[0] or ''
969         if j or p:
970             return j + (p and (':' + p) or '')
971         return False
972
973     def onchange_date(self, cr, user, ids, date, context=None):
974         """
975         Returns a dict that contains new values and context
976         @param cr: A database cursor
977         @param user: ID of the user currently logged in
978         @param date: latest value from user input for field date
979         @param args: other arguments
980         @param context: context arguments, like lang, time zone
981         @return: Returns a dict which contains new values, and context
982         """
983         res = {}
984         if context is None:
985             context = {}
986         period_pool = self.pool.get('account.period')
987         pids = period_pool.search(cr, user, [('date_start','<=',date), ('date_stop','>=',date)])
988         if pids:
989             res.update({
990                 'period_id':pids[0]
991             })
992             context.update({
993                 'period_id':pids[0]
994             })
995         return {
996             'value':res,
997             'context':context,
998         }
999
1000     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1001         journal_pool = self.pool.get('account.journal')
1002         if context is None:
1003             context = {}
1004         result = super(account_move_line, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
1005         if (view_type != 'tree') or view_id:
1006             #Remove the toolbar from the form view
1007             if view_type == 'form':
1008                 if result.get('toolbar', False):
1009                     result['toolbar']['action'] = []
1010             #Restrict the list of journal view in search view
1011             if view_type == 'search' and result['fields'].get('journal_id', False):
1012                 result['fields']['journal_id']['selection'] = journal_pool.name_search(cr, uid, '', [], context=context)
1013                 ctx = context.copy()
1014                 #we add the refunds journal in the selection field of journal
1015                 if context.get('journal_type', False) == 'sale':
1016                     ctx.update({'journal_type': 'sale_refund'})
1017                     result['fields']['journal_id']['selection'] += journal_pool.name_search(cr, uid, '', [], context=ctx)
1018                 elif context.get('journal_type', False) == 'purchase':
1019                     ctx.update({'journal_type': 'purchase_refund'})
1020                     result['fields']['journal_id']['selection'] += journal_pool.name_search(cr, uid, '', [], context=ctx)
1021             return result
1022         if context.get('view_mode', False):
1023             return result
1024         fld = []
1025         flds = []
1026         title = _("Accounting Entries")  # self.view_header_get(cr, uid, view_id, view_type, context)
1027
1028         ids = journal_pool.search(cr, uid, [], context=context)
1029         journals = journal_pool.browse(cr, uid, ids, context=context)
1030         for journal in journals:
1031             for field in journal.view_id.columns_id:
1032                 # sometimes, it's possible that a defined column is not loaded (the module containing
1033                 # this field is not loaded) when we make an update.
1034                 if field.field not in self._columns:
1035                     continue
1036
1037                 if not field.field in flds:
1038                     fld.append((field.field, field.sequence))
1039                     flds.append(field.field)
1040
1041         default_columns = {
1042             'period_id': 3,
1043             'journal_id': 10,
1044             'state': sys.maxint,
1045         }
1046         for d in default_columns:
1047             if d not in flds:
1048                 fld.append((d, default_columns[d]))
1049                 flds.append(d)
1050
1051         fld = sorted(fld, key=itemgetter(1))
1052         widths = {
1053             'statement_id': 50,
1054             'state': 60,
1055             'tax_code_id': 50,
1056             'move_id': 40,
1057         }
1058
1059         document = etree.Element('tree', string=title, editable="top",
1060                                  on_write="on_create_write",
1061                                  colors="red:state=='draft';black:state=='valid'")
1062         fields_get = self.fields_get(cr, uid, flds, context)
1063         for field, _seq in fld:
1064             # TODO add string to element
1065             f = etree.SubElement(document, 'field', name=field)
1066
1067             if field == 'debit':
1068                 f.set('sum', _("Total debit"))
1069
1070             elif field == 'credit':
1071                 f.set('sum', _("Total credit"))
1072
1073             elif field == 'move_id':
1074                 f.set('required', 'False')
1075
1076             elif field == 'account_tax_id':
1077                 f.set('domain', "[('parent_id', '=' ,False)]")
1078                 f.set('context', "{'journal_id': journal_id}")
1079
1080             elif field == 'account_id' and journal.id:
1081                 f.set('domain', "[('journal_id', '=', journal_id),('type','!=','view'), ('type','!=','closed')]")
1082                 f.set('on_change', 'onchange_account_id(account_id, partner_id)')
1083
1084             elif field == 'partner_id':
1085                 f.set('on_change', 'onchange_partner_id(move_id, partner_id, account_id, debit, credit, date, journal_id)')
1086
1087             elif field == 'journal_id':
1088                 f.set('context', "{'journal_id': journal_id}")
1089
1090             elif field == 'statement_id':
1091                 f.set('domain', "[('state', '!=', 'confirm'),('journal_id.type', '=', 'bank')]")
1092                 f.set('invisible', 'True')
1093
1094             elif field == 'date':
1095                 f.set('on_change', 'onchange_date(date)')
1096
1097             elif field == 'analytic_account_id':
1098                 # Currently it is not working due to being executed by superclass's fields_view_get
1099                 # f.set('groups', 'analytic.group_analytic_accounting')
1100                 pass
1101
1102             if field in ('amount_currency', 'currency_id'):
1103                 f.set('on_change', 'onchange_currency(account_id, amount_currency, currency_id, date, journal_id)')
1104                 f.set('attrs', "{'readonly': [('state', '=', 'valid')]}")
1105
1106             if field in widths:
1107                 f.set('width', str(widths[field]))
1108
1109             if field in ('journal_id',):
1110                 f.set("invisible", "context.get('journal_id', False)")
1111             elif field in ('period_id',):
1112                 f.set("invisible", "context.get('period_id', False)")
1113
1114             orm.setup_modifiers(f, fields_get[field], context=context,
1115                                 in_tree_view=True)
1116
1117         result['arch'] = etree.tostring(document, pretty_print=True)
1118         result['fields'] = fields_get
1119         return result
1120
1121     def _check_moves(self, cr, uid, context=None):
1122         # use the first move ever created for this journal and period
1123         if context is None:
1124             context = {}
1125         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']))
1126         res = cr.fetchone()
1127         if res:
1128             if res[1] != 'draft':
1129                 raise osv.except_osv(_('User Error!'),
1130                        _('The account move (%s) for centralisation ' \
1131                                 'has been confirmed.') % res[2])
1132         return res
1133
1134     def _remove_move_reconcile(self, cr, uid, move_ids=None, context=None):
1135         # Function remove move rencocile ids related with moves
1136         obj_move_line = self.pool.get('account.move.line')
1137         obj_move_rec = self.pool.get('account.move.reconcile')
1138         unlink_ids = []
1139         if not move_ids:
1140             return True
1141         recs = obj_move_line.read(cr, uid, move_ids, ['reconcile_id', 'reconcile_partial_id'])
1142         full_recs = filter(lambda x: x['reconcile_id'], recs)
1143         rec_ids = [rec['reconcile_id'][0] for rec in full_recs]
1144         part_recs = filter(lambda x: x['reconcile_partial_id'], recs)
1145         part_rec_ids = [rec['reconcile_partial_id'][0] for rec in part_recs]
1146         unlink_ids += rec_ids
1147         unlink_ids += part_rec_ids
1148         if unlink_ids:
1149             obj_move_rec.unlink(cr, uid, unlink_ids)
1150         return True
1151
1152     def unlink(self, cr, uid, ids, context=None, check=True):
1153         if context is None:
1154             context = {}
1155         move_obj = self.pool.get('account.move')
1156         self._update_check(cr, uid, ids, context)
1157         result = False
1158         move_ids = set()
1159         for line in self.browse(cr, uid, ids, context=context):
1160             move_ids.add(line.move_id.id)
1161             context['journal_id'] = line.journal_id.id
1162             context['period_id'] = line.period_id.id
1163             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
1164         move_ids = list(move_ids)
1165         if check and move_ids:
1166             move_obj.validate(cr, uid, move_ids, context=context)
1167         return result
1168
1169     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1170         if context is None:
1171             context={}
1172         move_obj = self.pool.get('account.move')
1173         account_obj = self.pool.get('account.account')
1174         journal_obj = self.pool.get('account.journal')
1175         if isinstance(ids, (int, long)):
1176             ids = [ids]
1177         if vals.get('account_tax_id', False):
1178             raise osv.except_osv(_('Unable to change tax!'), _('You cannot change the tax, you should remove and recreate lines.'))
1179         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1180             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1181         if update_check:
1182             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):
1183                 self._update_check(cr, uid, ids, context)
1184
1185         todo_date = None
1186         if vals.get('date', False):
1187             todo_date = vals['date']
1188             del vals['date']
1189
1190         for line in self.browse(cr, uid, ids, context=context):
1191             ctx = context.copy()
1192             if ('journal_id' not in ctx):
1193                 if line.move_id:
1194                    ctx['journal_id'] = line.move_id.journal_id.id
1195                 else:
1196                     ctx['journal_id'] = line.journal_id.id
1197             if ('period_id' not in ctx):
1198                 if line.move_id:
1199                     ctx['period_id'] = line.move_id.period_id.id
1200                 else:
1201                     ctx['period_id'] = line.period_id.id
1202             #Check for centralisation
1203             journal = journal_obj.browse(cr, uid, ctx['journal_id'], context=ctx)
1204             if journal.centralisation:
1205                 self._check_moves(cr, uid, context=ctx)
1206         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
1207         if check:
1208             done = []
1209             for line in self.browse(cr, uid, ids):
1210                 if line.move_id.id not in done:
1211                     done.append(line.move_id.id)
1212                     move_obj.validate(cr, uid, [line.move_id.id], context)
1213                     if todo_date:
1214                         move_obj.write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
1215         return result
1216
1217     def _update_journal_check(self, cr, uid, journal_id, period_id, context=None):
1218         journal_obj = self.pool.get('account.journal')
1219         period_obj = self.pool.get('account.period')
1220         jour_period_obj = self.pool.get('account.journal.period')
1221         cr.execute('SELECT state FROM account_journal_period WHERE journal_id = %s AND period_id = %s', (journal_id, period_id))
1222         result = cr.fetchall()
1223         journal = journal_obj.browse(cr, uid, journal_id, context=context)
1224         period = period_obj.browse(cr, uid, period_id, context=context)
1225         for (state,) in result:
1226             if state == 'done':
1227                 raise osv.except_osv(_('Error !'), _('You can not add/modify entries in a closed period %s of journal %s.' % (period.name,journal.name)))                
1228         if not result:
1229             jour_period_obj.create(cr, uid, {
1230                 'name': (journal.code or journal.name)+':'+(period.name or ''),
1231                 'journal_id': journal.id,
1232                 'period_id': period.id
1233             })
1234         return True
1235
1236     def _update_check(self, cr, uid, ids, context=None):
1237         done = {}
1238         for line in self.browse(cr, uid, ids, context=context):
1239             err_msg = _('Move name (id): %s (%s)') % (line.move_id.name, str(line.move_id.id))
1240             if line.move_id.state <> 'draft' and (not line.journal_id.entry_posted):
1241                 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)
1242             if line.reconcile_id:
1243                 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)
1244             t = (line.journal_id.id, line.period_id.id)
1245             if t not in done:
1246                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
1247                 done[t] = True
1248         return True
1249
1250     def create(self, cr, uid, vals, context=None, check=True):
1251         account_obj = self.pool.get('account.account')
1252         tax_obj = self.pool.get('account.tax')
1253         move_obj = self.pool.get('account.move')
1254         cur_obj = self.pool.get('res.currency')
1255         journal_obj = self.pool.get('account.journal')
1256         if context is None:
1257             context = {}
1258         if vals.get('move_id', False):
1259             company_id = self.pool.get('account.move').read(cr, uid, vals['move_id'], ['company_id']).get('company_id', False)
1260             if company_id:
1261                 vals['company_id'] = company_id[0]
1262         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
1263             raise osv.except_osv(_('Bad Account!'), _('You cannot use an inactive account.'))
1264         if 'journal_id' in vals and vals['journal_id']:
1265             context['journal_id'] = vals['journal_id']
1266         if 'period_id' in vals and vals['period_id']:
1267             context['period_id'] = vals['period_id']
1268         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
1269             m = move_obj.browse(cr, uid, vals['move_id'])
1270             context['journal_id'] = m.journal_id.id
1271             context['period_id'] = m.period_id.id
1272         #we need to treat the case where a value is given in the context for period_id as a string
1273         if 'period_id' in context and not isinstance(context.get('period_id', ''), (int, long)):
1274             period_candidate_ids = self.pool.get('account.period').name_search(cr, uid, name=context.get('period_id',''))
1275             if len(period_candidate_ids) != 1:
1276                 raise osv.except_osv(_('Error!'), _('No period found or more than one period found for the given date.'))
1277             context['period_id'] = period_candidate_ids[0][0]
1278         if not context.get('journal_id', False) and context.get('search_default_journal_id', False):
1279             context['journal_id'] = context.get('search_default_journal_id')
1280         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
1281         move_id = vals.get('move_id', False)
1282         journal = journal_obj.browse(cr, uid, context['journal_id'], context=context)
1283         vals['journal_id'] = vals.get('journal_id') or context.get('journal_id')
1284         vals['period_id'] = vals.get('period_id') or context.get('period_id')
1285         vals['date'] = vals.get('date') or context.get('date')
1286         if not move_id:
1287             if journal.centralisation:
1288                 #Check for centralisation
1289                 res = self._check_moves(cr, uid, context)
1290                 if res:
1291                     vals['move_id'] = res[0]
1292             if not vals.get('move_id', False):
1293                 if journal.sequence_id:
1294                     #name = self.pool.get('ir.sequence').next_by_id(cr, uid, journal.sequence_id.id)
1295                     v = {
1296                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1297                         'period_id': context['period_id'],
1298                         'journal_id': context['journal_id']
1299                     }
1300                     if vals.get('ref', ''):
1301                         v.update({'ref': vals['ref']})
1302                     move_id = move_obj.create(cr, uid, v, context)
1303                     vals['move_id'] = move_id
1304                 else:
1305                     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.'))
1306         ok = not (journal.type_control_ids or journal.account_control_ids)
1307         if ('account_id' in vals):
1308             account = account_obj.browse(cr, uid, vals['account_id'], context=context)
1309             if journal.type_control_ids:
1310                 type = account.user_type
1311                 for t in journal.type_control_ids:
1312                     if type.code == t.code:
1313                         ok = True
1314                         break
1315             if journal.account_control_ids and not ok:
1316                 for a in journal.account_control_ids:
1317                     if a.id == vals['account_id']:
1318                         ok = True
1319                         break
1320             # Automatically convert in the account's secondary currency if there is one and
1321             # the provided values were not already multi-currency
1322             if account.currency_id and (vals.get('amount_currency', False) is False) and account.currency_id.id != account.company_id.currency_id.id:
1323                 vals['currency_id'] = account.currency_id.id
1324                 ctx = {}
1325                 if 'date' in vals:
1326                     ctx['date'] = vals['date']
1327                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
1328                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0), context=ctx)
1329         if not ok:
1330             raise osv.except_osv(_('Bad Account!'), _('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
1331
1332         if vals.get('analytic_account_id',False):
1333             if journal.analytic_journal_id:
1334                 vals['analytic_lines'] = [(0,0, {
1335                         'name': vals['name'],
1336                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
1337                         'account_id': vals.get('analytic_account_id', False),
1338                         'unit_amount': vals.get('quantity', 1.0),
1339                         'amount': vals.get('debit', 0.0) or vals.get('credit', 0.0),
1340                         'general_account_id': vals.get('account_id', False),
1341                         'journal_id': journal.analytic_journal_id.id,
1342                         'ref': vals.get('ref', False),
1343                         'user_id': uid
1344             })]
1345
1346         result = super(account_move_line, self).create(cr, uid, vals, context=context)
1347         # CREATE Taxes
1348         if vals.get('account_tax_id', False):
1349             tax_id = tax_obj.browse(cr, uid, vals['account_tax_id'])
1350             total = vals['debit'] - vals['credit']
1351             if journal.type in ('purchase_refund', 'sale_refund'):
1352                 base_code = 'ref_base_code_id'
1353                 tax_code = 'ref_tax_code_id'
1354                 account_id = 'account_paid_id'
1355                 base_sign = 'ref_base_sign'
1356                 tax_sign = 'ref_tax_sign'
1357             else:
1358                 base_code = 'base_code_id'
1359                 tax_code = 'tax_code_id'
1360                 account_id = 'account_collected_id'
1361                 base_sign = 'base_sign'
1362                 tax_sign = 'tax_sign'
1363             tmp_cnt = 0
1364             for tax in tax_obj.compute_all(cr, uid, [tax_id], total, 1.00, force_excluded=True).get('taxes'):
1365                 #create the base movement
1366                 if tmp_cnt == 0:
1367                     if tax[base_code]:
1368                         tmp_cnt += 1
1369                         self.write(cr, uid,[result], {
1370                             'tax_code_id': tax[base_code],
1371                             'tax_amount': tax[base_sign] * abs(total)
1372                         })
1373                 else:
1374                     data = {
1375                         'move_id': vals['move_id'],
1376                         'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1377                         'date': vals['date'],
1378                         'partner_id': vals.get('partner_id',False),
1379                         'ref': vals.get('ref',False),
1380                         'account_tax_id': False,
1381                         'tax_code_id': tax[base_code],
1382                         'tax_amount': tax[base_sign] * abs(total),
1383                         'account_id': vals['account_id'],
1384                         'credit': 0.0,
1385                         'debit': 0.0,
1386                     }
1387                     if data['tax_code_id']:
1388                         self.create(cr, uid, data, context)
1389                 #create the Tax movement
1390                 data = {
1391                     'move_id': vals['move_id'],
1392                     'name': tools.ustr(vals['name'] or '') + ' ' + tools.ustr(tax['name'] or ''),
1393                     'date': vals['date'],
1394                     'partner_id': vals.get('partner_id',False),
1395                     'ref': vals.get('ref',False),
1396                     'account_tax_id': False,
1397                     'tax_code_id': tax[tax_code],
1398                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
1399                     'account_id': tax[account_id] or vals['account_id'],
1400                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
1401                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
1402                 }
1403                 if data['tax_code_id']:
1404                     self.create(cr, uid, data, context)
1405             del vals['account_tax_id']
1406
1407         if check and ((not context.get('no_store_function')) or journal.entry_posted):
1408             tmp = move_obj.validate(cr, uid, [vals['move_id']], context)
1409             if journal.entry_posted and tmp:
1410                 move_obj.button_validate(cr,uid, [vals['move_id']], context)
1411         return result
1412
1413     def list_periods(self, cr, uid, context=None):
1414         ids = self.pool.get('account.period').search(cr,uid,[])
1415         return self.pool.get('account.period').name_get(cr, uid, ids, context=context)
1416
1417     def list_journals(self, cr, uid, context=None):
1418         ids = self.pool.get('account.journal').search(cr,uid,[])
1419         return self.pool.get('account.journal').name_get(cr, uid, ids, context=context)
1420
1421 account_move_line()
1422
1423 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: