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