improve
[odoo/odoo.git] / addons / project / project.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #
6 # $Id$
7 #
8 # WARNING: This program as such is intended to be used by professional
9 # programmers who take the whole responsability of assessing all potential
10 # consequences resulting from its eventual inadequacies and bugs
11 # End users who are looking for a ready-to-use solution with commercial
12 # garantees and support are strongly adviced to contract a Free Software
13 # Service Company
14 #
15 # This program is Free Software; you can redistribute it and/or
16 # modify it under the terms of the GNU General Public License
17 # as published by the Free Software Foundation; either version 2
18 # of the License, or (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the Free Software
27 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
28 #
29 ##############################################################################
30
31 from lxml import etree
32 from mx import DateTime
33 from mx.DateTime import now
34 import time
35
36 from osv import fields, osv
37
38 class project(osv.osv):
39     _name = "project.project"
40     _description = "Project"
41
42     def _calc_effective(self, cr, uid, ids, name, args, context):
43         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
44         res_sum = {}
45         if ids2:
46             cr.execute('SELECT t.project_id, COALESCE(SUM(w.hours),0) \
47                     FROM project_task t \
48                         LEFT JOIN project_task_work w \
49                             ON (w.task_id = t.id) \
50                     WHERE t.project_id in (' + ','.join([str(x) for x in ids2]) + ') \
51                         AND active \
52                     GROUP BY project_id')
53             for project_id, sum in cr.fetchall():
54                 res_sum[project_id] = sum
55         res={}
56         for id in ids:
57             ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
58             res.setdefault(id, 0.0)
59             for idx in ids3:
60                 res[id] += res_sum.get(idx, 0.0)
61         return res
62
63     def _calc_planned(self, cr, uid, ids, name, args, context):
64         ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
65         res_sum = {}
66         if ids2:
67             cr.execute('SELECT project_id, COALESCE(SUM(total_hours),0) \
68                     FROM project_task \
69                     WHERE project_id IN (' + ','.join([str(x) for x in ids2]) + ') \
70                         AND active \
71                     GROUP BY project_id')
72             for project_id, sum in cr.fetchall():
73                 res_sum[project_id] = sum
74         res = {}
75         for id in ids:
76             ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
77             res.setdefault(id, 0.0)
78             for idx in ids3:
79                 res[id] += res_sum.get(idx, 0.0)
80         return res
81
82     def check_recursion(self, cursor, user, ids, parent=None):
83         return super(project, self).check_recursion(cursor, user, ids,
84                 parent=parent)
85
86     def onchange_partner_id(self, cr, uid, ids, part):
87         if not part:
88             return {'value':{'contact_id': False, 'pricelist_id': False}}
89         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
90
91         pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
92         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
93
94     def _progress_rate(self, cr, uid, ids, name, arg, context=None):
95         res = {}.fromkeys(ids, 0.0)
96         if not ids:
97             return res
98         cr.execute('''SELECT
99                 project_id, sum(progress*total_hours), sum(total_hours) 
100             FROM
101                 project_task 
102             WHERE
103                 project_id in ('''+','.join(map(str,ids))+''') AND
104                 state<>'cancelled'
105             GROUP BY
106                 project_id''')
107         for id,prog,tot in cr.fetchall():
108             if tot:
109                 res[id] = prog / tot
110         return res
111
112     _columns = {
113         'name': fields.char("Project Name", size=128, required=True),
114         'active': fields.boolean('Active'),
115         'category_id': fields.many2one('account.analytic.account','Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It ables to connect projects with budgets, plannings, costs and revenues analysis, timesheet on projects, etc."),
116         'priority': fields.integer('Sequence'),
117         'manager': fields.many2one('res.users', 'Project Manager'),
118         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team."),
119         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members'),
120         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
121         'parent_id': fields.many2one('project.project', 'Parent Project'),
122         'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
123         'planned_hours': fields.function(_calc_planned, method=True, string='Planned hours'),
124         'effective_hours': fields.function(_calc_effective, method=True, string='Hours spent'),
125         'progress_rate': fields.function(_progress_rate, method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
126         'date_start': fields.date('Starting Date'),
127         'date_end': fields.date('Expected End'),
128         'partner_id': fields.many2one('res.partner', 'Partner'),
129         'contact_id': fields.many2one('res.partner.address', 'Contact'),
130         'warn_customer': fields.boolean('Warn Partner'),
131         'warn_header': fields.text('Mail header'),
132         'warn_footer': fields.text('Mail footer'),
133         'notes': fields.text('Notes'),
134         'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
135         'state': fields.selection([('template', 'Template'), ('open', 'Open'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True),
136      }
137
138     _defaults = {
139         'active': lambda *a: True,
140         'manager': lambda object,cr,uid,context: uid,
141         'priority': lambda *a: 1,
142         'date_start': lambda *a: time.strftime('%Y-%m-%d'),
143         'state': lambda *a: 'open'
144     }
145
146     _order = "priority"
147     _constraints = [
148         (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
149     ]
150
151     # toggle activity of projects, their sub projects and their tasks
152     def set_template(self, cr, uid, ids, context={}):
153         res = self.setActive(cr, uid, ids, value=False, context=context) 
154         return res
155
156     def set_done(self, cr, uid, ids, context={}):
157         self.write(cr, uid, ids, {'state':'done'}, context=context)
158         return True
159
160     def set_cancel(self, cr, uid, ids, context={}):
161         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
162         return True
163
164     def set_pending(self, cr, uid, ids, context={}):
165         self.write(cr, uid, ids, {'state':'pending'}, context=context)
166         return True
167
168     def set_open(self, cr, uid, ids, context={}):
169         self.write(cr, uid, ids, {'state':'open'}, context=context)
170         return True
171
172     def reset_project(self, cr, uid, ids, context={}):
173         res = self.setActive(cr, uid, ids,value=True, context=context)
174         return res
175
176     def copy(self, cr, uid, id, default={},context={}):
177         default = default or {}
178         default['tasks'] = []
179         default['child_id'] = []
180         return super(project, self).copy(cr, uid, id, default, context)
181
182     def duplicate_template(self, cr, uid, ids,context={}):
183         for proj in self.browse(cr, uid, ids):
184             parent_id=context.get('parent_id',False)
185             new_id=self.pool.get('project.project').copy(cr, uid, proj.id,default={'name':proj.name+_(' (copy)'),'state':'open','parent_id':parent_id})
186             cr.execute('select id from project_task where project_id=%d', (proj.id,))
187             res = cr.fetchall()
188             for (tasks_id,) in res:
189                 self.pool.get('project.task').copy(cr, uid, tasks_id,default={'project_id':new_id,'active':True}, context=context)
190             cr.execute('select id from project_project where parent_id=%d', (proj.id,))
191             res = cr.fetchall()
192             project_ids = [x[0] for x in res]
193             for child in project_ids:
194                 self.duplicate_template(cr, uid, [child],context={'parent_id':new_id}) 
195
196         # TODO : Improve this to open the new project (using a wizard)
197
198         cr.commit()
199         raise osv.except_osv(_('Operation Done'), _('A new project has been created !\nWe suggest you to close this one and work on this new project.'))
200
201     # set active value for a project, its sub projects and its tasks
202     def setActive(self, cr, uid, ids, value=True, context={}):   
203         for proj in self.browse(cr, uid, ids, context):            
204             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
205             cr.execute('select id from project_task where project_id=%d', (proj.id,))
206             tasks_id = [x[0] for x in cr.fetchall()]
207             if tasks_id:
208                 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
209             cr.execute('select id from project_project where parent_id=%d', (proj.id,))            
210             project_ids = [x[0] for x in cr.fetchall()]            
211             for child in project_ids:
212                 self.setActive(cr, uid, [child], value, context)                
213         return True
214 project()
215
216 class project_task_type(osv.osv):
217     _name = 'project.task.type'
218     _description = 'Project task type'
219     _columns = {
220         'name': fields.char('Type', required=True, size=64, translate=True),
221         'description': fields.text('Description'),
222     }
223 project_task_type()
224
225 class task(osv.osv):
226     _name = "project.task"
227     _description = "Task"
228     _date_name = "date_start"
229     def _str_get(self, task, level=0, border='***', context={}):
230         return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
231             border[0]+' '+(task.name or '')+'\n'+ \
232             (task.description or '')+'\n\n'
233
234     def _history_get(self, cr, uid, ids, name, args, context={}):
235         result = {}
236         for task in self.browse(cr, uid, ids, context=context):
237             result[task.id] = self._str_get(task, border='===')
238             t2 = task.parent_id
239             level = 0
240             while t2:
241                 level -= 1
242                 result[task.id] = self._str_get(t2, level) + result[task.id]
243                 t2 = t2.parent_id
244             t3 = map(lambda x: (x,1), task.child_ids)
245             while t3:
246                 t2 = t3.pop(0)
247                 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
248                 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
249         return result
250
251 # Compute: effective_hours, total_hours, progress
252     def _hours_get(self, cr, uid, ids, field_names, args, context):
253         task_set = ','.join(map(str, ids))
254         cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
255         hours = dict(cr.fetchall())
256         res = {}
257         for task in self.browse(cr, uid, ids, context=context):
258             res[task.id] = {}
259             res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
260             res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
261             if (task.remaining_hours + hours.get(task.id, 0.0)):
262                 res[task.id]['progress'] = min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100)
263             else:
264                 res[task.id]['progress'] = 0.0
265             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
266         return res
267
268     def onchange_planned(self, cr, uid, ids, planned, effective):
269         return {'value':{'remaining_hours': planned-effective}}
270
271     #_sql_constraints = [
272     #    ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
273     #]
274
275     _columns = {
276         'active': fields.boolean('Active'),
277         'name': fields.char('Task summary', size=128, required=True),
278         'description': fields.text('Description'),
279         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
280         'sequence': fields.integer('Sequence'),
281         'type': fields.many2one('project.task.type', 'Type'),
282         'state': fields.selection([('draft', 'Draft'),('open', 'Open'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'Status', readonly=True, required=True),
283         'date_start': fields.datetime('Date Opened'),
284         'date_deadline': fields.datetime('Deadline'),
285         'date_close': fields.datetime('Date Closed', readonly=True),
286         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
287         'parent_id': fields.many2one('project.task', 'Parent Task'),
288         'child_ids': fields.one2many('project.task', 'parent_id', 'Delegated Tasks'),
289         'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
290         'notes': fields.text('Notes'),
291
292         'planned_hours': fields.float('Planned Hours', readonly=True, states={'draft':[('readonly',False)]}, required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
293         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
294         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
295         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
296         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
297         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as: Total Time - Estimated Time. It gives the difference of the time estimated by the project manager and the real time to close the task."),
298
299         'user_id': fields.many2one('res.users', 'Assigned to'),
300         'partner_id': fields.many2one('res.partner', 'Partner'),
301         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done', readonly=False, states={'draft':[('readonly',True)]}),
302     }
303     _defaults = {
304         'user_id': lambda obj,cr,uid,context: uid,
305         'state': lambda *a: 'draft',
306         'priority': lambda *a: '2',
307         'progress': lambda *a: 0,
308         'sequence': lambda *a: 10,
309         'active': lambda *a: True,
310         'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
311     }
312     _order = "sequence, priority, date_deadline, id"
313
314     #
315     # Override view according to the company definition
316     #
317     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False):
318         tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode
319         f = self.pool.get('res.company').fields_get(cr, uid, ['project_time_mode'], context)
320         word = dict(f['project_time_mode']['selection'])[tm]
321
322         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar)
323         if tm=='hours':
324             return res
325         eview = etree.fromstring(res['arch'])
326         def _check_rec(eview, tm):
327             if eview.attrib.get('widget',False) == 'float_time':
328                 eview.set('widget','float')
329             for child in eview:
330                 _check_rec(child, tm)
331             return True
332         _check_rec(eview, tm)
333         res['arch'] = etree.tostring(eview)
334         for f in res['fields']:
335             if 'Hours' in res['fields'][f]['string']:
336                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',word)
337         return res
338
339     def do_close(self, cr, uid, ids, *args):
340         request = self.pool.get('res.request')
341         tasks = self.browse(cr, uid, ids)
342         for task in tasks:
343             project = task.project_id
344             if project:
345                 if project.warn_manager and project.manager and (project.manager.id != uid):
346                     request.create(cr, uid, {
347                         'name': "Task '%s' closed" % task.name,
348                         'state': 'waiting',
349                         'act_from': uid,
350                         'act_to': project.manager.id,
351                         'ref_partner_id': task.partner_id.id,
352                         'ref_doc1': 'project.task,%d'% (task.id,),
353                         'ref_doc2': 'project.project,%d'% (project.id,),
354                     })
355             self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
356             if task.parent_id and task.parent_id.state in ('pending','draft'):
357                 self.do_reopen(cr, uid, [task.parent_id.id])
358         return True
359
360     def do_reopen(self, cr, uid, ids, *args):
361         request = self.pool.get('res.request')
362         tasks = self.browse(cr, uid, ids)
363         for task in tasks:
364             project = task.project_id
365             if project and project.warn_manager and project.manager.id and (project.manager.id != uid):
366                 request.create(cr, uid, {
367                     'name': "Task '%s' reopened" % task.name,
368                     'state': 'waiting',
369                     'act_from': uid,
370                     'act_to': project.manager.id,
371                     'ref_partner_id': task.partner_id.id,
372                     'ref_doc1': 'project.task,%d' % task.id,
373                     'ref_doc2': 'project.project,%d' % project.id,
374                 })
375
376             self.write(cr, uid, [task.id], {'state': 'open'})
377         return True
378
379     def do_cancel(self, cr, uid, ids, *args):
380         request = self.pool.get('res.request')
381         tasks = self.browse(cr, uid, ids)
382         for task in tasks:
383             project = task.project_id
384             if project.warn_manager and project.manager and (project.manager.id != uid):
385                 request.create(cr, uid, {
386                     'name': "Task '%s' cancelled" % task.name,
387                     'state': 'waiting',
388                     'act_from': uid,
389                     'act_to': project.manager.id,
390                     'ref_partner_id': task.partner_id.id,
391                     'ref_doc1': 'project.task,%d' % task.id,
392                     'ref_doc2': 'project.project,%d' % project.id,
393                 })
394             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
395         return True
396
397     def do_open(self, cr, uid, ids, *args):
398         tasks= self.browse(cr,uid,ids)
399         for t in tasks:
400             self.write(cr, uid, [t.id], {'state': 'open'})
401         return True
402
403     def do_draft(self, cr, uid, ids, *args):
404         self.write(cr, uid, ids, {'state': 'draft'})
405         return True
406
407
408     def do_pending(self, cr, uid, ids, *args):
409         self.write(cr, uid, ids, {'state': 'pending'})
410         return True
411
412
413 task()
414
415 class project_work(osv.osv):
416     _name = "project.task.work"
417     _description = "Task Work"
418     _columns = {
419         'name': fields.char('Work summary', size=128),
420         'date': fields.datetime('Date'),
421         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
422         'hours': fields.float('Time Spent'),
423         'user_id': fields.many2one('res.users', 'Done by', required=True),
424     }
425     _defaults = {
426         'user_id': lambda obj,cr,uid,context: uid,
427         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
428     }
429     _order = "date desc"
430     def create(self, cr, uid, vals, *args, **kwargs):
431         if 'task_id' in vals:
432             cr.execute('update project_task set remaining_hours=remaining_hours+%.2f where id=%d', (-vals.get('hours',0.0), vals['task_id']))
433         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
434
435     def write(self, cr, uid, ids,vals,context={}):
436         for work in self.browse(cr, uid, ids, context):
437             cr.execute('update project_task set remaining_hours=remaining_hours+%.2f+(%.2f) where id=%d', (-vals.get('hours',0.0), work.hours, work.task_id.id))
438         return super(project_work,self).write(cr, uid, ids, vals, context)
439
440     def unlink(self, cr, uid, ids, *args, **kwargs):
441         for work in self.browse(cr, uid, ids):
442             cr.execute('update project_task set remaining_hours=remaining_hours+%.2f where id=%d', (work.hours, work.task_id.id))
443         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
444 project_work()
445
446
447
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
449