1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
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
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.
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.
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.
29 ##############################################################################
31 from lxml import etree
32 from mx import DateTime
33 from mx.DateTime import now
36 from osv import fields, osv
38 class project(osv.osv):
39 _name = "project.project"
40 _description = "Project"
42 def _calc_effective(self, cr, uid, ids, name, args, context):
43 ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
46 cr.execute('SELECT t.project_id, COALESCE(SUM(w.hours),0) \
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]) + ') \
53 for project_id, sum in cr.fetchall():
54 res_sum[project_id] = sum
57 ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
58 res.setdefault(id, 0.0)
60 res[id] += res_sum.get(idx, 0.0)
63 def _calc_planned(self, cr, uid, ids, name, args, context):
64 ids2 = self.search(cr, uid, [('parent_id', 'child_of', ids)])
67 cr.execute('SELECT project_id, COALESCE(SUM(total_hours),0) \
69 WHERE project_id IN (' + ','.join([str(x) for x in ids2]) + ') \
72 for project_id, sum in cr.fetchall():
73 res_sum[project_id] = sum
76 ids3 = self.search(cr, uid, [('parent_id', 'child_of', [id])])
77 res.setdefault(id, 0.0)
79 res[id] += res_sum.get(idx, 0.0)
82 def check_recursion(self, cursor, user, ids, parent=None):
83 return super(project, self).check_recursion(cursor, user, ids,
86 def onchange_partner_id(self, cr, uid, ids, part):
88 return {'value':{'contact_id': False, 'pricelist_id': False}}
89 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
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}}
94 def _progress_rate(self, cr, uid, ids, name, arg, context=None):
95 res = {}.fromkeys(ids, 0.0)
99 project_id, sum(progress*total_hours), sum(total_hours)
103 project_id in ('''+','.join(map(str,ids))+''') AND
107 for id,prog,tot in cr.fetchall():
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),
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'
148 (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
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)
156 def set_done(self, cr, uid, ids, context={}):
157 self.write(cr, uid, ids, {'state':'done'}, context=context)
160 def set_cancel(self, cr, uid, ids, context={}):
161 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
164 def set_pending(self, cr, uid, ids, context={}):
165 self.write(cr, uid, ids, {'state':'pending'}, context=context)
168 def set_open(self, cr, uid, ids, context={}):
169 self.write(cr, uid, ids, {'state':'open'}, context=context)
172 def reset_project(self, cr, uid, ids, context={}):
173 res = self.setActive(cr, uid, ids,value=True, context=context)
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)
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,))
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,))
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})
196 # TODO : Improve this to open the new project (using a wizard)
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.'))
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()]
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)
216 class project_task_type(osv.osv):
217 _name = 'project.task.type'
218 _description = 'Project task type'
220 'name': fields.char('Type', required=True, size=64, translate=True),
221 'description': fields.text('Description'),
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'
234 def _history_get(self, cr, uid, ids, name, args, context={}):
236 for task in self.browse(cr, uid, ids, context=context):
237 result[task.id] = self._str_get(task, border='===')
242 result[task.id] = self._str_get(t2, level) + result[task.id]
244 t3 = map(lambda x: (x,1), task.child_ids)
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)
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())
257 for task in self.browse(cr, uid, ids, context=context):
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)
264 res[task.id]['progress'] = 0.0
265 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
268 def onchange_planned(self, cr, uid, ids, planned, effective):
269 return {'value':{'remaining_hours': planned-effective}}
271 #_sql_constraints = [
272 # ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
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'),
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."),
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)]}),
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'),
312 _order = "sequence, priority, date_deadline, id"
315 # Override view according to the company definition
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]
322 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar)
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')
330 _check_rec(child, tm)
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)
339 def do_close(self, cr, uid, ids, *args):
340 request = self.pool.get('res.request')
341 tasks = self.browse(cr, uid, ids)
343 project = task.project_id
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,
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,),
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])
360 def do_reopen(self, cr, uid, ids, *args):
361 request = self.pool.get('res.request')
362 tasks = self.browse(cr, uid, ids)
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,
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,
376 self.write(cr, uid, [task.id], {'state': 'open'})
379 def do_cancel(self, cr, uid, ids, *args):
380 request = self.pool.get('res.request')
381 tasks = self.browse(cr, uid, ids)
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,
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,
394 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
397 def do_open(self, cr, uid, ids, *args):
398 tasks= self.browse(cr,uid,ids)
400 self.write(cr, uid, [t.id], {'state': 'open'})
403 def do_draft(self, cr, uid, ids, *args):
404 self.write(cr, uid, ids, {'state': 'draft'})
408 def do_pending(self, cr, uid, ids, *args):
409 self.write(cr, uid, ids, {'state': 'pending'})
415 class project_work(osv.osv):
416 _name = "project.task.work"
417 _description = "Task Work"
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),
426 'user_id': lambda obj,cr,uid,context: uid,
427 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
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)
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)
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)
448 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: