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