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