1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 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
24 from datetime import datetime, date
26 from tools.translate import _
27 from osv import fields, osv
28 from resource.faces import task as Task
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 # _name = 'project.project'
35 class project_task_type(osv.osv):
36 _name = 'project.task.type'
37 _description = 'Task Stage'
40 'name': fields.char('Stage Name', required=True, size=64, translate=True),
41 'description': fields.text('Description'),
42 'sequence': fields.integer('Sequence'),
43 'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
52 class project(osv.osv):
53 _name = "project.project"
54 _description = "Project"
55 _inherits = {'account.analytic.account': "analytic_account_id"}
57 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
59 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
60 if context and context.get('user_preference'):
61 cr.execute("""SELECT project.id FROM project_project project
62 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
63 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
64 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
65 return [(r[0]) for r in cr.fetchall()]
66 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
67 context=context, count=count)
69 def _complete_name(self, cr, uid, ids, name, args, context=None):
71 for m in self.browse(cr, uid, ids, context=context):
72 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
75 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
76 partner_obj = self.pool.get('res.partner')
78 return {'value':{'contact_id': False}}
79 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
80 val = {'contact_id': addr['contact']}
81 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84 val['pricelist_id'] = pricelist_id
87 def _get_childs(self, cr, uid, ids, childs,context=None):
88 cr.execute("""SELECT id FROM project_project WHERE analytic_account_id IN (
89 SELECT id FROM account_analytic_account WHERE parent_id = (
90 SELECT analytic_account_id FROM project_project project
91 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
95 for child in cr.fetchall():
96 if child[0] not in childs: childs.append(child[0])
97 self._get_childs( cr, uid, child[0], childs,context)
101 def _get_parents(self, cr, uid, ids, parents,context=None):
102 for project in self.read(cr, uid, ids, ['id', 'parent_id'],context):
103 if project.get('parent_id'):
104 cr.execute('''SELECT id FROM project_project WHERE analytic_account_id = '%s' '''%project.get('parent_id')[0])
105 for child in cr.fetchall():
106 if child[0] not in parents: parents.append(child[0])
107 child_rec= self.read(cr, uid, child[0], ['id', 'parent_id'],context)
108 if child_rec.get('parent_id'):
109 parents = self._get_parents(cr, uid, [child[0]], parents,context)
112 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
113 res = {}.fromkeys(ids, 0.0)
116 parents = self._get_parents(cr, uid, ids, ids,context)
118 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
125 project_id''', (tuple(ids),))
126 progress = dict(map(lambda x: (x[0], (x[1] or 0.0 ,x[2] or 0.0 ,x[3] or 0.0 ,x[4] or 0.0)), cr.fetchall()))
127 for project in self.browse(cr, uid, parents, context=context):
129 childs = self._get_childs(cr, uid, project.id, childs,context)
130 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
132 'planned_hours': s[0],
133 'effective_hours': s[2],
135 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
140 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
147 project_id''', (tuple(childs),))
148 child_progress = dict(map(lambda x: (x[0], (x[1] or 0.0 ,x[2] or 0.0 ,x[3] or 0.0 ,x[4] or 0.0)), cr.fetchall()))
149 planned_hours, effective_hours, total_hours, rnd= 0.0, 0.0,0.0, 0.0
151 ch_vals = child_progress.get(child, (0.0,0.0,0.0,0.0))
152 planned_hours, effective_hours, total_hours = planned_hours+ch_vals[0], effective_hours+ch_vals[2] , total_hours+ch_vals[1]
153 if res.get(project.id).get('planned_hours')+ planned_hours > 0:
154 rnd = round(( res.get(project.id).get('effective_hours')+effective_hours)/(res.get(project.id).get('planned_hours')+ planned_hours)*100,2) or 0.0
156 'planned_hours': res.get(project.id).get('planned_hours')+ planned_hours,
157 'effective_hours': res.get(project.id).get('effective_hours')+ effective_hours,
158 'total_hours': res.get(project.id).get('total_hours')+ total_hours,
163 def _get_project_task(self, cr, uid, ids, context=None):
165 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
167 result[task.project_id.id] = True
168 if task.project_id.parent_id:
169 cr.execute('''SELECT id FROM project_project WHERE analytic_account_id = '%s' '''%task.project_id.parent_id.id)
170 for parent in cr.fetchall():
171 result[parent[0]] = True
174 def _get_project_work(self, cr, uid, ids, context=None):
176 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
177 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
180 def unlink(self, cr, uid, ids, *args, **kwargs):
181 for proj in self.browse(cr, uid, ids):
183 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
184 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
187 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
188 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
189 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
190 'analytic_account_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.", ondelete="cascade", required=True),
191 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
192 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
194 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
195 help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
196 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
197 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
199 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks', 'parent_id', 'child_ids'], 10),
200 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
202 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
203 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
204 'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
206 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks','parent_id', 'child_ids'], 10),
207 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
209 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
210 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
211 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
212 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
213 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
215 def _get_type_common(self, cr, uid, context):
216 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
224 'type_ids': _get_type_common
227 # TODO: Why not using a SQL contraints ?
228 def _check_dates(self, cr, uid, ids, context=None):
229 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
230 if leave['date_start'] and leave['date']:
231 if leave['date_start'] > leave['date']:
236 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
239 def set_template(self, cr, uid, ids, context=None):
240 res = self.setActive(cr, uid, ids, value=False, context=context)
243 def set_done(self, cr, uid, ids, context=None):
244 task_obj = self.pool.get('project.task')
245 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
246 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
247 self.write(cr, uid, ids, {'state':'close'}, context=context)
248 for (id, name) in self.name_get(cr, uid, ids):
249 message = _("The project '%s' has been closed.") % name
250 self.log(cr, uid, id, message)
253 def set_cancel(self, cr, uid, ids, context=None):
254 task_obj = self.pool.get('project.task')
255 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
256 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
257 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
260 def set_pending(self, cr, uid, ids, context=None):
261 self.write(cr, uid, ids, {'state':'pending'}, context=context)
264 def set_open(self, cr, uid, ids, context=None):
265 self.write(cr, uid, ids, {'state':'open'}, context=context)
268 def reset_project(self, cr, uid, ids, context=None):
269 res = self.setActive(cr, uid, ids, value=True, context=context)
270 for (id, name) in self.name_get(cr, uid, ids):
271 message = _("The project '%s' has been opened.") % name
272 self.log(cr, uid, id, message)
275 def copy(self, cr, uid, id, default={}, context=None):
279 default = default or {}
280 context['active_test'] = False
281 default['state'] = 'open'
282 proj = self.browse(cr, uid, id, context=context)
283 if not default.get('name', False):
284 default['name'] = proj.name + _(' (copy)')
286 res = super(project, self).copy(cr, uid, id, default, context)
290 def template_copy(self, cr, uid, id, default={}, context=None):
291 task_obj = self.pool.get('project.task')
292 proj = self.browse(cr, uid, id, context=context)
294 default['tasks'] = [] #avoid to copy all the task automaticly
295 res = self.copy(cr, uid, id, default=default, context=context)
297 #copy all the task manually
299 for task in proj.tasks:
300 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
302 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
303 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
307 def duplicate_template(self, cr, uid, ids, context=None):
310 data_obj = self.pool.get('ir.model.data')
312 for proj in self.browse(cr, uid, ids, context=context):
313 parent_id = context.get('parent_id', False)
314 context.update({'analytic_project_copy': True})
315 new_date_start = time.strftime('%Y-%m-%d')
317 if proj.date_start and proj.date:
318 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
319 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
320 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
321 context.update({'copy':True})
322 new_id = self.template_copy(cr, uid, proj.id, default = {
323 'name': proj.name +_(' (copy)'),
325 'date_start':new_date_start,
327 'parent_id':parent_id}, context=context)
328 result.append(new_id)
330 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
331 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
333 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
335 if result and len(result):
337 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
338 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
339 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
340 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
341 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
342 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
344 'name': _('Projects'),
346 'view_mode': 'form,tree',
347 'res_model': 'project.project',
350 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
351 'type': 'ir.actions.act_window',
352 'search_view_id': search_view['res_id'],
356 # set active value for a project, its sub projects and its tasks
357 def setActive(self, cr, uid, ids, value=True, context=None):
358 task_obj = self.pool.get('project.task')
359 for proj in self.browse(cr, uid, ids, context=None):
360 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
361 cr.execute('select id from project_task where project_id=%s', (proj.id,))
362 tasks_id = [x[0] for x in cr.fetchall()]
364 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
365 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
367 self.setActive(cr, uid, child_ids, value, context=None)
370 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
371 context = context or {}
372 if type(ids) in (long, int,):
374 projects = self.browse(cr, uid, ids, context=context)
376 for project in projects:
377 if (not project.members) and force_members:
378 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
380 resource_pool = self.pool.get('resource.resource')
382 result = "from resource.faces import *\n"
383 result += "import datetime\n"
384 for project in self.browse(cr, uid, ids, context=context):
385 u_ids = [i.id for i in project.members]
386 if project.user_id and (project.user_id.id not in u_ids):
387 u_ids.append(project.user_id.id)
388 for task in project.tasks:
389 if task.state in ('done','cancelled'):
391 if task.user_id and (task.user_id.id not in u_ids):
392 u_ids.append(task.user_id.id)
393 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
394 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
395 for key, vals in resource_objs.items():
397 class User_%s(Resource):
399 ''' % (key, vals.get('efficiency', False))
406 def _schedule_project(self, cr, uid, project, context=None):
407 resource_pool = self.pool.get('resource.resource')
408 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
409 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
410 # TODO: check if we need working_..., default values are ok.
411 puids = [x.id for x in project.members]
413 puids.append(project.user_id.id)
421 project.date_start, working_days,
422 '|'.join(['User_'+str(x) for x in puids])
424 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
431 #TODO: DO Resource allocation and compute availability
432 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
438 def schedule_tasks(self, cr, uid, ids, context=None):
439 context = context or {}
440 if type(ids) in (long, int,):
442 projects = self.browse(cr, uid, ids, context=context)
443 result = self._schedule_header(cr, uid, ids, False, context=context)
444 for project in projects:
445 result += self._schedule_project(cr, uid, project, context=context)
446 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
449 exec result in local_dict
450 projects_gantt = Task.BalancedProject(local_dict['Project'])
452 for project in projects:
453 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
454 for task in project.tasks:
455 if task.state in ('done','cancelled'):
458 p = getattr(project_gantt, 'Task_%d' % (task.id,))
460 self.pool.get('project.task').write(cr, uid, [task.id], {
461 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
462 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
464 if (not task.user_id) and (p.booked_resource):
465 self.pool.get('project.task').write(cr, uid, [task.id], {
466 'user_id': int(p.booked_resource[0].name[5:]),
471 class users(osv.osv):
472 _inherit = 'res.users'
474 'context_project_id': fields.many2one('project.project', 'Project')
479 _name = "project.task"
480 _description = "Task"
482 _date_name = "date_start"
485 def _resolve_project_id_from_context(self, cr, uid, context=None):
486 """Return ID of project based on the value of 'project_id'
487 context key, or None if it cannot be resolved to a single project.
489 if context is None: context = {}
490 if type(context.get('project_id')) in (int, long):
491 project_id = context['project_id']
493 if isinstance(context.get('project_id'), basestring):
494 project_name = context['project_id']
495 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
496 if len(project_ids) == 1:
497 return project_ids[0][0]
499 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
500 stage_obj = self.pool.get('project.task.type')
501 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
502 order = stage_obj._order
503 access_rights_uid = access_rights_uid or uid
504 if read_group_order == 'type_id desc':
505 # lame way to allow reverting search, should just work in the trivial case
506 order = '%s desc' % order
508 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
510 domain = ['|', ('id','in',ids), ('project_default','=',1)]
511 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
512 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
513 # restore order of the search
514 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
517 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
518 res_users = self.pool.get('res.users')
519 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
520 access_rights_uid = access_rights_uid or uid
522 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
523 order = res_users._order
524 # lame way to allow reverting search, should just work in the trivial case
525 if read_group_order == 'user_id desc':
526 order = '%s desc' % order
527 # de-duplicate and apply search order
528 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
529 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
530 # restore order of the search
531 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
535 'type_id': _read_group_type_id,
536 'user_id': _read_group_user_id
540 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
541 obj_project = self.pool.get('project.project')
543 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
544 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
545 if id and isinstance(id, (long, int)):
546 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
547 args.append(('active', '=', False))
548 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
550 def _str_get(self, task, level=0, border='***', context=None):
551 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'+ \
552 border[0]+' '+(task.name or '')+'\n'+ \
553 (task.description or '')+'\n\n'
555 # Compute: effective_hours, total_hours, progress
556 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
558 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
559 hours = dict(cr.fetchall())
560 for task in self.browse(cr, uid, ids, context=context):
561 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
562 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
563 res[task.id]['progress'] = 0.0
564 if (task.remaining_hours + hours.get(task.id, 0.0)):
565 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
566 if task.state in ('done','cancelled'):
567 res[task.id]['progress'] = 100.0
571 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
572 if remaining and not planned:
573 return {'value':{'planned_hours': remaining}}
576 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
577 return {'value':{'remaining_hours': planned - effective}}
579 def onchange_project(self, cr, uid, id, project_id):
582 data = self.pool.get('project.project').browse(cr, uid, [project_id])
583 partner_id=data and data[0].parent_id.partner_id
585 return {'value':{'partner_id':partner_id.id}}
588 def _default_project(self, cr, uid, context=None):
591 if 'project_id' in context and context['project_id']:
592 return int(context['project_id'])
595 def duplicate_task(self, cr, uid, map_ids, context=None):
596 for new in map_ids.values():
597 task = self.browse(cr, uid, new, context)
598 child_ids = [ ch.id for ch in task.child_ids]
600 for child in task.child_ids:
601 if child.id in map_ids.keys():
602 child_ids.remove(child.id)
603 child_ids.append(map_ids[child.id])
605 parent_ids = [ ch.id for ch in task.parent_ids]
607 for parent in task.parent_ids:
608 if parent.id in map_ids.keys():
609 parent_ids.remove(parent.id)
610 parent_ids.append(map_ids[parent.id])
611 #FIXME why there is already the copy and the old one
612 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
614 def copy_data(self, cr, uid, id, default={}, context=None):
615 default = default or {}
616 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
617 if not default.get('remaining_hours', False):
618 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
619 default['active'] = True
620 default['type_id'] = False
621 if not default.get('name', False):
622 default['name'] = self.browse(cr, uid, id, context=context).name or ''
623 if not context.get('copy',False):
624 new_name = _("%s (copy)")%default.get('name','')
625 default.update({'name':new_name})
626 return super(task, self).copy_data(cr, uid, id, default, context)
629 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
631 for task in self.browse(cr, uid, ids, context=context):
634 if task.project_id.active == False or task.project_id.state == 'template':
638 def _get_task(self, cr, uid, ids, context=None):
640 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
641 if work.task_id: result[work.task_id.id] = True
645 'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
646 'name': fields.char('Task Summary', size=128, required=True),
647 'description': fields.text('Description'),
648 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
649 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
650 'type_id': fields.many2one('project.task.type', 'Stage'),
651 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
652 help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
653 \n If the task is over, the states is set to \'Done\'.'),
654 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
655 help="A task's kanban state indicates special situations affecting it:\n"
656 " * Normal is the default situation\n"
657 " * Blocked indicates something is preventing the progress of this task\n"
658 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
659 readonly=True, required=False),
660 'create_date': fields.datetime('Create Date', readonly=True,select=True),
661 'date_start': fields.datetime('Starting Date',select=True),
662 'date_end': fields.datetime('Ending Date',select=True),
663 'date_deadline': fields.date('Deadline',select=True),
664 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
665 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
666 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
667 'notes': fields.text('Notes'),
668 'planned_hours': fields.float('Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
669 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
671 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
672 'project.task.work': (_get_task, ['hours'], 10),
674 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
675 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
677 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
678 'project.task.work': (_get_task, ['hours'], 10),
680 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
682 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
683 'project.task.work': (_get_task, ['hours'], 10),
685 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
687 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
688 'project.task.work': (_get_task, ['hours'], 10),
690 'user_id': fields.many2one('res.users', 'Assigned to'),
691 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
692 'partner_id': fields.many2one('res.partner', 'Partner'),
693 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
694 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
695 'company_id': fields.many2one('res.company', 'Company'),
696 'id': fields.integer('ID', readonly=True),
697 'color': fields.integer('Color Index'),
698 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
703 'kanban_state': 'normal',
708 'project_id': _default_project,
709 'user_id': lambda obj, cr, uid, context: uid,
710 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
713 _order = "priority, sequence, date_start, name, id"
715 def set_priority(self, cr, uid, ids, priority):
718 return self.write(cr, uid, ids, {'priority' : priority})
720 def set_high_priority(self, cr, uid, ids, *args):
721 """Set task priority to high
723 return self.set_priority(cr, uid, ids, '1')
725 def set_normal_priority(self, cr, uid, ids, *args):
726 """Set task priority to normal
728 return self.set_priority(cr, uid, ids, '3')
730 def _check_recursion(self, cr, uid, ids, context=None):
732 visited_branch = set()
734 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
740 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
741 if id in visited_branch: #Cycle
744 if id in visited_node: #Already tested don't work one more time for nothing
747 visited_branch.add(id)
750 #visit child using DFS
751 task = self.browse(cr, uid, id, context=context)
752 for child in task.child_ids:
753 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
757 visited_branch.remove(id)
760 def _check_dates(self, cr, uid, ids, context=None):
763 obj_task = self.browse(cr, uid, ids[0], context=context)
764 start = obj_task.date_start or False
765 end = obj_task.date_end or False
772 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
773 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
776 # Override view according to the company definition
778 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
779 users_obj = self.pool.get('res.users')
781 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
782 # this should be safe (no context passed to avoid side-effects)
783 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
784 tm = obj_tm and obj_tm.name or 'Hours'
786 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
788 if tm in ['Hours','Hour']:
791 eview = etree.fromstring(res['arch'])
793 def _check_rec(eview):
794 if eview.attrib.get('widget','') == 'float_time':
795 eview.set('widget','float')
802 res['arch'] = etree.tostring(eview)
804 for f in res['fields']:
805 if 'Hours' in res['fields'][f]['string']:
806 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
809 def _check_child_task(self, cr, uid, ids, context=None):
812 tasks = self.browse(cr, uid, ids, context=context)
815 for child in task.child_ids:
816 if child.state in ['draft', 'open', 'pending']:
817 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
820 def action_close(self, cr, uid, ids, context=None):
821 # This action open wizard to send email to partner or project manager after close task.
824 task_id = len(ids) and ids[0] or False
825 self._check_child_task(cr, uid, ids, context=context)
826 if not task_id: return False
827 task = self.browse(cr, uid, task_id, context=context)
828 project = task.project_id
829 res = self.do_close(cr, uid, [task_id], context=context)
830 if project.warn_manager or project.warn_customer:
832 'name': _('Send Email after close task'),
835 'res_model': 'mail.compose.message',
836 'type': 'ir.actions.act_window',
839 'context': {'active_id': task.id,
840 'active_model': 'project.task'}
844 def do_close(self, cr, uid, ids, context={}):
848 request = self.pool.get('res.request')
849 if not isinstance(ids,list): ids = [ids]
850 for task in self.browse(cr, uid, ids, context=context):
852 project = task.project_id
854 # Send request to project manager
855 if project.warn_manager and project.user_id and (project.user_id.id != uid):
856 request.create(cr, uid, {
857 'name': _("Task '%s' closed") % task.name,
860 'act_to': project.user_id.id,
861 'ref_partner_id': task.partner_id.id,
862 'ref_doc1': 'project.task,%d'% (task.id,),
863 'ref_doc2': 'project.project,%d'% (project.id,),
866 for parent_id in task.parent_ids:
867 if parent_id.state in ('pending','draft'):
869 for child in parent_id.child_ids:
870 if child.id != task.id and child.state not in ('done','cancelled'):
873 self.do_reopen(cr, uid, [parent_id.id], context=context)
874 vals.update({'state': 'done'})
875 vals.update({'remaining_hours': 0.0})
876 if not task.date_end:
877 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
878 self.write(cr, uid, [task.id],vals, context=context)
879 message = _("The task '%s' is done") % (task.name,)
880 self.log(cr, uid, task.id, message)
883 def do_reopen(self, cr, uid, ids, context=None):
884 request = self.pool.get('res.request')
886 for task in self.browse(cr, uid, ids, context=context):
887 project = task.project_id
888 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
889 request.create(cr, uid, {
890 'name': _("Task '%s' set in progress") % task.name,
893 'act_to': project.user_id.id,
894 'ref_partner_id': task.partner_id.id,
895 'ref_doc1': 'project.task,%d' % task.id,
896 'ref_doc2': 'project.project,%d' % project.id,
899 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
902 def do_cancel(self, cr, uid, ids, context={}):
903 request = self.pool.get('res.request')
904 tasks = self.browse(cr, uid, ids, context=context)
905 self._check_child_task(cr, uid, ids, context=context)
907 project = task.project_id
908 if project.warn_manager and project.user_id and (project.user_id.id != uid):
909 request.create(cr, uid, {
910 'name': _("Task '%s' cancelled") % task.name,
913 'act_to': project.user_id.id,
914 'ref_partner_id': task.partner_id.id,
915 'ref_doc1': 'project.task,%d' % task.id,
916 'ref_doc2': 'project.project,%d' % project.id,
918 message = _("The task '%s' is cancelled.") % (task.name,)
919 self.log(cr, uid, task.id, message)
920 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
923 def do_open(self, cr, uid, ids, context={}):
924 if not isinstance(ids,list): ids = [ids]
925 tasks= self.browse(cr, uid, ids, context=context)
927 data = {'state': 'open'}
929 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
930 self.write(cr, uid, [t.id], data, context=context)
931 message = _("The task '%s' is opened.") % (t.name,)
932 self.log(cr, uid, t.id, message)
935 def do_draft(self, cr, uid, ids, context={}):
936 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
939 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
941 Delegate Task to another users.
943 assert delegate_data['user_id'], _("Delegated User should be specified")
944 delegrated_tasks = {}
945 for task in self.browse(cr, uid, ids, context=context):
946 delegrated_task_id = self.copy(cr, uid, task.id, {
947 'name': delegate_data['name'],
948 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
949 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
950 'planned_hours': delegate_data['planned_hours'] or 0.0,
951 'parent_ids': [(6, 0, [task.id])],
953 'description': delegate_data['new_task_description'] or '',
957 newname = delegate_data['prefix'] or ''
959 'remaining_hours': delegate_data['planned_hours_me'],
960 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
963 if delegate_data['state'] == 'pending':
964 self.do_pending(cr, uid, task.id, context=context)
965 elif delegate_data['state'] == 'done':
966 self.do_close(cr, uid, task.id, context=context)
968 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
969 self.log(cr, uid, task.id, message)
970 delegrated_tasks[task.id] = delegrated_task_id
971 return delegrated_tasks
973 def do_pending(self, cr, uid, ids, context={}):
974 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
975 for (id, name) in self.name_get(cr, uid, ids):
976 message = _("The task '%s' is pending.") % name
977 self.log(cr, uid, id, message)
980 def set_remaining_time_1(self, cr, uid, ids, context=None):
981 self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
984 def set_remaining_time_2(self, cr, uid, ids, context=None):
985 self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
988 def set_remaining_time_5(self, cr, uid, ids, context=None):
989 self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
992 def set_remaining_time_10(self, cr, uid, ids, context=None):
993 self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
996 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
997 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
999 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1000 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1002 def set_kanban_state_done(self, cr, uid, ids, context=None):
1003 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1005 def _change_type(self, cr, uid, ids, next, *args):
1007 go to the next stage
1008 if next is False, go to previous stage
1010 for task in self.browse(cr, uid, ids):
1011 if task.project_id.type_ids:
1012 typeid = task.type_id.id
1014 for type in task.project_id.type_ids :
1015 types_seq[type.id] = type.sequence
1017 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1019 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1020 sorted_types = [x[0] for x in types]
1022 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1023 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1024 index = sorted_types.index(typeid)
1025 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1028 def next_type(self, cr, uid, ids, *args):
1029 return self._change_type(cr, uid, ids, True, *args)
1031 def prev_type(self, cr, uid, ids, *args):
1032 return self._change_type(cr, uid, ids, False, *args)
1034 # Overridden to reset the kanban_state to normal whenever
1035 # the stage (type_id) of the task changes.
1036 def write(self, cr, uid, ids, vals, context=None):
1037 if isinstance(ids, (int, long)):
1039 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1040 new_stage = vals.get('type_id')
1041 vals_reset_kstate = dict(vals, kanban_state='normal')
1042 for t in self.browse(cr, uid, ids, context=context):
1043 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1044 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1046 return super(task,self).write(cr, uid, ids, vals, context=context)
1048 def unlink(self, cr, uid, ids, context=None):
1051 self._check_child_task(cr, uid, ids, context=context)
1052 res = super(task, self).unlink(cr, uid, ids, context)
1055 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1056 context = context or {}
1060 if task.state in ('done','cancelled'):
1065 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1067 for t2 in task.parent_ids:
1068 start.append("up.Task_%s.end" % (t2.id,))
1072 ''' % (ident,','.join(start))
1077 ''' % (ident, 'User_'+str(task.user_id.id))
1084 class project_work(osv.osv):
1085 _name = "project.task.work"
1086 _description = "Project Task Work"
1088 'name': fields.char('Work summary', size=128),
1089 'date': fields.datetime('Date', select="1"),
1090 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1091 'hours': fields.float('Time Spent'),
1092 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1093 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1097 'user_id': lambda obj, cr, uid, context: uid,
1098 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1101 _order = "date desc"
1102 def create(self, cr, uid, vals, *args, **kwargs):
1103 if 'hours' in vals and (not vals['hours']):
1104 vals['hours'] = 0.00
1105 if 'task_id' in vals:
1106 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1107 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1109 def write(self, cr, uid, ids, vals, context=None):
1110 if 'hours' in vals and (not vals['hours']):
1111 vals['hours'] = 0.00
1113 for work in self.browse(cr, uid, ids, context=context):
1114 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))
1115 return super(project_work,self).write(cr, uid, ids, vals, context)
1117 def unlink(self, cr, uid, ids, *args, **kwargs):
1118 for work in self.browse(cr, uid, ids):
1119 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1120 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1123 class account_analytic_account(osv.osv):
1125 _inherit = 'account.analytic.account'
1126 _description = 'Analytic Account'
1128 def create(self, cr, uid, vals, context=None):
1131 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1132 vals['child_ids'] = []
1133 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1135 def unlink(self, cr, uid, ids, *args, **kwargs):
1136 project_obj = self.pool.get('project.project')
1137 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1139 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1140 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1142 account_analytic_account()
1144 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: