1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from lxml import etree
26 from tools.translate import _
27 from osv import fields, osv
28 from tools.translate import _
30 #from resource.faces import *
31 #from resource.faces.lib import workbreakdown
32 #from resource.faces.lib import generator
33 #from resource.faces.lib import resource
35 class project(osv.osv):
36 _name = "project.project"
37 _description = "Project"
38 _inherits = {'account.analytic.account':"category_id"}
39 def _complete_name(self, cr, uid, ids, name, args, context):
41 for m in self.browse(cr, uid, ids, context=context):
42 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
46 def check_recursion(self, cursor, user, ids, parent=None):
47 return super(project, self).check_recursion(cursor, user, ids,
50 def onchange_partner_id(self, cr, uid, ids, part):
52 return {'value':{'contact_id': False, 'pricelist_id': False}}
53 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
55 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
56 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
58 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
59 res = {}.fromkeys(ids, 0.0)
63 ids2 = self.search(cr, uid, [('parent_id','child_of',ids)])
66 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
70 project_id in ('''+','.join(map(str,ids2))+''') AND
74 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3])), cr.fetchall()))
75 for project in self.browse(cr, uid, ids, context=context):
80 tocompute += p.child_ids
82 s[i] += progress.get(p.id, (0.0,0.0,0.0))[i]
84 'planned_hours': s[0],
85 'effective_hours': s[2],
87 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
91 def unlink(self, cr, uid, ids, *args, **kwargs):
92 for proj in self.browse(cr, uid, ids):
94 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
95 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
97 # 'name': fields.char("Project Name", size=128, required=True),
98 # 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128),
99 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
100 '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."),
101 'priority': fields.integer('Sequence'),
102 # 'manager': fields.many2one('res.users', 'Project Manager'),
103 '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."),
104 'resource_ids': fields.many2many('resource.resource', 'project_resource_rel', 'project_id', 'resource_id', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
105 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
106 # 'parent_id': fields.many2one('project.project', 'Parent Project',\
107 # help="If you have [?] in the name, it means there are no analytic account linked to project."),
108 # 'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
109 '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."),
110 '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."),
111 '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."),
112 '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."),
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 'resource_calendar_id': fields.many2one('resource.calendar', '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,
121 # help='The project can be in either if the states \'Template\' and \'Running\'.\n If it is template then we can make projects based on the template projects. If its in \'Running\' state it is a normal project.\
122 # \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'),
123 # 'company_id': fields.many2one('res.company', 'Company'),
127 'active': lambda *a: True,
128 # 'manager': lambda object,cr,uid,context: uid,
129 'priority': lambda *a: 1,
130 # 'state': lambda *a: 'open',
131 # 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.project', context=c)
134 # _order = "parent_id,priority,name"
136 # (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
139 # toggle activity of projects, their sub projects and their tasks
140 def set_template(self, cr, uid, ids, context={}):
141 res = self.setActive(cr, uid, ids, value=False, context=context)
144 def set_done(self, cr, uid, ids, context={}):
145 self.write(cr, uid, ids, {'state':'done'}, context=context)
148 def set_cancel(self, cr, uid, ids, context={}):
149 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
152 def set_pending(self, cr, uid, ids, context={}):
153 self.write(cr, uid, ids, {'state':'pending'}, context=context)
156 def set_open(self, cr, uid, ids, context={}):
157 self.write(cr, uid, ids, {'state':'open'}, context=context)
160 def reset_project(self, cr, uid, ids, context={}):
161 res = self.setActive(cr, uid, ids,value=True, context=context)
164 def copy(self, cr, uid, id, default={},context={}):
165 proj = self.browse(cr, uid, id, context=context)
166 default = default or {}
167 context['active_test'] = False
168 default['state'] = 'open'
169 if not default.get('name', False):
170 default['name'] = proj.name+_(' (copy)')
171 res = super(project, self).copy(cr, uid, id, default, context)
172 ids = self.search(cr, uid, [('parent_id','child_of', [res])])
173 cr.execute('update project_task set active=True where project_id in ('+','.join(map(str, ids))+')')
176 def duplicate_template(self, cr, uid, ids,context={}):
177 for proj in self.browse(cr, uid, ids):
178 parent_id=context.get('parent_id',False)
179 new_id=self.pool.get('project.project').copy(cr, uid, proj.id,default={'name':proj.name+_(' (copy)'),'state':'open','parent_id':parent_id})
180 cr.execute('select id from project_task where project_id=%s', (proj.id,))
182 for (tasks_id,) in res:
183 self.pool.get('project.task').copy(cr, uid, tasks_id,default={'project_id':new_id,'active':True}, context=context)
184 cr.execute('select id from project_project where parent_id=%s', (proj.id,))
186 project_ids = [x[0] for x in res]
187 for child in project_ids:
188 self.duplicate_template(cr, uid, [child],context={'parent_id':new_id})
190 # TODO : Improve this to open the new project (using a wizard)
193 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.'))
195 # set active value for a project, its sub projects and its tasks
196 def setActive(self, cr, uid, ids, value=True, context={}):
197 for proj in self.browse(cr, uid, ids, context):
198 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
199 cr.execute('select id from project_task where project_id=%s', (proj.id,))
200 tasks_id = [x[0] for x in cr.fetchall()]
202 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
203 cr.execute('select id from project_project where parent_id=%s', (proj.id,))
204 project_ids = [x[0] for x in cr.fetchall()]
205 for child in project_ids:
206 self.setActive(cr, uid, [child], value, context)
210 class project_task_type(osv.osv):
211 _name = 'project.task.type'
212 _description = 'Project task type'
214 'name': fields.char('Type', required=True, size=64, translate=True),
215 'description': fields.text('Description'),
220 _name = "project.task"
221 _description = "Tasks"
222 _date_name = "date_start"
224 def _str_get(self, task, level=0, border='***', context={}):
225 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'+ \
226 border[0]+' '+(task.name or '')+'\n'+ \
227 (task.description or '')+'\n\n'
229 def _history_get(self, cr, uid, ids, name, args, context={}):
231 for task in self.browse(cr, uid, ids, context=context):
232 result[task.id] = self._str_get(task, border='===')
237 result[task.id] = self._str_get(t2, level) + result[task.id]
239 t3 = map(lambda x: (x,1), task.child_ids)
242 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
243 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
246 # Compute: effective_hours, total_hours, progress
247 def _hours_get(self, cr, uid, ids, field_names, args, context):
248 task_set = ','.join(map(str, ids))
249 cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
250 hours = dict(cr.fetchall())
252 for task in self.browse(cr, uid, ids, context=context):
254 res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
255 res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
256 if (task.remaining_hours + hours.get(task.id, 0.0)):
257 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2)
259 res[task.id]['progress'] = 0.0
260 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
263 def onchange_planned(self, cr, uid, ids, planned, effective, date_start,occupation_rate=0.0):
265 for res in self.browse(cr, uid, ids):
266 if date_start and planned:
267 resource_id = self.pool.get('resource.resource').search(cr,uid,[('user_id','=',res.user_id.id)])
268 resource_obj = self.pool.get('resource.resource').browse(cr,uid,resource_id)[0]
269 print 'Resource Calendar::::',resource_obj.calendar_id.id
270 d = mx.DateTime.strptime(date_start,'%Y-%m-%d %H:%M:%S')
271 hrs = (planned)/(occupation_rate)
272 print 'Hours::::',hrs
273 work_times = self.pool.get('resource.calendar').interval_get(cr, uid, resource_obj.calendar_id.id or False, d, hrs or 0.0, resource_obj.id)
274 print 'Date_end',work_times
275 result['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
276 print 'Date End',result['date_end']
277 result['remaining_hours'] = planned-effective
278 return {'value':result}
281 def _default_project(self, cr, uid, context={}):
282 if 'project_id' in context and context['project_id']:
283 return context['project_id']
286 #_sql_constraints = [
287 # ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
290 def copy_data(self, cr, uid, id, default={},context={}):
291 default = default or {}
292 default['work_ids'] = []
293 return super(task, self).copy_data(cr, uid, id, default, context)
295 def _check_date(self,cr,uid,ids):
296 for res in self.browse(cr,uid,ids):
297 if res.date_start and res.date_end:
298 if res.date_start > res.date_end:
303 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it."),
304 'name': fields.char('Task summary', size=128, required=True),
305 'description': fields.text('Description'),
306 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
307 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
308 'type': fields.many2one('project.task.type', 'Type'),
309 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
310 help='If the task is created the state \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
311 \n If the task is over, the states is set to \'Done\'.'),
312 'date_start': fields.datetime('Starting Date'),
313 'date_end': fields.datetime('Ending Date'),
314 'date_deadline': fields.datetime('Deadline'),
315 'date_close': fields.datetime('Date Closed', readonly=True),
316 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
317 help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
318 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
319 'child_ids': fields.many2many('project.task', 'project_task_child_rel', 'task_id', 'child_id', 'Delegated Tasks'),
320 'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
321 'notes': fields.text('Notes'),
322 'occupation_rate': fields.float('Occupation Rate', help='The occupation rate fields indicates how much of his time a user is working on a task. A 100% occupation rate means the user works full time on the tasks. The ending date of a task is computed like this: Starting Date + Duration / Occupation Rate.'),
323 '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.'),
324 '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."),
325 'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
326 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
327 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
328 '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."),
330 'user_id': fields.many2one('res.users', 'Assigned to'),
331 'delegated_user_id': fields.related('child_ids','user_id',type='many2one', relation='res.users', string='Delegated To'),
332 'partner_id': fields.many2one('res.partner', 'Partner'),
333 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
334 'manager_id': fields.related('project_id','manager', type='many2one', relation='res.users', string='Project Manager'),
335 'company_id': fields.many2one('res.company', 'Company'),
338 'user_id': lambda obj,cr,uid,context: uid,
339 'state': lambda *a: 'draft',
340 'priority': lambda *a: '2',
341 'progress': lambda *a: 0,
342 'sequence': lambda *a: 10,
343 'active': lambda *a: True,
344 'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
345 'project_id': _default_project,
346 'occupation_rate':lambda *a: '1',
347 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
349 _order = "sequence, priority, date_deadline, id"
352 # Override view according to the company definition
354 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
355 obj_tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode_id
356 tm = obj_tm and obj_tm.name or 'Hours'
358 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
360 if tm in ['Hours','Hour']:
363 eview = etree.fromstring(res['arch'])
365 def _check_rec(eview):
366 if eview.attrib.get('widget','') == 'float_time':
367 eview.set('widget','float')
374 res['arch'] = etree.tostring(eview)
376 for f in res['fields']:
377 if 'Hours' in res['fields'][f]['string']:
378 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
382 def do_close(self, cr, uid, ids, *args):
383 request = self.pool.get('res.request')
384 tasks = self.browse(cr, uid, ids)
386 project = task.project_id
388 if project.warn_manager and project.user_id and (project.user_id.id != uid):
389 request.create(cr, uid, {
390 'name': _("Task '%s' closed") % task.name,
393 'act_to': project.user_id.id,
394 'ref_partner_id': task.partner_id.id,
395 'ref_doc1': 'project.task,%d'% (task.id,),
396 'ref_doc2': 'project.project,%d'% (project.id,),
398 self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
399 if task.parent_ids and task.parent_ids.state in ('pending','draft'):
401 for child in task.parent_ids.child_ids:
402 if child.id != task.id and child.state not in ('done','cancelled'):
405 self.do_reopen(cr, uid, [task.parent_ids.id])
408 def do_reopen(self, cr, uid, ids, *args):
409 request = self.pool.get('res.request')
410 tasks = self.browse(cr, uid, ids)
412 project = task.project_id
413 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
414 request.create(cr, uid, {
415 'name': _("Task '%s' set in progress") % task.name,
418 'act_to': project.user_id.id,
419 'ref_partner_id': task.partner_id.id,
420 'ref_doc1': 'project.task,%d' % task.id,
421 'ref_doc2': 'project.project,%d' % project.id,
424 self.write(cr, uid, [task.id], {'state': 'open'})
427 def do_cancel(self, cr, uid, ids, *args):
428 request = self.pool.get('res.request')
429 tasks = self.browse(cr, uid, ids)
431 project = task.project_id
432 if project.warn_manager and project.user_id and (project.user_id.id != uid):
433 request.create(cr, uid, {
434 'name': _("Task '%s' cancelled") % task.name,
437 'act_to': project.user_id.id,
438 'ref_partner_id': task.partner_id.id,
439 'ref_doc1': 'project.task,%d' % task.id,
440 'ref_doc2': 'project.project,%d' % project.id,
442 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
445 def do_open(self, cr, uid, ids, *args):
446 tasks= self.browse(cr,uid,ids)
448 self.write(cr, uid, [t.id], {'state': 'open'})
451 def do_draft(self, cr, uid, ids, *args):
452 self.write(cr, uid, ids, {'state': 'draft'})
456 def do_pending(self, cr, uid, ids, *args):
457 self.write(cr, uid, ids, {'state': 'pending'})
463 class project_work(osv.osv):
464 _name = "project.task.work"
465 _description = "Task Work"
467 'name': fields.char('Work summary', size=128),
468 'date': fields.datetime('Date'),
469 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
470 'hours': fields.float('Time Spent'),
471 'user_id': fields.many2one('res.users', 'Done by', required=True),
472 'company_id': fields.related('task_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
475 'user_id': lambda obj,cr,uid,context: uid,
476 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
479 def create(self, cr, uid, vals, *args, **kwargs):
480 if 'hours' in vals and (not vals['hours']):
482 if 'task_id' in vals:
483 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
484 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
486 def write(self, cr, uid, ids,vals,context={}):
487 if 'hours' in vals and (not vals['hours']):
490 for work in self.browse(cr, uid, ids, context):
491 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))
492 return super(project_work,self).write(cr, uid, ids, vals, context)
494 def unlink(self, cr, uid, ids, *args, **kwargs):
495 for work in self.browse(cr, uid, ids):
496 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
497 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
500 class config_compute_remaining(osv.osv_memory):
501 _name='config.compute.remaining'
502 def _get_remaining(self,cr, uid, ctx):
503 if 'active_id' in ctx:
504 return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
508 'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
512 'remaining_hours': _get_remaining
515 def compute_hours(self, cr, uid, ids, context=None):
516 if 'active_id' in context:
517 remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
518 self.pool.get('project.task').write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
520 'type': 'ir.actions.act_window_close',
522 config_compute_remaining()
524 class message(osv.osv):
525 _name = "project.message"
526 _description = "Message"
528 'subject': fields.char('Subject', size=128),
529 'description': fields.char('Description', size =128),
530 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
531 'date': fields.date('Date'),
532 'user_id': fields.many2one('res.users', 'User'),
536 def _project_get(self, cr, uid, context={}):
537 obj = self.pool.get('project.project')
538 ids = obj.search(cr, uid, [])
539 res = obj.read(cr, uid, ids, ['id','name'], context)
540 res = [(str(r['id']),r['name']) for r in res]
543 class users(osv.osv):
544 _inherit = 'res.users'
545 _description = "Users"
547 'context_project_id': fields.selection(_project_get, 'Project'),
552 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: