[IMP] remove subtype canceled & stage change
[odoo/odoo.git] / addons / analytic / analytic.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23
24 from osv import fields, osv
25 from tools.translate import _
26 import decimal_precision as dp
27
28 class account_analytic_account(osv.osv):
29     _name = 'account.analytic.account'
30     _inherit = ['mail.thread']
31     _description = 'Analytic Account'
32
33     def _compute_level_tree(self, cr, uid, ids, child_ids, res, field_names, context=None):
34         currency_obj = self.pool.get('res.currency')
35         recres = {}
36         def recursive_computation(account):
37             result2 = res[account.id].copy()
38             for son in account.child_ids:
39                 result = recursive_computation(son)
40                 for field in field_names:
41                     if (account.currency_id.id != son.currency_id.id) and (field!='quantity'):
42                         result[field] = currency_obj.compute(cr, uid, son.currency_id.id, account.currency_id.id, result[field], context=context)
43                     result2[field] += result[field]
44             return result2
45         for account in self.browse(cr, uid, ids, context=context):
46             if account.id not in child_ids:
47                 continue
48             recres[account.id] = recursive_computation(account)
49         return recres
50
51     def _debit_credit_bal_qtty(self, cr, uid, ids, fields, arg, context=None):
52         res = {}
53         if context is None:
54             context = {}
55         child_ids = tuple(self.search(cr, uid, [('parent_id', 'child_of', ids)]))
56         for i in child_ids:
57             res[i] =  {}
58             for n in fields:
59                 res[i][n] = 0.0
60
61         if not child_ids:
62             return res
63
64         where_date = ''
65         where_clause_args = [tuple(child_ids)]
66         if context.get('from_date', False):
67             where_date += " AND l.date >= %s"
68             where_clause_args  += [context['from_date']]
69         if context.get('to_date', False):
70             where_date += " AND l.date <= %s"
71             where_clause_args += [context['to_date']]
72         cr.execute("""
73               SELECT a.id,
74                      sum(
75                          CASE WHEN l.amount > 0
76                          THEN l.amount
77                          ELSE 0.0
78                          END
79                           ) as debit,
80                      sum(
81                          CASE WHEN l.amount < 0
82                          THEN -l.amount
83                          ELSE 0.0
84                          END
85                           ) as credit,
86                      COALESCE(SUM(l.amount),0) AS balance,
87                      COALESCE(SUM(l.unit_amount),0) AS quantity
88               FROM account_analytic_account a
89                   LEFT JOIN account_analytic_line l ON (a.id = l.account_id)
90               WHERE a.id IN %s
91               """ + where_date + """
92               GROUP BY a.id""", where_clause_args)
93         for row in cr.dictfetchall():
94             res[row['id']] = {}
95             for field in fields:
96                 res[row['id']][field] = row[field]
97         return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
98
99     def name_get(self, cr, uid, ids, context=None):
100         res = []
101         if not ids:
102             return res
103         if isinstance(ids, (int, long)):
104             ids = [ids]
105         for id in ids:
106             elmt = self.browse(cr, uid, id, context=context)
107             res.append((id, self._get_one_full_name(elmt)))
108         return res
109
110     def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
111         if context == None:
112             context = {}
113         res = {}
114         for elmt in self.browse(cr, uid, ids, context=context):
115             res[elmt.id] = self._get_one_full_name(elmt)
116         return res
117
118     def _get_one_full_name(self, elmt, level=6):
119         if level<=0:
120             return '...'
121         if elmt.parent_id:
122             parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/"
123         else:
124             parent_path = ''
125         return parent_path + elmt.name
126
127     def _child_compute(self, cr, uid, ids, name, arg, context=None):
128         result = {}
129         if context is None:
130             context = {}
131
132         for account in self.browse(cr, uid, ids, context=context):
133             result[account.id] = map(lambda x: x.id, [child for child in account.child_ids if child.state != 'template'])
134
135         return result
136
137     def _get_analytic_account(self, cr, uid, ids, context=None):
138         company_obj = self.pool.get('res.company')
139         analytic_obj = self.pool.get('account.analytic.account')
140         accounts = []
141         for company in company_obj.browse(cr, uid, ids, context=context):
142             accounts += analytic_obj.search(cr, uid, [('company_id', '=', company.id)])
143         return accounts
144
145     def _set_company_currency(self, cr, uid, ids, name, value, arg, context=None):
146         if isinstance(ids, (int, long)):
147             ids=[ids]
148         for account in self.browse(cr, uid, ids, context=context):
149             if account.company_id:
150                 if account.company_id.currency_id.id != value:
151                     raise osv.except_osv(_('Error!'), _("If you set a company, the currency selected has to be the same as it's currency. \nYou can remove the company belonging, and thus change the currency, only on analytic account of type 'view'. This can be really usefull for consolidation purposes of several companies charts with different currencies, for example."))
152         if value:
153             return cr.execute("""update account_analytic_account set currency_id=%s where id=%s""", (value, account.id, ))
154
155     def _currency(self, cr, uid, ids, field_name, arg, context=None):
156         result = {}
157         for rec in self.browse(cr, uid, ids, context=context):
158             if rec.company_id:
159                 result[rec.id] = rec.company_id.currency_id.id
160             else:
161                 result[rec.id] = rec.currency_id.id
162         return result
163
164     _columns = {
165         'name': fields.char('Account/Contract Name', size=128, required=True),
166         'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
167         'code': fields.char('Reference', size=24, select=True),
168         'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
169                                  help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\
170                                   "The type 'Analytic account' stands for usual accounts that you only want to use in accounting.\n"\
171                                   "If you select Contract or Project, it offers you the possibility to manage the validity and the invoicing options for this account.\n"\
172                                   "The special type 'Template of Contract' allows you to define a template with default data that you can reuse easily."),
173         'template_id': fields.many2one('account.analytic.account', 'Template of Contract'),
174         'description': fields.text('Description'),
175         'parent_id': fields.many2one('account.analytic.account', 'Parent Analytic Account', select=2),
176         'child_ids': fields.one2many('account.analytic.account', 'parent_id', 'Child Accounts'),
177         'child_complete_ids': fields.function(_child_compute, relation='account.analytic.account', string="Account Hierarchy", type='many2many'),
178         'line_ids': fields.one2many('account.analytic.line', 'account_id', 'Analytic Entries'),
179         'balance': fields.function(_debit_credit_bal_qtty, type='float', string='Balance', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
180         'debit': fields.function(_debit_credit_bal_qtty, type='float', string='Debit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
181         'credit': fields.function(_debit_credit_bal_qtty, type='float', string='Credit', multi='debit_credit_bal_qtty', digits_compute=dp.get_precision('Account')),
182         'quantity': fields.function(_debit_credit_bal_qtty, type='float', string='Quantity', multi='debit_credit_bal_qtty'),
183         'quantity_max': fields.float('Prepaid Service Units', help='Sets the higher limit of time to work on the contract, based on the timesheet. (for instance, number of hours in a limited support contract.)'),
184         'partner_id': fields.many2one('res.partner', 'Customer'),
185         'user_id': fields.many2one('res.users', 'Project Manager'),
186         'manager_id': fields.many2one('res.users', 'Account Manager'),
187         'date_start': fields.date('Start Date'),
188         'date': fields.date('Date End', select=True),
189         'company_id': fields.many2one('res.company', 'Company', required=False), #not required because we want to allow different companies to use the same chart of account, except for leaf accounts.
190         'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','To Renew'),('close','Closed')], 'Status', required=True,),
191         'currency_id': fields.function(_currency, fnct_inv=_set_company_currency, #the currency_id field is readonly except if it's a view account and if there is no company
192             store = {
193                 'res.company': (_get_analytic_account, ['currency_id'], 10),
194             }, string='Currency', type='many2one', relation='res.currency'),
195     }
196
197     def on_change_template(self, cr, uid, ids, template_id, context=None):
198         if not template_id:
199             return {}
200         res = {'value':{}}
201         template = self.browse(cr, uid, template_id, context=context)
202         res['value']['date_start'] = template.date_start
203         res['value']['date'] = template.date
204         res['value']['quantity_max'] = template.quantity_max
205         res['value']['description'] = template.description
206         return res
207
208     def on_change_partner_id(self, cr, uid, ids,partner_id, name, context={}):
209         res={}
210         if partner_id:
211             partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
212             if partner.user_id:
213                 res['manager_id'] = partner.user_id.id
214             if not name:
215                 res['name'] = _('Contract: ') + partner.name
216         return {'value': res}
217
218     def _default_company(self, cr, uid, context=None):
219         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
220         if user.company_id:
221             return user.company_id.id
222         return self.pool.get('res.company').search(cr, uid, [('parent_id', '=', False)])[0]
223
224     def _get_default_currency(self, cr, uid, context=None):
225         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
226         return user.company_id.currency_id.id
227
228     _defaults = {
229         'type': 'normal',
230         'company_id': _default_company,
231         'code' : lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'account.analytic.account'),
232         'state': 'open',
233         'user_id': lambda self, cr, uid, ctx: uid,
234         'partner_id': lambda self, cr, uid, ctx: ctx.get('partner_id', False),
235         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
236         'currency_id': _get_default_currency,
237     }
238
239     def check_recursion(self, cr, uid, ids, context=None, parent=None):
240         return super(account_analytic_account, self)._check_recursion(cr, uid, ids, context=context, parent=parent)
241
242     _order = 'name asc'
243     _constraints = [
244         (check_recursion, 'Error! You cannot create recursive analytic accounts.', ['parent_id']),
245     ]
246
247     def copy(self, cr, uid, id, default=None, context=None):
248         if not default:
249             default = {}
250         analytic = self.browse(cr, uid, id, context=context)
251         default.update(
252             code=False,
253             line_ids=[],
254             name=_("%s (copy)") % (analytic['name']))
255         return super(account_analytic_account, self).copy(cr, uid, id, default, context=context)
256
257     def on_change_company(self, cr, uid, id, company_id):
258         if not company_id:
259             return {}
260         currency = self.pool.get('res.company').read(cr, uid, [company_id], ['currency_id'])[0]['currency_id']
261         return {'value': {'currency_id': currency}}
262
263     def on_change_parent(self, cr, uid, id, parent_id):
264         if not parent_id:
265             return {}
266         parent = self.read(cr, uid, [parent_id], ['partner_id','code'])[0]
267         if parent['partner_id']:
268             partner = parent['partner_id'][0]
269         else:
270             partner = False
271         res = {'value': {}}
272         if partner:
273             res['value']['partner_id'] = partner
274         return res
275
276     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
277         if not args:
278             args=[]
279         if context is None:
280             context={}
281         if context.get('current_model') == 'project.project':
282             project_obj = self.pool.get("account.analytic.account")
283             project_ids = project_obj.search(cr, uid, args)
284             return self.name_get(cr, uid, project_ids, context=context)
285         if name:
286             account_ids = self.search(cr, uid, [('code', '=', name)] + args, limit=limit, context=context)
287             if not account_ids:
288                 names=map(lambda i : i.strip(),name.split('/'))
289                 for i in range(len(names)):
290                     dom=[('name', operator, names[i])]
291                     if i>0:
292                         dom+=[('id','child_of',account_ids)]
293                     account_ids = self.search(cr, uid, dom, limit=limit, context=context)
294                 newacc = account_ids
295                 while newacc:
296                     newacc = self.search(cr, uid, [('parent_id', 'in', newacc)], limit=limit, context=context)
297                     account_ids += newacc
298                 if args:
299                     account_ids = self.search(cr, uid, [('id', 'in', account_ids)] + args, limit=limit, context=context)
300         else:
301             account_ids = self.search(cr, uid, args, limit=limit, context=context)
302         return self.name_get(cr, uid, account_ids, context=context)
303
304     def create(self, cr, uid, vals, context=None):
305         contract =  super(account_analytic_account, self).create(cr, uid, vals, context=context)
306         if contract:
307             self.create_send_note(cr, uid, [contract], context=context)
308         return contract
309
310     def create_send_note(self, cr, uid, ids, context=None):
311         for obj in self.browse(cr, uid, ids, context=context):
312             message = _("Contract <b>created</b>.")
313             if obj.partner_id:
314                 message = _("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name,)
315             self.message_post(cr, uid, [obj.id], body=message,
316                 context=context)
317
318 account_analytic_account()
319
320
321 class account_analytic_line(osv.osv):
322     _name = 'account.analytic.line'
323     _description = 'Analytic Line'
324
325     _columns = {
326         'name': fields.char('Description', size=256, required=True),
327         'date': fields.date('Date', required=True, select=True),
328         'amount': fields.float('Amount', required=True, help='Calculated by multiplying the quantity and the price given in the Product\'s cost price. Always expressed in the company main currency.', digits_compute=dp.get_precision('Account')),
329         'unit_amount': fields.float('Quantity', help='Specifies the amount of quantity to count.'),
330         'account_id': fields.many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', select=True, domain=[('type','<>','view')]),
331         'user_id': fields.many2one('res.users', 'User'),
332         'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
333
334     }
335
336     def _get_default_date(self, cr, uid, context=None):
337         return fields.date.context_today(self, cr, uid, context=context)
338
339     def __get_default_date(self, cr, uid, context=None):
340         return self._get_default_date(cr, uid, context=context)
341
342     _defaults = {
343         'date': __get_default_date,
344         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
345         'amount': 0.00
346     }
347
348     _order = 'date desc'
349
350     def _check_no_view(self, cr, uid, ids, context=None):
351         analytic_lines = self.browse(cr, uid, ids, context=context)
352         for line in analytic_lines:
353             if line.account_id.type == 'view':
354                 return False
355         return True
356
357     _constraints = [
358         (_check_no_view, 'You cannot create analytic line on view account.', ['account_id']),
359     ]
360
361 account_analytic_line()
362
363 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: