[Merged] stable and trunk addons
[odoo/odoo.git] / addons / account / account_move_line.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import time
24 import netsvc
25 from osv import fields, osv
26 from tools.translate import _
27
28 import mx.DateTime
29 from mx.DateTime import RelativeDateTime, now, DateTime, localtime
30
31 class account_move_line(osv.osv):
32     _name = "account.move.line"
33     _description = "Entry lines"
34
35     def _query_get(self, cr, uid, obj='l', context={}):
36         fiscalyear_obj = self.pool.get('account.fiscalyear')
37         if not context.get('fiscalyear', False):
38             fiscalyear_ids = fiscalyear_obj.search(cr, uid, [('state', '=', 'draft')])
39             fiscalyear_clause = (','.join([str(x) for x in fiscalyear_ids])) or '0'
40         else:
41             fiscalyear_clause = '%s' % context['fiscalyear']
42         state=context.get('state',False)
43         where_move_state = ''
44         where_move_lines_by_date = ''
45
46         if context.get('date_from', False) and context.get('date_to', False):
47             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']+"')"
48             
49         if state:
50             if state.lower() not in ['all']:
51                 where_move_state= " AND "+obj+".move_id in (select id from account_move where account_move.state = '"+state+"')"
52         
53                 
54         if context.get('periods', False):
55             ids = ','.join([str(x) for x in context['periods']])
56             return 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)
57         else:
58             return 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)
59
60     def default_get(self, cr, uid, fields, context={}):
61         data = self._default_get(cr, uid, fields, context)
62         for f in data.keys():
63             if f not in fields:
64                 del data[f]
65         return data
66
67     def create_analytic_lines(self, cr, uid, ids, context={}):
68         for obj_line in self.browse(cr, uid, ids, context):
69             if obj_line.analytic_account_id:
70                 if not obj_line.journal_id.analytic_journal_id:
71                     raise osv.except_osv(_('No Analytic Journal !'),_("You have to define an analytic journal on the '%s' journal!") % (obj_line.journal_id.name,))
72                 amt = (obj_line.credit or  0.0) - (obj_line.debit or 0.0)
73                 vals_lines={
74                     'name': obj_line.name,
75                     'date': obj_line.date,
76                     'account_id': obj_line.analytic_account_id.id,
77                     'unit_amount':obj_line.quantity,
78                     'product_id': obj_line.product_id and obj_line.product_id.id or False,
79                     'product_uom_id': obj_line.product_uom_id and obj_line.product_uom_id.id or False,
80                     'amount': amt,
81                     'general_account_id': obj_line.account_id.id,
82                     'journal_id': obj_line.journal_id.analytic_journal_id.id,
83                     'ref': obj_line.ref,
84                     'move_id':obj_line.id
85                 }
86                 new_id = self.pool.get('account.analytic.line').create(cr,uid,vals_lines)
87         return True
88
89     def _default_get_move_form_hook(self, cursor, user, data):
90         '''Called in the end of default_get method for manual entry in account_move form'''
91         if data.has_key('analytic_account_id'):
92             del(data['analytic_account_id'])
93         if data.has_key('account_tax_id'):
94             del(data['account_tax_id'])
95         return data
96
97     def _default_get(self, cr, uid, fields, context={}):
98         # Compute simple values
99         data = super(account_move_line, self).default_get(cr, uid, fields, context)
100         # Starts: Manual entry from account.move form
101         if context.get('lines',[]):
102
103             total_new=0.00
104             for i in context['lines']:
105                 total_new +=(i[2]['debit'] or 0.00)- (i[2]['credit'] or 0.00)
106                 for item in i[2]:
107                         data[item]=i[2][item]
108             if context['journal']:
109                 journal_obj=self.pool.get('account.journal').browse(cr,uid,context['journal'])
110                 if journal_obj.type == 'purchase':
111                     if total_new>0:
112                         account = journal_obj.default_credit_account_id
113                     else:
114                         account = journal_obj.default_debit_account_id
115                 else:
116                     if total_new>0:
117                         account = journal_obj.default_credit_account_id
118                     else:
119                         account = journal_obj.default_debit_account_id
120
121
122                 if account and ((not fields) or ('debit' in fields) or ('credit' in fields)) and 'partner_id' in data and (data['partner_id']):
123                     part = self.pool.get('res.partner').browse(cr, uid, data['partner_id'])
124                     account = self.pool.get('account.fiscal.position').map_account(cr, uid, part and part.property_account_position or False, account.id)
125                     account = self.pool.get('account.account').browse(cr, uid, account)
126                     data['account_id'] =  account.id
127
128             s = -total_new
129             data['debit'] = s>0  and s or 0.0
130             data['credit'] = s<0  and -s or 0.0
131             data = self._default_get_move_form_hook(cr, uid, data)
132             return data
133         # Ends: Manual entry from account.move form
134
135         if not 'move_id' in fields: #we are not in manual entry
136             return data
137
138         period_obj = self.pool.get('account.period')
139
140         # Compute the current move
141         move_id = False
142         partner_id = False
143         if context.get('journal_id',False) and context.get('period_id',False):
144             if 'move_id' in fields:
145                 cr.execute('select move_id \
146                     from \
147                         account_move_line \
148                     where \
149                         journal_id=%s and period_id=%s and create_uid=%s and state=%s \
150                     order by id desc limit 1',
151                     (context['journal_id'], context['period_id'], uid, 'draft'))
152                 res = cr.fetchone()
153                 move_id = (res and res[0]) or False
154
155                 if not move_id:
156                     return data
157                 else:
158                     data['move_id'] = move_id
159             if 'date' in fields:
160                 cr.execute('select date  \
161                     from \
162                         account_move_line \
163                     where \
164                         journal_id=%s and period_id=%s and create_uid=%s \
165                     order by id desc',
166                     (context['journal_id'], context['period_id'], uid))
167                 res = cr.fetchone()
168                 if res:
169                     data['date'] = res[0]
170                 else:
171                     period = period_obj.browse(cr, uid, context['period_id'],
172                             context=context)
173                     data['date'] = period.date_start
174
175         if not move_id:
176             return data
177
178         total = 0
179         ref_id = False
180         move = self.pool.get('account.move').browse(cr, uid, move_id, context)
181         if 'name' in fields:
182             data.setdefault('name', move.line_id[-1].name)
183         acc1 = False
184         for l in move.line_id:
185             acc1 = l.account_id
186             partner_id = partner_id or l.partner_id.id
187             ref_id = ref_id or l.ref
188             total += (l.debit or 0.0) - (l.credit or 0.0)
189
190         if 'ref' in fields:
191             data['ref'] = ref_id
192         if 'partner_id' in fields:
193             data['partner_id'] = partner_id
194
195         if move.journal_id.type == 'purchase':
196             if total>0:
197                 account = move.journal_id.default_credit_account_id
198             else:
199                 account = move.journal_id.default_debit_account_id
200         else:
201             if total>0:
202                 account = move.journal_id.default_credit_account_id
203             else:
204                 account = move.journal_id.default_debit_account_id
205
206         part = partner_id and self.pool.get('res.partner').browse(cr, uid, partner_id) or False
207         # part = False is acceptable for fiscal position.
208         account = self.pool.get('account.fiscal.position').map_account(cr, uid, part and part.property_account_position or False, account.id)
209         if account:
210             account = self.pool.get('account.account').browse(cr, uid, account)
211
212         if account and ((not fields) or ('debit' in fields) or ('credit' in fields)):
213             data['account_id'] = account.id
214             # Propose the price VAT excluded, the VAT will be added when confirming line
215             if account.tax_ids:
216                 taxes = self.pool.get('account.fiscal.position').map_tax(cr, uid, part and part.property_account_position or False, account.tax_ids)
217                 tax = self.pool.get('account.tax').browse(cr, uid, taxes)
218                 for t in self.pool.get('account.tax').compute_inv(cr, uid, tax, total, 1):
219                     total -= t['amount']
220
221         s = -total
222         data['debit'] = s>0  and s or 0.0
223         data['credit'] = s<0  and -s or 0.0
224
225         if account and account.currency_id:
226             data['currency_id'] = account.currency_id.id
227             acc = account
228             if s>0:
229                 acc = acc1
230             v = self.pool.get('res.currency').compute(cr, uid,
231                 account.company_id.currency_id.id,
232                 data['currency_id'],
233                 s, account=acc, account_invert=True)
234             data['amount_currency'] = v
235         return data
236
237     def _on_create_write(self, cr, uid, id, context={}):
238         ml = self.browse(cr, uid, id, context)
239         return map(lambda x: x.id, ml.move_id.line_id)
240
241     def _balance(self, cr, uid, ids, prop, unknow_none, unknow_dict):
242         res={}
243         # TODO group the foreach in sql
244         for id in ids:
245             cr.execute('SELECT date,account_id FROM account_move_line WHERE id=%s', (id,))
246             dt, acc = cr.fetchone()
247             cr.execute('SELECT SUM(debit-credit) FROM account_move_line WHERE account_id=%s AND (date<%s OR (date=%s AND id<=%s))', (acc,dt,dt,id))
248             res[id] = cr.fetchone()[0]
249         return res
250
251     def _invoice(self, cursor, user, ids, name, arg, context=None):
252         invoice_obj = self.pool.get('account.invoice')
253         res = {}
254         for line_id in ids:
255             res[line_id] = False
256         cursor.execute('SELECT l.id, i.id ' \
257                 'FROM account_move_line l, account_invoice i ' \
258                 'WHERE l.move_id = i.move_id ' \
259                     'AND l.id in (' + ','.join([str(x) for x in ids]) + ')')
260         invoice_ids = []
261         for line_id, invoice_id in cursor.fetchall():
262             res[line_id] = invoice_id
263             invoice_ids.append(invoice_id)
264         invoice_names = {False: ''}
265         for invoice_id, name in invoice_obj.name_get(cursor, user,
266                 invoice_ids, context=context):
267             invoice_names[invoice_id] = name
268         for line_id in res.keys():
269             invoice_id = res[line_id]
270             res[line_id] = (invoice_id, invoice_names[invoice_id])
271         return res
272
273     def name_get(self, cr, uid, ids, context={}):
274         if not len(ids):
275             return []
276         result = []
277         for line in self.browse(cr, uid, ids, context):
278             if line.ref:
279                 result.append((line.id, (line.name or '')+' ('+line.ref+')'))
280             else:
281                 result.append((line.id, line.name))
282         return result
283
284     def _invoice_search(self, cursor, user, obj, name, args):
285         if not len(args):
286             return []
287         invoice_obj = self.pool.get('account.invoice')
288
289         i = 0
290         while i < len(args):
291             fargs = args[i][0].split('.', 1)
292             if len(fargs) > 1:
293                 args[i] = (fargs[0], 'in', invoice_obj.search(cursor, user,
294                     [(fargs[1], args[i][1], args[i][2])]))
295                 i += 1
296                 continue
297             if isinstance(args[i][2], basestring):
298                 res_ids = invoice_obj.name_search(cursor, user, args[i][2], [],
299                         args[i][1])
300                 args[i] = (args[i][0], 'in', [x[0] for x in res_ids])
301             i += 1
302         qu1, qu2 = [], []
303         for x in args:
304             if x[1] != 'in':
305                 if (x[2] is False) and (x[1] == '='):
306                     qu1.append('(i.id IS NULL)')
307                 elif (x[2] is False) and (x[1] == '<>' or x[1] == '!='):
308                     qu1.append('(i.id IS NOT NULL)')
309                 else:
310                     qu1.append('(i.id %s %s)' % (x[1], '%s'))
311                     qu2.append(x[2])
312             elif x[1] == 'in':
313                 if len(x[2]) > 0:
314                     qu1.append('(i.id in (%s))' % (','.join(['%s'] * len(x[2]))))
315                     qu2 += x[2]
316                 else:
317                     qu1.append(' (False)')
318         if len(qu1):
319             qu1 = ' AND' + ' AND'.join(qu1)
320         else:
321             qu1 = ''
322         cursor.execute('SELECT l.id ' \
323                 'FROM account_move_line l, account_invoice i ' \
324                 'WHERE l.move_id = i.move_id ' + qu1, qu2)
325         res = cursor.fetchall()
326         if not len(res):
327             return [('id', '=', '0')]
328         return [('id', 'in', [x[0] for x in res])]
329
330     def _get_move_lines(self, cr, uid, ids, context={}):
331         result = []
332         for move in self.pool.get('account.move').browse(cr, uid, ids, context=context):
333             for line in move.line_id:
334                 result.append(line.id)
335         return result
336
337     _columns = {
338         'name': fields.char('Name', size=64, required=True),
339         '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 usefull for some reports."),
340         'product_uom_id': fields.many2one('product.uom', 'UoM'),
341         'product_id': fields.many2one('product.product', 'Product'),
342         'debit': fields.float('Debit', digits=(16,2)),
343         'credit': fields.float('Credit', digits=(16,2)),
344         'account_id': fields.many2one('account.account', 'Account', required=True, ondelete="cascade", domain=[('type','<>','view'), ('type', '<>', 'closed')], select=2),
345         'move_id': fields.many2one('account.move', 'Move', ondelete="cascade", states={'valid':[('readonly',True)]}, help="The move of this entry line.", select=2),
346
347         'ref': fields.char('Ref.', size=32),
348         'statement_id': fields.many2one('account.bank.statement', 'Statement', help="The bank statement used for bank reconciliation", select=1),
349         'reconcile_id': fields.many2one('account.move.reconcile', 'Reconcile', readonly=True, ondelete='set null', select=2),
350         'reconcile_partial_id': fields.many2one('account.move.reconcile', 'Partial Reconcile', readonly=True, ondelete='set null', select=2),
351         'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry."),
352         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
353
354         'period_id': fields.many2one('account.period', 'Period', required=True, select=2),
355         'journal_id': fields.many2one('account.journal', 'Journal', required=True, select=1),
356         'blocked': fields.boolean('Litigation', help="You can check this box to mark the entry line as a litigation with the associated partner"),
357
358         'partner_id': fields.many2one('res.partner', 'Partner Ref.'),
359         'date_maturity': fields.date('Maturity date', help="This field is used for payable and receivable entries. You can put the limit date for the payment of this entry line."),
360         'date': fields.related('move_id','date', string='Effective date', type='date', required=True,
361             store={
362                 'account.move': (_get_move_lines, ['date'], 20)
363             }),
364         'date_created': fields.date('Creation date'),
365         'analytic_lines': fields.one2many('account.analytic.line', 'move_id', 'Analytic lines'),
366         'centralisation': fields.selection([('normal','Normal'),('credit','Credit Centralisation'),('debit','Debit Centralisation')], 'Centralisation', size=6),
367         'balance': fields.function(_balance, method=True, string='Balance'),
368         'state': fields.selection([('draft','Draft'), ('valid','Valid')], 'Status', readonly=True),
369         'tax_code_id': fields.many2one('account.tax.code', 'Tax Account', help="The Account can either be a base tax code or tax code account."),
370         'tax_amount': fields.float('Tax/Base Amount', digits=(16,2), select=True, help="If the Tax account is tax code account, this field will contain the taxed amount.If the tax account is base tax code,\
371                     this field will contain the basic amount(without tax)."),
372         'invoice': fields.function(_invoice, method=True, string='Invoice',
373             type='many2one', relation='account.invoice', fnct_search=_invoice_search),
374         'account_tax_id':fields.many2one('account.tax', 'Tax'),
375         'analytic_account_id' : fields.many2one('account.analytic.account', 'Analytic Account'),
376 #TODO: remove this
377         'amount_taxed':fields.float("Taxed Amount",digits=(16,2)),
378
379     }
380
381     def _get_date(self, cr, uid, context):
382         period_obj = self.pool.get('account.period')
383         dt = time.strftime('%Y-%m-%d')
384         if ('journal_id' in context) and ('period_id' in context):
385             cr.execute('select date from account_move_line ' \
386                     'where journal_id=%s and period_id=%s ' \
387                     'order by id desc limit 1',
388                     (context['journal_id'], context['period_id']))
389             res = cr.fetchone()
390             if res:
391                 dt = res[0]
392             else:
393                 period = period_obj.browse(cr, uid, context['period_id'],
394                         context=context)
395                 dt = period.date_start
396         return dt
397     def _get_currency(self, cr, uid, context={}):
398         if not context.get('journal_id', False):
399             return False
400         cur = self.pool.get('account.journal').browse(cr, uid, context['journal_id']).currency
401         return cur and cur.id or False
402
403     _defaults = {
404         'blocked': lambda *a: False,
405         'centralisation': lambda *a: 'normal',
406         'date': _get_date,
407         'date_created': lambda *a: time.strftime('%Y-%m-%d'),
408         'state': lambda *a: 'draft',
409         'currency_id': _get_currency,
410         'journal_id': lambda self, cr, uid, c: c.get('journal_id', False),
411         'period_id': lambda self, cr, uid, c: c.get('period_id', False),
412     }
413     _order = "date desc,id desc"
414     _sql_constraints = [
415         ('credit_debit1', 'CHECK (credit*debit=0)',  'Wrong credit or debit value in accounting entry !'),
416         ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
417     ]
418
419     def _auto_init(self, cr, context={}):
420         super(account_move_line, self)._auto_init(cr, context)
421         cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'')
422         if not cr.fetchone():
423             cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)')
424             cr.commit()
425
426     def _check_no_view(self, cr, uid, ids):
427         lines = self.browse(cr, uid, ids)
428         for l in lines:
429             if l.account_id.type == 'view':
430                 return False
431         return True
432
433     def _check_no_closed(self, cr, uid, ids):
434         lines = self.browse(cr, uid, ids)
435         for l in lines:
436             if l.account_id.type == 'closed':
437                 return False
438         return True
439
440     _constraints = [
441         (_check_no_view, 'You can not create move line on view account.', ['account_id']),
442         (_check_no_closed, 'You can not create move line on closed account.', ['account_id']),
443     ]
444
445     #TODO: ONCHANGE_ACCOUNT_ID: set account_tax_id
446
447     def onchange_currency(self, cr, uid, ids, account_id, amount, currency_id, date=False, journal=False):
448         if (not currency_id) or (not account_id):
449             return {}
450         result = {}
451         acc =self.pool.get('account.account').browse(cr, uid, account_id)
452         if (amount>0) and journal:
453             x = self.pool.get('account.journal').browse(cr, uid, journal).default_credit_account_id
454             if x: acc = x
455         v = self.pool.get('res.currency').compute(cr, uid, currency_id,acc.company_id.currency_id.id, amount, account=acc)
456         result['value'] = {
457             'debit': v>0 and v or 0.0,
458             'credit': v<0 and -v or 0.0
459         }
460         return result
461
462     def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False):
463         val = {}
464         val['date_maturity'] = False
465
466         if not partner_id:
467             return {'value':val}
468         if not date:
469             date = now().strftime('%Y-%m-%d')
470         part = self.pool.get('res.partner').browse(cr, uid, partner_id)
471
472         if part.property_payment_term and part.property_payment_term.line_ids:
473             payterm = part.property_payment_term.line_ids[0]
474             res = self.pool.get('account.payment.term').compute(cr, uid, payterm.id, 100, date)
475             if res:
476                 val['date_maturity'] = res[0][0]
477         if not account_id:
478             id1 = part.property_account_payable.id
479             id2 =  part.property_account_receivable.id
480             if journal:
481                 jt = self.pool.get('account.journal').browse(cr, uid, journal).type
482                 if jt=='sale':
483                     val['account_id'] = self.pool.get('account.fiscal.position').map_account(cr, uid, part and part.property_account_position or False, id2)
484
485                 elif jt=='purchase':
486                     val['account_id'] = self.pool.get('account.fiscal.position').map_account(cr, uid, part and part.property_account_position or False, id1)
487                 if val.get('account_id', False):
488                     d = self.onchange_account_id(cr, uid, ids, val['account_id'])
489                     val.update(d['value'])
490
491         return {'value':val}
492
493     def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False):
494         val = {}
495         if account_id:
496             res = self.pool.get('account.account').browse(cr, uid, account_id)
497             tax_ids = res.tax_ids
498             if tax_ids and partner_id:
499                 part = self.pool.get('res.partner').browse(cr, uid, partner_id)
500                 tax_id = self.pool.get('account.fiscal.position').map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
501             else:
502                 tax_id = tax_ids and tax_ids[0].id or False
503             val['account_tax_id'] = tax_id
504         return {'value':val}
505
506     #
507     # type: the type if reconciliation (no logic behind this field, for info)
508     #
509     # writeoff; entry generated for the difference between the lines
510     #
511
512     def reconcile_partial(self, cr, uid, ids, type='auto', context={}):
513         merges = []
514         unmerge = []
515         total = 0.0
516         merges_rec = []
517         for line in self.browse(cr, uid, ids, context):
518             if line.reconcile_id:
519                 raise osv.except_osv(_('Already Reconciled'), _('Already Reconciled'))
520             if line.reconcile_partial_id:
521                 for line2 in line.reconcile_partial_id.line_partial_ids:
522                     if not line2.reconcile_id:
523                         merges.append(line2.id)
524                         total += (line2.debit or 0.0) - (line2.credit or 0.0)
525                 merges_rec.append(line.reconcile_partial_id.id)
526             else:
527                 unmerge.append(line.id)
528                 total += (line.debit or 0.0) - (line.credit or 0.0)
529         if not total:
530             res = self.reconcile(cr, uid, merges+unmerge, context=context)
531             return res
532         r_id = self.pool.get('account.move.reconcile').create(cr, uid, {
533             'type': type,
534             'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
535         })
536         self.pool.get('account.move.reconcile').reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
537         return True
538
539     def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context={}):
540         id_set = ','.join(map(str, ids))
541         
542         lines = self.browse(cr, uid, ids, context=context)
543         unrec_lines = filter(lambda x: not x['reconcile_id'], lines)
544         credit = debit = 0.0
545         currency = 0.0
546         account_id = False
547         partner_id = False
548         for line in unrec_lines:
549             if line.state <> 'valid':
550                 raise osv.except_osv(_('Error'),
551                         _('Entry "%s" is not valid !') % line.name)
552             credit += line['credit']
553             debit += line['debit']
554             currency += line['amount_currency'] or 0.0
555             account_id = line['account_id']['id']
556             partner_id = (line['partner_id'] and line['partner_id']['id']) or False
557         writeoff = debit - credit
558         # Ifdate_p in context => take this date
559         if context.has_key('date_p') and context['date_p']:
560             date=context['date_p']
561         else:
562             date = time.strftime('%Y-%m-%d')
563
564         cr.execute('SELECT account_id, reconcile_id \
565                 FROM account_move_line \
566                 WHERE id IN ('+id_set+') \
567                 GROUP BY account_id,reconcile_id')
568         r = cr.fetchall()
569 #TODO: move this check to a constraint in the account_move_reconcile object
570         if (len(r) != 1) and not context.get('fy_closing', False):
571             raise osv.except_osv(_('Error'), _('Entries are not of the same account or already reconciled ! '))
572         if not unrec_lines:
573             raise osv.except_osv(_('Error'), _('Entry is already reconciled'))
574         account = self.pool.get('account.account').browse(cr, uid, account_id, context=context)
575         if not context.get('fy_closing', False) and not account.reconcile:
576             raise osv.except_osv(_('Error'), _('The account is not defined to be reconciled !'))
577         if r[0][1] != None:
578             raise osv.except_osv(_('Error'), _('Some entries are already reconciled !'))
579
580         if (not self.pool.get('res.currency').is_zero(cr, uid, account.company_id.currency_id, writeoff)) or \
581            (account.currency_id and (not self.pool.get('res.currency').is_zero(cr, uid, account.currency_id, currency))):
582             if not writeoff_acc_id:
583                 raise osv.except_osv(_('Warning'), _('You have to provide an account for the write off entry !'))
584             if writeoff > 0:
585                 debit = writeoff
586                 credit = 0.0
587                 self_credit = writeoff
588                 self_debit = 0.0
589             else:
590                 debit = 0.0
591                 credit = -writeoff
592                 self_credit = 0.0
593                 self_debit = -writeoff
594
595             # If comment exist in context, take it
596             if 'comment' in context and context['comment']:
597                 libelle=context['comment']
598             else:
599                 libelle='Write-Off'
600
601             writeoff_lines = [
602                 (0, 0, {
603                     'name':libelle,
604                     'debit':self_debit,
605                     'credit':self_credit,
606                     'account_id':account_id,
607                     'date':date,
608                     'partner_id':partner_id,
609                     'currency_id': account.currency_id.id or False,
610                     'amount_currency': account.currency_id.id and -currency or 0.0
611                 }),
612                 (0, 0, {
613                     'name':libelle,
614                     'debit':debit,
615                     'credit':credit,
616                     'account_id':writeoff_acc_id,
617                     'date':date,
618                     'partner_id':partner_id
619                 })
620             ]
621
622             writeoff_move_id = self.pool.get('account.move').create(cr, uid, {
623                 'period_id': writeoff_period_id,
624                 'journal_id': writeoff_journal_id,
625
626                 'state': 'draft',
627                 'line_id': writeoff_lines
628             })
629
630             writeoff_line_ids = self.search(cr, uid, [('move_id', '=', writeoff_move_id), ('account_id', '=', account_id)])
631             ids += writeoff_line_ids
632
633         r_id = self.pool.get('account.move.reconcile').create(cr, uid, {
634             #'name': date,
635             'type': type,
636             'line_id': map(lambda x: (4,x,False), ids),
637             'line_partial_ids': map(lambda x: (3,x,False), ids)
638         })
639         wf_service = netsvc.LocalService("workflow")
640         # the id of the move.reconcile is written in the move.line (self) by the create method above
641         # because of the way the line_id are defined: (4, x, False)
642         for id in ids:
643             wf_service.trg_trigger(uid, 'account.move.line', id, cr)
644         return r_id
645
646     def view_header_get(self, cr, user, view_id, view_type, context):
647         if context.get('account_id', False):
648             cr.execute('select code from account_account where id=%s', (context['account_id'],))
649             res = cr.fetchone()
650             res = _('Entries: ')+ (res[0] or '')
651             return res
652         if (not context.get('journal_id', False)) or (not context.get('period_id', False)):
653             return False
654         cr.execute('select code from account_journal where id=%s', (context['journal_id'],))
655         j = cr.fetchone()[0] or ''
656         cr.execute('select code from account_period where id=%s', (context['period_id'],))
657         p = cr.fetchone()[0] or ''
658         if j or p:
659             return j+(p and (':'+p) or '')
660         return False
661
662     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context={}, toolbar=False, submenu=False):
663         result = super(osv.osv, self).fields_view_get(cr, uid, view_id,view_type,context,toolbar=toolbar, submenu=submenu)
664         if view_type=='tree' and 'journal_id' in context:
665             title = self.view_header_get(cr, uid, view_id, view_type, context)
666             journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id'])
667
668             # if the journal view has a state field, color lines depending on
669             # its value
670             state = ''
671             for field in journal.view_id.columns_id:
672                 if field.field=='state':
673                     state = ' colors="red:state==\'draft\'"'
674
675             #xml = '''<?xml version="1.0"?>\n<tree string="%s" editable="top" refresh="5"%s>\n\t''' % (title, state)
676             xml = '''<?xml version="1.0"?>\n<tree string="%s" editable="top" refresh="5" on_write="_on_create_write"%s>\n\t''' % (title, state)
677             fields = []
678
679             widths = {
680                 'ref': 50,
681                 'statement_id': 50,
682                 'state': 60,
683                 'tax_code_id': 50,
684                 'move_id': 40,
685             }
686             for field in journal.view_id.columns_id:
687                 fields.append(field.field)
688                 attrs = []
689                 if field.field=='debit':
690                     attrs.append('sum="Total debit"')
691                 elif field.field=='credit':
692                     attrs.append('sum="Total credit"')
693                 elif field.field=='account_tax_id':
694                     attrs.append('domain="[(\'parent_id\',\'=\',False)]"')
695                 elif field.field=='account_id' and journal.id:
696                     attrs.append('domain="[(\'journal_id\', \'=\', '+str(journal.id)+'),(\'type\',\'&lt;&gt;\',\'view\'), (\'type\',\'&lt;&gt;\',\'closed\')]" on_change="onchange_account_id(account_id, partner_id)"')
697                 elif field.field == 'partner_id':
698                     attrs.append('on_change="onchange_partner_id(move_id,partner_id,account_id,debit,credit,date,((\'journal_id\' in context) and context[\'journal_id\']) or {})"')
699                 if field.readonly:
700                     attrs.append('readonly="1"')
701                 if field.required:
702                     attrs.append('required="1"')
703                 else:
704                     attrs.append('required="0"')
705                 if field.field in ('amount_currency','currency_id'):
706                     attrs.append('on_change="onchange_currency(account_id,amount_currency,currency_id,date,((\'journal_id\' in context) and context[\'journal_id\']) or {})"')
707
708                 if field.field in widths:
709                     attrs.append('width="'+str(widths[field.field])+'"')
710                 xml += '''<field name="%s" %s/>\n''' % (field.field,' '.join(attrs))
711
712             xml += '''</tree>'''
713             result['arch'] = xml
714             result['fields'] = self.fields_get(cr, uid, fields, context)
715         return result
716
717     def unlink(self, cr, uid, ids, context={}, check=True):
718         self._update_check(cr, uid, ids, context)
719         result = False
720         for line in self.browse(cr, uid, ids, context):
721             context['journal_id']=line.journal_id.id
722             context['period_id']=line.period_id.id
723             result = super(account_move_line, self).unlink(cr, uid, [line.id], context=context)
724             if check:
725                 self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context=context)
726         return result
727
728     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
729         if not context:
730             context={}
731         if vals.get('account_tax_id', False):
732             raise osv.except_osv(_('Unable to change tax !'), _('You can not change the tax, you should remove and recreate lines !'))
733
734         account_obj = self.pool.get('account.account')
735         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
736             raise osv.except_osv(_('Bad account!'), _('You can not use an inactive account!'))
737         if update_check:
738             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):
739                 self._update_check(cr, uid, ids, context)
740
741         todo_date = None
742         if vals.get('date', False):
743             todo_date = vals['date']
744             del vals['date']
745         result = super(account_move_line, self).write(cr, uid, ids, vals, context)
746
747         if check:
748             done = []
749             for line in self.browse(cr, uid, ids):
750                 if line.move_id.id not in done:
751                     done.append(line.move_id.id)
752                     self.pool.get('account.move').validate(cr, uid, [line.move_id.id], context)
753                     if todo_date:
754                         self.pool.get('account.move').write(cr, uid, [line.move_id.id], {'date': todo_date}, context=context)
755         return result
756
757     def _update_journal_check(self, cr, uid, journal_id, period_id, context={}):
758         cr.execute('select state from account_journal_period where journal_id=%s and period_id=%s', (journal_id, period_id))
759         result = cr.fetchall()
760         for (state,) in result:
761             if state=='done':
762                 raise osv.except_osv(_('Error !'), _('You can not add/modify entries in a closed journal.'))
763         if not result:
764             journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context)
765             period = self.pool.get('account.period').browse(cr, uid, period_id, context)
766             self.pool.get('account.journal.period').create(cr, uid, {
767                 'name': (journal.code or journal.name)+':'+(period.name or ''),
768                 'journal_id': journal.id,
769                 'period_id': period.id
770             })
771         return True
772
773     def _update_check(self, cr, uid, ids, context={}):
774         done = {}
775         for line in self.browse(cr, uid, ids, context):
776             if line.move_id.state<>'draft':
777                 raise osv.except_osv(_('Error !'), _('You can not do this modification on a confirmed entry ! Please note that you can just change some non important fields !'))
778             if line.reconcile_id:
779                 raise osv.except_osv(_('Error !'), _('You can not do this modification on a reconciled entry ! Please note that you can just change some non important fields !'))
780             t = (line.journal_id.id, line.period_id.id)
781             if t not in done:
782                 self._update_journal_check(cr, uid, line.journal_id.id, line.period_id.id, context)
783                 done[t] = True
784         return True
785
786     def create(self, cr, uid, vals, context=None, check=True):
787         if not context:
788             context={}
789         account_obj = self.pool.get('account.account')
790         tax_obj=self.pool.get('account.tax')
791         if ('account_id' in vals) and not account_obj.read(cr, uid, vals['account_id'], ['active'])['active']:
792             raise osv.except_osv(_('Bad account!'), _('You can not use an inactive account!'))
793         if 'journal_id' in vals and 'journal_id' not in context:
794             context['journal_id'] = vals['journal_id']
795         if 'period_id' in vals and 'period_id' not in context:
796             context['period_id'] = vals['period_id']
797         if ('journal_id' not in context) and ('move_id' in vals) and vals['move_id']:
798             m = self.pool.get('account.move').browse(cr, uid, vals['move_id'])
799             context['journal_id'] = m.journal_id.id
800             context['period_id'] = m.period_id.id
801
802         self._update_journal_check(cr, uid, context['journal_id'], context['period_id'], context)
803         company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
804
805         move_id = vals.get('move_id', False)
806         journal = self.pool.get('account.journal').browse(cr, uid, context['journal_id'])
807         is_new_move = False
808         if not move_id:
809             if journal.centralisation:
810                 # use the first move ever created for this journal and period
811                 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']))
812                 res = cr.fetchone()
813                 if res:
814                     if res[1] != 'draft':
815                         raise osv.except_osv(_('UserError'),
816                                 _('The account move (%s) for centralisation ' \
817                                         'has been confirmed!') % res[2])
818                     vals['move_id'] = res[0]
819
820             if not vals.get('move_id', False):
821                 if journal.sequence_id:
822                     #name = self.pool.get('ir.sequence').get_id(cr, uid, journal.sequence_id.id)
823                     v = {
824                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
825                         'period_id': context['period_id'],
826                         'journal_id': context['journal_id']
827                     }
828                     move_id = self.pool.get('account.move').create(cr, uid, v, context)
829                     vals['move_id'] = move_id
830                 else:
831                     raise osv.except_osv(_('No piece number !'), _('Can not create an automatic sequence for this piece !\n\nPut a sequence in the journal definition for automatic numbering or create a sequence manually for this piece.'))
832             is_new_move = True
833
834         ok = not (journal.type_control_ids or journal.account_control_ids)
835         if ('account_id' in vals):
836             account = account_obj.browse(cr, uid, vals['account_id'])
837             if journal.type_control_ids:
838                 type = account.user_type
839                 for t in journal.type_control_ids:
840                     if type.code == t.code:
841                         ok = True
842                         break
843             if journal.account_control_ids and not ok:
844                 for a in journal.account_control_ids:
845                     if a.id==vals['account_id']:
846                         ok = True
847                         break
848             if (account.currency_id) and 'amount_currency' not in vals and account.currency_id.id <> company_currency:
849                 vals['currency_id'] = account.currency_id.id
850                 cur_obj = self.pool.get('res.currency')
851                 ctx = {}
852                 if 'date' in vals:
853                     ctx['date'] = vals['date']
854                 vals['amount_currency'] = cur_obj.compute(cr, uid, account.company_id.currency_id.id,
855                     account.currency_id.id, vals.get('debit', 0.0)-vals.get('credit', 0.0),
856                     context=ctx)
857         if not ok:
858             raise osv.except_osv(_('Bad account !'), _('You can not use this general account in this journal !'))
859
860         if 'analytic_account_id' in vals and vals['analytic_account_id']:
861             if journal.analytic_journal_id:
862                 vals['analytic_lines'] = [(0,0, {
863                         'name': vals['name'],
864                         'date': vals.get('date', time.strftime('%Y-%m-%d')),
865                         'account_id': vals['analytic_account_id'],
866                         'unit_amount':'quantity' in vals and vals['quantity'] or 1.0,
867                         'amount': vals['debit'] or vals['credit'],
868                         'general_account_id': vals['account_id'],
869                         'journal_id': journal.analytic_journal_id.id,
870                         'ref': vals['ref'],
871                     })]
872             #else:
873             #    raise osv.except_osv(_('No analytic journal !'), _('Please set an analytic journal on this financial journal !'))
874
875         #if not 'currency_id' in vals:
876         #    vals['currency_id'] = account.company_id.currency_id.id
877
878         result = super(osv.osv, self).create(cr, uid, vals, context)
879         # CREATE Taxes
880         if 'account_tax_id' in vals and vals['account_tax_id']:
881             tax_id=tax_obj.browse(cr,uid,vals['account_tax_id'])
882             total = vals['debit'] - vals['credit']
883             if journal.refund_journal:
884                 base_code = 'ref_base_code_id'
885                 tax_code = 'ref_tax_code_id'
886                 account_id = 'account_paid_id'
887                 base_sign = 'ref_base_sign'
888                 tax_sign = 'ref_tax_sign'
889             else:
890                 base_code = 'base_code_id'
891                 tax_code = 'tax_code_id'
892                 account_id = 'account_collected_id'
893                 base_sign = 'base_sign'
894                 tax_sign = 'tax_sign'
895
896             tmp_cnt = 0
897             for tax in tax_obj.compute(cr,uid,[tax_id],total,1.00):
898                 #create the base movement
899                 if tmp_cnt == 0:
900                     if tax[base_code]:
901                         tmp_cnt += 1
902                         self.write(cr, uid,[result], {
903                             'tax_code_id': tax[base_code],
904                             'tax_amount': tax[base_sign] * abs(total)
905                         })
906                 else:
907                     data = {
908                         'move_id': vals['move_id'],
909                         'journal_id': vals['journal_id'],
910                         'period_id': vals['period_id'],
911                         'name': vals['name']+' '+tax['name'],
912                         'date': vals['date'],
913                         'partner_id': vals.get('partner_id',False),
914                         'ref': vals.get('ref',False),
915                         'account_tax_id': False,
916                         'tax_code_id': tax[base_code],
917                         'tax_amount': tax[base_sign] * abs(total),
918                         'account_id': vals['account_id'],
919                         'credit': 0.0,
920                         'debit': 0.0,
921                     }
922                     if data['tax_code_id']:
923                         self.create(cr, uid, data, context)
924
925                 #create the VAT movement
926                 data = {
927                     'move_id': vals['move_id'],
928                     'journal_id': vals['journal_id'],
929                     'period_id': vals['period_id'],
930                     'name': vals['name']+' '+tax['name'],
931                     'date': vals['date'],
932                     'partner_id': vals.get('partner_id',False),
933                     'ref': vals.get('ref',False),
934                     'account_tax_id': False,
935                     'tax_code_id': tax[tax_code],
936                     'tax_amount': tax[tax_sign] * abs(tax['amount']),
937                     'account_id': tax[account_id] or vals['account_id'],
938                     'credit': tax['amount']<0 and -tax['amount'] or 0.0,
939                     'debit': tax['amount']>0 and tax['amount'] or 0.0,
940                 }
941                 if data['tax_code_id']:
942                     self.create(cr, uid, data, context)
943             del vals['account_tax_id']
944
945         # No needed, related to the job
946         #if not is_new_move and 'date' in vals:
947         #    if context and ('__last_update' in context):
948         #        del context['__last_update']
949         #    self.pool.get('account.move').write(cr, uid, [move_id], {'date':vals['date']}, context=context)
950         if check and not context.get('no_store_function'):
951             tmp = self.pool.get('account.move').validate(cr, uid, [vals['move_id']], context)
952             if journal.entry_posted and tmp:
953                 self.pool.get('account.move').button_validate(cr,uid, [vals['move_id']],context)
954         return result
955 account_move_line()
956
957
958 class account_bank_statement_reconcile(osv.osv):
959     _inherit = "account.bank.statement.reconcile"
960     _columns = {
961         'line_ids': fields.many2many('account.move.line', 'account_bank_statement_line_rel', 'statement_id', 'line_id', 'Entries'),
962     }
963 account_bank_statement_reconcile()
964
965 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
966