1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from lxml import etree
24 from mx import DateTime
25 from mx.DateTime import now
27 from tools.translate import _
29 from osv import fields, osv
30 from tools.translate import _
32 class project(osv.osv):
33 _name = "project.project"
34 _description = "Project"
36 def _complete_name(self, cr, uid, ids, name, args, context):
38 for m in self.browse(cr, uid, ids, context=context):
39 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
43 def check_recursion(self, cursor, user, ids, parent=None):
44 return super(project, self).check_recursion(cursor, user, ids,
47 def onchange_partner_id(self, cr, uid, ids, part):
49 return {'value':{'contact_id': False, 'pricelist_id': False}}
50 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
52 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
53 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
55 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
56 res = {}.fromkeys(ids, 0.0)
60 ids2 = self.search(cr, uid, [('parent_id','child_of',ids)])
63 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
72 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3])), cr.fetchall()))
73 for project in self.browse(cr, uid, ids, context=context):
78 tocompute += p.child_id
80 s[i] += progress.get(p.id, (0.0,0.0,0.0))[i]
82 'planned_hours': s[0],
83 'effective_hours': s[2],
85 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
89 def unlink(self, cr, uid, ids, *args, **kwargs):
90 for proj in self.browse(cr, uid, ids):
92 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
93 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
95 'name': fields.char("Project Name", size=128, required=True),
96 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128),
97 'active': fields.boolean('Active'),
98 '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 enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc."),
99 'priority': fields.integer('Sequence'),
100 'manager': fields.many2one('res.users', 'Project Manager'),
101 '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."),
102 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
103 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
104 'parent_id': fields.many2one('project.project', 'Parent Project',\
105 help="If you have [?] in the name, it means there are no analytic account linked to project."),
106 'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
107 'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project."),
108 'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project."),
109 'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project."),
110 'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
111 'date_start': fields.date('Starting Date'),
112 'date_end': fields.date('Expected End'),
113 'partner_id': fields.many2one('res.partner', 'Partner'),
114 'contact_id': fields.many2one('res.partner.address', 'Contact'),
115 'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer."),
116 'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed."),
117 'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed."),
118 'notes': fields.text('Notes', help="Internal description of the project."),
119 'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
120 'state': fields.selection([('template', 'Template'), ('open', 'Running'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True),
124 'active': lambda *a: True,
125 'manager': lambda object,cr,uid,context: uid,
126 'priority': lambda *a: 1,
127 'date_start': lambda *a: time.strftime('%Y-%m-%d'),
128 'state': lambda *a: 'open'
131 _order = "parent_id,priority,name"
133 (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
136 # toggle activity of projects, their sub projects and their tasks
137 def set_template(self, cr, uid, ids, context={}):
138 res = self.setActive(cr, uid, ids, value=False, context=context)
141 def set_done(self, cr, uid, ids, context={}):
142 self.write(cr, uid, ids, {'state':'done'}, context=context)
145 def set_cancel(self, cr, uid, ids, context={}):
146 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
149 def set_pending(self, cr, uid, ids, context={}):
150 self.write(cr, uid, ids, {'state':'pending'}, context=context)
153 def set_open(self, cr, uid, ids, context={}):
154 self.write(cr, uid, ids, {'state':'open'}, context=context)
157 def reset_project(self, cr, uid, ids, context={}):
158 res = self.setActive(cr, uid, ids,value=True, context=context)
161 def copy(self, cr, uid, id, default={},context={}):
162 proj = self.browse(cr, uid, id, context=context)
163 default = default or {}
164 context['active_test'] = False
165 default['state'] = 'open'
166 if not default.get('name', False):
167 default['name'] = proj.name+_(' (copy)')
168 res = super(project, self).copy(cr, uid, id, default, context)
169 ids = self.search(cr, uid, [('parent_id','child_of', [res])])
170 cr.execute('update project_task set active=True where project_id in %s', (tuple(ids),))
173 def duplicate_template(self, cr, uid, ids,context={}):
174 default = {'parent_id': context.get('parent_id',False)}
176 self.copy(cr, uid, id, default=default)
178 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.'))
180 # set active value for a project, its sub projects and its tasks
181 def setActive(self, cr, uid, ids, value=True, context={}):
182 for proj in self.browse(cr, uid, ids, context):
183 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
184 cr.execute('select id from project_task where project_id=%s', (proj.id,))
185 tasks_id = [x[0] for x in cr.fetchall()]
187 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
188 cr.execute('select id from project_project where parent_id=%s', (proj.id,))
189 project_ids = [x[0] for x in cr.fetchall()]
190 for child in project_ids:
191 self.setActive(cr, uid, [child], value, context)
195 class project_task_type(osv.osv):
196 _name = 'project.task.type'
197 _description = 'Project task type'
199 'name': fields.char('Type', required=True, size=64, translate=True),
200 'description': fields.text('Description'),
205 _name = "project.task"
206 _description = "Tasks"
207 _date_name = "date_start"
208 def _str_get(self, task, level=0, border='***', context={}):
209 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'+ \
210 border[0]+' '+(task.name or '')+'\n'+ \
211 (task.description or '')+'\n\n'
213 def _history_get(self, cr, uid, ids, name, args, context={}):
215 for task in self.browse(cr, uid, ids, context=context):
216 result[task.id] = self._str_get(task, border='===')
221 result[task.id] = self._str_get(t2, level) + result[task.id]
223 t3 = map(lambda x: (x,1), task.child_ids)
226 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
227 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
230 # Compute: effective_hours, total_hours, progress
231 def _hours_get(self, cr, uid, ids, field_names, args, context):
232 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in %s GROUP BY task_id", (tuple(ids),))
233 hours = dict(cr.fetchall())
235 for task in self.browse(cr, uid, ids, context=context):
237 res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
238 res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
239 if (task.remaining_hours + hours.get(task.id, 0.0)):
240 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2)
242 res[task.id]['progress'] = 0.0
243 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
246 def onchange_planned(self, cr, uid, ids, planned, effective=0.0):
247 return {'value':{'remaining_hours': planned-effective}}
249 def _default_project(self, cr, uid, context={}):
250 if 'project_id' in context and context['project_id']:
251 return context['project_id']
254 #_sql_constraints = [
255 # ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
258 def copy_data(self, cr, uid, id, default={},context={}):
259 default = default or {}
260 default['work_ids'] = []
261 return super(task, self).copy_data(cr, uid, id, default, context)
264 'active': fields.boolean('Active'),
265 'name': fields.char('Task summary', size=128, required=True),
266 'description': fields.text('Description'),
267 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
268 'sequence': fields.integer('Sequence'),
269 'type': fields.many2one('project.task.type', 'Type'),
270 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'Status', readonly=True, required=True),
271 'date_start': fields.datetime('Starting Date'),
272 'date_deadline': fields.datetime('Deadline'),
273 'date_close': fields.datetime('Date Closed', readonly=True),
274 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
275 help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
276 'parent_id': fields.many2one('project.task', 'Parent Task'),
277 'child_ids': fields.one2many('project.task', 'parent_id', 'Delegated Tasks'),
278 'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
279 'notes': fields.text('Notes'),
281 'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
282 '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."),
283 'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
284 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
285 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
286 '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."),
288 'user_id': fields.many2one('res.users', 'Assigned to'),
289 'delegated_user_id': fields.related('child_ids','user_id',type='many2one', relation='res.users', string='Delegated To'),
290 'partner_id': fields.many2one('res.partner', 'Partner'),
291 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
294 'user_id': lambda obj,cr,uid,context: uid,
295 'state': lambda *a: 'draft',
296 'priority': lambda *a: '2',
297 'progress': lambda *a: 0,
298 'sequence': lambda *a: 10,
299 'active': lambda *a: True,
300 'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
301 'project_id': _default_project,
303 _order = "sequence, priority, date_deadline, id"
305 def _check_recursion(self, cr, uid, ids):
308 cr.execute('select distinct parent_id from project_task where id in %s', (tuple(ids),))
309 ids = filter(None, map(lambda x:x[0], cr.fetchall()))
316 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_id'])
319 # Override view according to the company definition
321 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False):
322 tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode or False
323 f = self.pool.get('res.company').fields_get(cr, uid, ['project_time_mode'], context)
326 word = dict(f['project_time_mode']['selection'])[tm]
328 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar)
329 if (not tm) or (tm=='hours'):
331 eview = etree.fromstring(res['arch'])
332 def _check_rec(eview, tm):
333 if eview.attrib.get('widget',False) == 'float_time':
334 eview.set('widget','float')
336 _check_rec(child, tm)
338 _check_rec(eview, tm)
339 res['arch'] = etree.tostring(eview)
340 for f in res['fields']:
341 if 'Hours' in res['fields'][f]['string']:
342 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',word)
345 def do_close(self, cr, uid, ids, *args):
346 request = self.pool.get('res.request')
347 tasks = self.browse(cr, uid, ids)
349 project = task.project_id
351 if project.warn_manager and project.manager and (project.manager.id != uid):
352 request.create(cr, uid, {
353 'name': _("Task '%s' closed") % task.name,
356 'act_to': project.manager.id,
357 'ref_partner_id': task.partner_id.id,
358 'ref_doc1': 'project.task,%d'% (task.id,),
359 'ref_doc2': 'project.project,%d'% (project.id,),
361 self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
362 if task.parent_id and task.parent_id.state in ('pending','draft'):
364 for child in task.parent_id.child_ids:
365 if child.id != task.id and child.state not in ('done','cancelled'):
368 self.do_reopen(cr, uid, [task.parent_id.id])
371 def do_reopen(self, cr, uid, ids, *args):
372 request = self.pool.get('res.request')
373 tasks = self.browse(cr, uid, ids)
375 project = task.project_id
376 if project and project.warn_manager and project.manager.id and (project.manager.id != uid):
377 request.create(cr, uid, {
378 'name': _("Task '%s' set in progress") % task.name,
381 'act_to': project.manager.id,
382 'ref_partner_id': task.partner_id.id,
383 'ref_doc1': 'project.task,%d' % task.id,
384 'ref_doc2': 'project.project,%d' % project.id,
387 self.write(cr, uid, [task.id], {'state': 'open'})
390 def do_cancel(self, cr, uid, ids, *args):
391 request = self.pool.get('res.request')
392 tasks = self.browse(cr, uid, ids)
394 project = task.project_id
395 if project.warn_manager and project.manager and (project.manager.id != uid):
396 request.create(cr, uid, {
397 'name': _("Task '%s' cancelled") % task.name,
400 'act_to': project.manager.id,
401 'ref_partner_id': task.partner_id.id,
402 'ref_doc1': 'project.task,%d' % task.id,
403 'ref_doc2': 'project.project,%d' % project.id,
405 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
408 def do_open(self, cr, uid, ids, *args):
409 tasks= self.browse(cr,uid,ids)
411 self.write(cr, uid, [t.id], {'state': 'open'})
414 def do_draft(self, cr, uid, ids, *args):
415 self.write(cr, uid, ids, {'state': 'draft'})
419 def do_pending(self, cr, uid, ids, *args):
420 self.write(cr, uid, ids, {'state': 'pending'})
426 class project_work(osv.osv):
427 _name = "project.task.work"
428 _description = "Task Work"
430 'name': fields.char('Work summary', size=128),
431 'date': fields.datetime('Date'),
432 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
433 'hours': fields.float('Time Spent'),
434 'user_id': fields.many2one('res.users', 'Done by', required=True),
437 'user_id': lambda obj,cr,uid,context: uid,
438 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
441 def create(self, cr, uid, vals, *args, **kwargs):
442 if 'hours' in vals and (not vals['hours']):
444 if 'task_id' in vals:
445 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
446 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
448 def write(self, cr, uid, ids,vals,context={}):
449 if 'hours' in vals and (not vals['hours']):
452 for work in self.browse(cr, uid, ids, context):
453 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
454 return super(project_work,self).write(cr, uid, ids, vals, context)
456 def unlink(self, cr, uid, ids, *args, **kwargs):
457 for work in self.browse(cr, uid, ids):
458 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
459 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
462 class config_compute_remaining(osv.osv_memory):
463 _name='config.compute.remaining'
464 def _get_remaining(self,cr, uid, ctx):
465 if 'active_id' in ctx:
466 return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
470 'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
474 'remaining_hours': _get_remaining
477 def compute_hours(self, cr, uid, ids, context=None):
478 if 'active_id' in context:
479 remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
480 self.pool.get('project.task').write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
482 'type': 'ir.actions.act_window_close',
484 config_compute_remaining()
486 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: