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