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 openerp.addons.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"}
56 _inherit = ['ir.needaction_mixin', 'mail.thread']
58 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
61 if context and context.get('user_preference'):
62 cr.execute("""SELECT project.id FROM project_project project
63 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
64 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
65 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
66 return [(r[0]) for r in cr.fetchall()]
67 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
68 context=context, count=count)
70 def _complete_name(self, cr, uid, ids, name, args, context=None):
72 for m in self.browse(cr, uid, ids, context=context):
73 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
76 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
77 partner_obj = self.pool.get('res.partner')
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_projects_from_tasks(self, cr, uid, task_ids, context=None):
88 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
89 project_ids = [task.project_id.id for task in tasks if task.project_id]
90 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
92 def _get_project_and_parents(self, cr, uid, ids, context=None):
93 """ return the project ids and all their parent projects """
97 SELECT DISTINCT parent.id
98 FROM project_project project, project_project parent, account_analytic_account account
99 WHERE project.analytic_account_id = account.id
100 AND parent.analytic_account_id = account.parent_id
103 ids = [t[0] for t in cr.fetchall()]
107 def _get_project_and_children(self, cr, uid, ids, context=None):
108 """ retrieve all children projects of project ids;
109 return a dictionary mapping each project to its parent project (or None)
111 res = dict.fromkeys(ids, None)
114 SELECT project.id, parent.id
115 FROM project_project project, project_project parent, account_analytic_account account
116 WHERE project.analytic_account_id = account.id
117 AND parent.analytic_account_id = account.parent_id
120 dic = dict(cr.fetchall())
125 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
126 child_parent = self._get_project_and_children(cr, uid, ids, context)
127 # compute planned_hours, total_hours, effective_hours specific to each project
129 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
130 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
131 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
133 """, (tuple(child_parent.keys()),))
134 # aggregate results into res
135 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
136 for id, planned, total, effective in cr.fetchall():
137 # add the values specific to id to all parent projects of id in the result
140 res[id]['planned_hours'] += planned
141 res[id]['total_hours'] += total
142 res[id]['effective_hours'] += effective
143 id = child_parent[id]
144 # compute progress rates
146 if res[id]['total_hours']:
147 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
149 res[id]['progress_rate'] = 0.0
152 def unlink(self, cr, uid, ids, *args, **kwargs):
153 for proj in self.browse(cr, uid, ids):
155 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
156 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
158 def _open_task(self, cr, uid, ids, field_name, arg, context=None):
160 task_pool=self.pool.get('project.task')
162 task_ids = task_pool.search(cr, uid, [('project_id', '=', id)])
163 open_task[id] = len(task_ids)
166 def company_uom_id(self, cr, uid, ids, field_name, arg, context=None):
168 for project in self.browse(cr,uid,ids):
169 uom_company[project.id] = project.company_id.project_time_mode_id.name or "Hour"
174 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
175 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
176 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
177 '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),
178 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
179 'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive an email each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
181 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
182 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)]}),
183 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
184 '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.",
186 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
187 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
189 '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.",
191 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
192 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
194 '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.",
196 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
197 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
199 '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.",
201 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
202 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
204 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
205 '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)]}),
206 '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)]}),
207 '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)]}),
208 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
209 'task': fields.boolean('Task',help = "If you check this field tasks appears in kanban view"),
210 'open_task': fields.function(_open_task , type='integer',string="Open Tasks"),
211 'color': fields.integer('Color Index'),
212 'company_uom_id': fields.function(company_uom_id,type="char"),
214 def dummy(self, cr, uid, ids, context=None):
217 def open_tasks(self, cr, uid, ids, context=None):
218 #Open the View for the Tasks for the project
220 This opens Tasks views
221 @return :Dictionary value for task view
226 data_obj = self.pool.get('ir.model.data')
227 for project in self.browse(cr, uid, ids, context=context):
229 tree_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_tree2')
230 form_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_form2')
231 calander_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_calendar')
232 search_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_search_form')
233 kanban_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_kanban')
235 #'search_default_user_id': uid,
236 'search_default_project_id':project.id,
237 #'search_default_open':1,
243 'view_mode': 'form,tree',
244 'res_model': 'project.task',
246 'domain':[('project_id','in',ids)],
248 'views': [(kanban_view and kanban_view[1] or False, 'kanban'),(tree_view and tree_view[1] or False, 'tree'),(calander_view and calander_view[1] or False, 'calendar'),(form_view and form_view[1] or False, 'form')],
249 'type': 'ir.actions.act_window',
250 'search_view_id': search_view and search_view[1] or False,
255 def open_users(self, cr, uid, ids, context=None):
256 #Open the View for the Tasks for the project
258 This opens Tasks views
259 @return :Dictionary value for task view
264 data_obj = self.pool.get('ir.model.data')
265 for project in self.browse(cr, uid, ids, context=context):
267 tree_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_tree')
268 form_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_form')
269 search_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_search')
275 'view_mode': 'form,tree',
276 'res_model': 'res.users',
279 'res_id': project.user_id.id,
280 'views': [(form_view and form_view[1] or False, 'form'),(tree_view and tree_view[1] or False, 'tree')],
281 'type': 'ir.actions.act_window',
282 'search_view_id': search_view and search_view[1] or False,
287 def _get_type_common(self, cr, uid, context):
288 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
296 'type_ids': _get_type_common,
300 # TODO: Why not using a SQL contraints ?
301 def _check_dates(self, cr, uid, ids, context=None):
302 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
303 if leave['date_start'] and leave['date']:
304 if leave['date_start'] > leave['date']:
309 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
312 def set_template(self, cr, uid, ids, context=None):
313 res = self.setActive(cr, uid, ids, value=False, context=context)
316 def set_done(self, cr, uid, ids, context=None):
317 task_obj = self.pool.get('project.task')
318 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
319 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
320 self.write(cr, uid, ids, {'state':'close'}, context=context)
321 self.set_close_send_note(cr, uid, ids, context=context)
324 def set_cancel(self, cr, uid, ids, context=None):
325 task_obj = self.pool.get('project.task')
326 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
327 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
328 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
329 self.set_cancel_send_note(cr, uid, ids, context=context)
332 def set_pending(self, cr, uid, ids, context=None):
333 self.write(cr, uid, ids, {'state':'pending'}, context=context)
334 self.set_pending_send_note(cr, uid, ids, context=context)
337 def set_open(self, cr, uid, ids, context=None):
338 self.write(cr, uid, ids, {'state':'open'}, context=context)
339 self.set_open_send_note(cr, uid, ids, context=context)
342 def reset_project(self, cr, uid, ids, context=None):
343 res = self.setActive(cr, uid, ids, value=True, context=context)
344 self.set_open_send_note(cr, uid, ids, context=context)
347 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
348 """ copy and map tasks from old to new project """
352 task_obj = self.pool.get('project.task')
353 proj = self.browse(cr, uid, old_project_id, context=context)
354 for task in proj.tasks:
355 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
356 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
357 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
360 def copy(self, cr, uid, id, default={}, context=None):
364 default = default or {}
365 context['active_test'] = False
366 default['state'] = 'open'
367 default['tasks'] = []
368 proj = self.browse(cr, uid, id, context=context)
369 if not default.get('name', False):
370 default['name'] = proj.name + _(' (copy)')
372 res = super(project, self).copy(cr, uid, id, default, context)
373 self.map_tasks(cr,uid,id,res,context)
376 def duplicate_template(self, cr, uid, ids, context=None):
379 data_obj = self.pool.get('ir.model.data')
381 for proj in self.browse(cr, uid, ids, context=context):
382 parent_id = context.get('parent_id', False)
383 context.update({'analytic_project_copy': True})
384 new_date_start = time.strftime('%Y-%m-%d')
386 if proj.date_start and proj.date:
387 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
388 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
389 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
390 context.update({'copy':True})
391 new_id = self.copy(cr, uid, proj.id, default = {
392 'name': proj.name +_(' (copy)'),
394 'date_start':new_date_start,
396 'parent_id':parent_id}, context=context)
397 result.append(new_id)
399 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
400 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
402 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
404 if result and len(result):
406 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
407 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
408 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
409 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
410 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
411 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
413 'name': _('Projects'),
415 'view_mode': 'form,tree',
416 'res_model': 'project.project',
419 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
420 'type': 'ir.actions.act_window',
421 'search_view_id': search_view['res_id'],
425 # set active value for a project, its sub projects and its tasks
426 def setActive(self, cr, uid, ids, value=True, context=None):
427 task_obj = self.pool.get('project.task')
428 for proj in self.browse(cr, uid, ids, context=None):
429 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
430 cr.execute('select id from project_task where project_id=%s', (proj.id,))
431 tasks_id = [x[0] for x in cr.fetchall()]
433 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
434 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
436 self.setActive(cr, uid, child_ids, value, context=None)
439 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
440 context = context or {}
441 if type(ids) in (long, int,):
443 projects = self.browse(cr, uid, ids, context=context)
445 for project in projects:
446 if (not project.members) and force_members:
447 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
449 resource_pool = self.pool.get('resource.resource')
451 result = "from openerp.addons.resource.faces import *\n"
452 result += "import datetime\n"
453 for project in self.browse(cr, uid, ids, context=context):
454 u_ids = [i.id for i in project.members]
455 if project.user_id and (project.user_id.id not in u_ids):
456 u_ids.append(project.user_id.id)
457 for task in project.tasks:
458 if task.state in ('done','cancelled'):
460 if task.user_id and (task.user_id.id not in u_ids):
461 u_ids.append(task.user_id.id)
462 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
463 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
464 for key, vals in resource_objs.items():
466 class User_%s(Resource):
468 ''' % (key, vals.get('efficiency', False))
475 def _schedule_project(self, cr, uid, project, context=None):
476 resource_pool = self.pool.get('resource.resource')
477 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
478 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
479 # TODO: check if we need working_..., default values are ok.
480 puids = [x.id for x in project.members]
482 puids.append(project.user_id.id)
490 project.date_start, working_days,
491 '|'.join(['User_'+str(x) for x in puids])
493 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
500 #TODO: DO Resource allocation and compute availability
501 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
507 def schedule_tasks(self, cr, uid, ids, context=None):
508 context = context or {}
509 if type(ids) in (long, int,):
511 projects = self.browse(cr, uid, ids, context=context)
512 result = self._schedule_header(cr, uid, ids, False, context=context)
513 for project in projects:
514 result += self._schedule_project(cr, uid, project, context=context)
515 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
518 exec result in local_dict
519 projects_gantt = Task.BalancedProject(local_dict['Project'])
521 for project in projects:
522 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
523 for task in project.tasks:
524 if task.state in ('done','cancelled'):
527 p = getattr(project_gantt, 'Task_%d' % (task.id,))
529 self.pool.get('project.task').write(cr, uid, [task.id], {
530 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
531 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
533 if (not task.user_id) and (p.booked_resource):
534 self.pool.get('project.task').write(cr, uid, [task.id], {
535 'user_id': int(p.booked_resource[0].name[5:]),
539 # ------------------------------------------------
540 # OpenChatter methods and notifications
541 # ------------------------------------------------
543 def get_needaction_user_ids(self, cr, uid, ids, context=None):
544 result = dict.fromkeys(ids)
545 for obj in self.browse(cr, uid, ids, context=context):
547 if obj.state == 'draft' and obj.user_id:
548 result[obj.id] = [obj.user_id.id]
551 def message_get_subscribers(self, cr, uid, ids, context=None):
552 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
553 for obj in self.browse(cr, uid, ids, context=context):
555 sub_ids.append(obj.user_id.id)
556 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
558 def create(self, cr, uid, vals, context=None):
559 obj_id = super(project, self).create(cr, uid, vals, context=context)
560 self.create_send_note(cr, uid, [obj_id], context=context)
563 def create_send_note(self, cr, uid, ids, context=None):
564 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
566 def set_open_send_note(self, cr, uid, ids, context=None):
567 message = _("Project has been <b>opened</b>.")
568 return self.message_append_note(cr, uid, ids, body=message, context=context)
570 def set_pending_send_note(self, cr, uid, ids, context=None):
571 message = _("Project is now <b>pending</b>.")
572 return self.message_append_note(cr, uid, ids, body=message, context=context)
574 def set_cancel_send_note(self, cr, uid, ids, context=None):
575 message = _("Project has been <b>cancelled</b>.")
576 return self.message_append_note(cr, uid, ids, body=message, context=context)
578 def set_close_send_note(self, cr, uid, ids, context=None):
579 message = _("Project has been <b>closed</b>.")
580 return self.message_append_note(cr, uid, ids, body=message, context=context)
584 class users(osv.osv):
585 _inherit = 'res.users'
587 'context_project_id': fields.many2one('project.project', 'Project')
592 _name = "project.task"
593 _description = "Task"
595 _date_name = "date_start"
596 _inherit = ['ir.needaction_mixin', 'mail.thread']
599 def _resolve_project_id_from_context(self, cr, uid, context=None):
600 """Return ID of project based on the value of 'project_id'
601 context key, or None if it cannot be resolved to a single project.
603 if context is None: context = {}
604 if type(context.get('project_id')) in (int, long):
605 project_id = context['project_id']
607 if isinstance(context.get('project_id'), basestring):
608 project_name = context['project_id']
609 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
610 if len(project_ids) == 1:
611 return project_ids[0][0]
613 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
614 stage_obj = self.pool.get('project.task.type')
615 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
616 order = stage_obj._order
617 access_rights_uid = access_rights_uid or uid
618 if read_group_order == 'type_id desc':
619 # lame way to allow reverting search, should just work in the trivial case
620 order = '%s desc' % order
622 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
624 domain = ['|', ('id','in',ids), ('project_default','=',1)]
625 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
626 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
627 # restore order of the search
628 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
631 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
632 res_users = self.pool.get('res.users')
633 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
634 access_rights_uid = access_rights_uid or uid
636 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
637 order = res_users._order
638 # lame way to allow reverting search, should just work in the trivial case
639 if read_group_order == 'user_id desc':
640 order = '%s desc' % order
641 # de-duplicate and apply search order
642 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
643 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
644 # restore order of the search
645 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
649 'type_id': _read_group_type_id,
650 'user_id': _read_group_user_id
654 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
655 obj_project = self.pool.get('project.project')
657 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
658 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
659 if id and isinstance(id, (long, int)):
660 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
661 args.append(('active', '=', False))
662 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
664 def _str_get(self, task, level=0, border='***', context=None):
665 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'+ \
666 border[0]+' '+(task.name or '')+'\n'+ \
667 (task.description or '')+'\n\n'
669 # Compute: effective_hours, total_hours, progress
670 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
672 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
673 hours = dict(cr.fetchall())
674 for task in self.browse(cr, uid, ids, context=context):
675 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)}
676 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
677 res[task.id]['progress'] = 0.0
678 if (task.remaining_hours + hours.get(task.id, 0.0)):
679 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
680 if task.state in ('done','cancelled'):
681 res[task.id]['progress'] = 100.0
685 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
686 if remaining and not planned:
687 return {'value':{'planned_hours': remaining}}
690 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
691 return {'value':{'remaining_hours': planned - effective}}
693 def onchange_project(self, cr, uid, id, project_id):
696 data = self.pool.get('project.project').browse(cr, uid, [project_id])
697 partner_id=data and data[0].partner_id
699 return {'value':{'partner_id':partner_id.id}}
702 def duplicate_task(self, cr, uid, map_ids, context=None):
703 for new in map_ids.values():
704 task = self.browse(cr, uid, new, context)
705 child_ids = [ ch.id for ch in task.child_ids]
707 for child in task.child_ids:
708 if child.id in map_ids.keys():
709 child_ids.remove(child.id)
710 child_ids.append(map_ids[child.id])
712 parent_ids = [ ch.id for ch in task.parent_ids]
714 for parent in task.parent_ids:
715 if parent.id in map_ids.keys():
716 parent_ids.remove(parent.id)
717 parent_ids.append(map_ids[parent.id])
718 #FIXME why there is already the copy and the old one
719 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
721 def copy_data(self, cr, uid, id, default={}, context=None):
722 default = default or {}
723 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
724 if not default.get('remaining_hours', False):
725 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
726 default['active'] = True
727 default['type_id'] = False
728 if not default.get('name', False):
729 default['name'] = self.browse(cr, uid, id, context=context).name or ''
730 if not context.get('copy',False):
731 new_name = _("%s (copy)")%default.get('name','')
732 default.update({'name':new_name})
733 return super(task, self).copy_data(cr, uid, id, default, context)
736 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
738 for task in self.browse(cr, uid, ids, context=context):
741 if task.project_id.active == False or task.project_id.state == 'template':
745 def _get_task(self, cr, uid, ids, context=None):
747 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
748 if work.task_id: result[work.task_id.id] = True
752 '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."),
753 'name': fields.char('Task Summary', size=128, required=True, select=True),
754 'description': fields.text('Description'),
755 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
756 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
757 'type_id': fields.many2one('project.task.type', 'Stage'),
758 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
759 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.\
760 \n If the task is over, the states is set to \'Done\'.'),
761 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
762 help="A task's kanban state indicates special situations affecting it:\n"
763 " * Normal is the default situation\n"
764 " * Blocked indicates something is preventing the progress of this task\n"
765 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
766 readonly=True, required=False),
767 'create_date': fields.datetime('Create Date', readonly=True,select=True),
768 'date_start': fields.datetime('Starting Date',select=True),
769 'date_end': fields.datetime('Ending Date',select=True),
770 'date_deadline': fields.date('Deadline',select=True),
771 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
772 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
773 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
774 'notes': fields.text('Notes'),
775 '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.'),
776 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
778 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
779 'project.task.work': (_get_task, ['hours'], 10),
781 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
782 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
784 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
785 'project.task.work': (_get_task, ['hours'], 10),
787 '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",
789 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
790 'project.task.work': (_get_task, ['hours'], 10),
792 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference between planned hours by the project manager and the total hours of the task.",
794 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
795 'project.task.work': (_get_task, ['hours'], 10),
797 'user_id': fields.many2one('res.users', 'Assigned to'),
798 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
799 'partner_id': fields.many2one('res.partner', 'Partner'),
800 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
801 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
802 'company_id': fields.many2one('res.company', 'Company'),
803 'id': fields.integer('ID', readonly=True),
804 'color': fields.integer('Color Index'),
805 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
810 'kanban_state': 'normal',
815 'user_id': lambda obj, cr, uid, context: uid,
816 'project_id':lambda self, cr, uid, context: context.get('active_id',False),
817 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
820 _order = "priority, sequence, date_start, name, id"
822 def set_priority(self, cr, uid, ids, priority):
825 return self.write(cr, uid, ids, {'priority' : priority})
827 def set_high_priority(self, cr, uid, ids, *args):
828 """Set task priority to high
830 return self.set_priority(cr, uid, ids, '1')
832 def set_normal_priority(self, cr, uid, ids, *args):
833 """Set task priority to normal
835 return self.set_priority(cr, uid, ids, '2')
837 def _check_recursion(self, cr, uid, ids, context=None):
839 visited_branch = set()
841 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
847 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
848 if id in visited_branch: #Cycle
851 if id in visited_node: #Already tested don't work one more time for nothing
854 visited_branch.add(id)
857 #visit child using DFS
858 task = self.browse(cr, uid, id, context=context)
859 for child in task.child_ids:
860 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
864 visited_branch.remove(id)
867 def _check_dates(self, cr, uid, ids, context=None):
870 obj_task = self.browse(cr, uid, ids[0], context=context)
871 start = obj_task.date_start or False
872 end = obj_task.date_end or False
879 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
880 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
883 # Override view according to the company definition
885 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
886 users_obj = self.pool.get('res.users')
888 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
889 # this should be safe (no context passed to avoid side-effects)
890 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
891 tm = obj_tm and obj_tm.name or 'Hours'
893 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
895 if tm in ['Hours','Hour']:
898 eview = etree.fromstring(res['arch'])
900 def _check_rec(eview):
901 if eview.attrib.get('widget','') == 'float_time':
902 eview.set('widget','float')
909 res['arch'] = etree.tostring(eview)
911 for f in res['fields']:
912 if 'Hours' in res['fields'][f]['string']:
913 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
916 def _check_child_task(self, cr, uid, ids, context=None):
919 tasks = self.browse(cr, uid, ids, context=context)
922 for child in task.child_ids:
923 if child.state in ['draft', 'open', 'pending']:
924 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
927 def action_close(self, cr, uid, ids, context=None):
928 # This action open wizard to send email to partner or project manager after close task.
931 task_id = len(ids) and ids[0] or False
932 self._check_child_task(cr, uid, ids, context=context)
933 if not task_id: return False
934 task = self.browse(cr, uid, task_id, context=context)
935 project = task.project_id
936 res = self.do_close(cr, uid, [task_id], context=context)
937 if project.warn_manager or project.warn_customer:
939 'name': _('Send Email after close task'),
942 'res_model': 'mail.compose.message',
943 'type': 'ir.actions.act_window',
946 'context': {'active_id': task.id,
947 'active_model': 'project.task'}
951 def do_close(self, cr, uid, ids, context={}):
955 request = self.pool.get('res.request')
956 if not isinstance(ids,list): ids = [ids]
957 for task in self.browse(cr, uid, ids, context=context):
959 project = task.project_id
961 # Send request to project manager
962 if project.warn_manager and project.user_id and (project.user_id.id != uid):
963 request.create(cr, uid, {
964 'name': _("Task '%s' closed") % task.name,
967 'act_to': project.user_id.id,
968 'ref_partner_id': task.partner_id.id,
969 'ref_doc1': 'project.task,%d'% (task.id,),
970 'ref_doc2': 'project.project,%d'% (project.id,),
973 for parent_id in task.parent_ids:
974 if parent_id.state in ('pending','draft'):
976 for child in parent_id.child_ids:
977 if child.id != task.id and child.state not in ('done','cancelled'):
980 self.do_reopen(cr, uid, [parent_id.id], context=context)
981 vals.update({'state': 'done'})
982 vals.update({'remaining_hours': 0.0})
983 if not task.date_end:
984 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
985 self.write(cr, uid, [task.id],vals, context=context)
986 self.do_close_send_note(cr, uid, [task.id], context)
989 def do_reopen(self, cr, uid, ids, context=None):
990 request = self.pool.get('res.request')
992 for task in self.browse(cr, uid, ids, context=context):
993 project = task.project_id
994 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
995 request.create(cr, uid, {
996 'name': _("Task '%s' set in progress") % task.name,
999 'act_to': project.user_id.id,
1000 'ref_partner_id': task.partner_id.id,
1001 'ref_doc1': 'project.task,%d' % task.id,
1002 'ref_doc2': 'project.project,%d' % project.id,
1005 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
1006 self.do_open_send_note(cr, uid, [task.id], context)
1009 def do_cancel(self, cr, uid, ids, context={}):
1010 request = self.pool.get('res.request')
1011 tasks = self.browse(cr, uid, ids, context=context)
1012 self._check_child_task(cr, uid, ids, context=context)
1014 project = task.project_id
1015 if project.warn_manager and project.user_id and (project.user_id.id != uid):
1016 request.create(cr, uid, {
1017 'name': _("Task '%s' cancelled") % task.name,
1020 'act_to': project.user_id.id,
1021 'ref_partner_id': task.partner_id.id,
1022 'ref_doc1': 'project.task,%d' % task.id,
1023 'ref_doc2': 'project.project,%d' % project.id,
1025 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
1026 self.do_cancel_send_note(cr, uid, [task.id], context)
1029 def do_open(self, cr, uid, ids, context={}):
1030 if not isinstance(ids,list): ids = [ids]
1031 tasks= self.browse(cr, uid, ids, context=context)
1033 data = {'state': 'open'}
1034 if not t.date_start:
1035 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
1036 self.write(cr, uid, [t.id], data, context=context)
1037 self.do_open_send_note(cr, uid, [t.id], context)
1040 def do_draft(self, cr, uid, ids, context={}):
1041 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
1042 self.do_draft_send_note(cr, uid, ids, context)
1046 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1047 attachment = self.pool.get('ir.attachment')
1048 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1049 new_attachment_ids = []
1050 for attachment_id in attachment_ids:
1051 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1052 return new_attachment_ids
1055 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1057 Delegate Task to another users.
1059 assert delegate_data['user_id'], _("Delegated User should be specified")
1060 delegated_tasks = {}
1061 for task in self.browse(cr, uid, ids, context=context):
1062 delegated_task_id = self.copy(cr, uid, task.id, {
1063 'name': delegate_data['name'],
1064 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1065 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1066 'planned_hours': delegate_data['planned_hours'] or 0.0,
1067 'parent_ids': [(6, 0, [task.id])],
1069 'description': delegate_data['new_task_description'] or '',
1073 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1074 newname = delegate_data['prefix'] or ''
1076 'remaining_hours': delegate_data['planned_hours_me'],
1077 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1080 if delegate_data['state'] == 'pending':
1081 self.do_pending(cr, uid, [task.id], context=context)
1082 elif delegate_data['state'] == 'done':
1083 self.do_close(cr, uid, [task.id], context=context)
1084 self.do_delegation_send_note(cr, uid, [task.id], context)
1085 delegated_tasks[task.id] = delegated_task_id
1086 return delegated_tasks
1088 def do_pending(self, cr, uid, ids, context={}):
1089 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1090 self.do_pending_send_note(cr, uid, ids, context)
1093 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1094 for task in self.browse(cr, uid, ids, context=context):
1095 if (task.state=='draft') or (task.planned_hours==0.0):
1096 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1097 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1100 def set_remaining_time_1(self, cr, uid, ids, context=None):
1101 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1103 def set_remaining_time_2(self, cr, uid, ids, context=None):
1104 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1106 def set_remaining_time_5(self, cr, uid, ids, context=None):
1107 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1109 def set_remaining_time_10(self, cr, uid, ids, context=None):
1110 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1112 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1113 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1115 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1116 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1118 def set_kanban_state_done(self, cr, uid, ids, context=None):
1119 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1121 def _change_type(self, cr, uid, ids, next, context=None):
1123 go to the next stage
1124 if next is False, go to previous stage
1126 for task in self.browse(cr, uid, ids):
1127 if task.project_id.type_ids:
1128 typeid = task.type_id.id
1130 for type in task.project_id.type_ids :
1131 types_seq[type.id] = type.sequence
1133 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1135 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1136 sorted_types = [x[0] for x in types]
1138 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1139 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1140 index = sorted_types.index(typeid)
1141 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1142 self.state_change_send_note(cr, uid, [task.id], context)
1145 def next_type(self, cr, uid, ids, context=None):
1146 return self._change_type(cr, uid, ids, True, context=context)
1148 def prev_type(self, cr, uid, ids, context=None):
1149 return self._change_type(cr, uid, ids, False, context=context)
1151 def _store_history(self, cr, uid, ids, context=None):
1152 for task in self.browse(cr, uid, ids, context=context):
1153 self.pool.get('project.task.history').create(cr, uid, {
1155 'remaining_hours': task.remaining_hours,
1156 'planned_hours': task.planned_hours,
1157 'kanban_state': task.kanban_state,
1158 'type_id': task.type_id.id,
1159 'state': task.state,
1160 'user_id': task.user_id.id
1165 def create(self, cr, uid, vals, context=None):
1166 task_id = super(task, self).create(cr, uid, vals, context=context)
1167 self._store_history(cr, uid, [task_id], context=context)
1168 self.create_send_note(cr, uid, [task_id], context=context)
1171 # Overridden to reset the kanban_state to normal whenever
1172 # the stage (type_id) of the task changes.
1173 def write(self, cr, uid, ids, vals, context=None):
1174 if isinstance(ids, (int, long)):
1176 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1177 new_stage = vals.get('type_id')
1178 vals_reset_kstate = dict(vals, kanban_state='normal')
1179 for t in self.browse(cr, uid, ids, context=context):
1180 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1181 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1184 result = super(task,self).write(cr, uid, ids, vals, context=context)
1185 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1186 self._store_history(cr, uid, ids, context=context)
1187 self.state_change_send_note(cr, uid, ids, context)
1190 def unlink(self, cr, uid, ids, context=None):
1193 self._check_child_task(cr, uid, ids, context=context)
1194 res = super(task, self).unlink(cr, uid, ids, context)
1197 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1198 context = context or {}
1202 if task.state in ('done','cancelled'):
1207 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1209 for t2 in task.parent_ids:
1210 start.append("up.Task_%s.end" % (t2.id,))
1214 ''' % (ident,','.join(start))
1219 ''' % (ident, 'User_'+str(task.user_id.id))
1224 # ---------------------------------------------------
1225 # OpenChatter methods and notifications
1226 # ---------------------------------------------------
1228 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1229 result = dict.fromkeys(ids, [])
1230 for obj in self.browse(cr, uid, ids, context=context):
1231 if obj.state == 'draft' and obj.user_id:
1232 result[obj.id] = [obj.user_id.id]
1235 def message_get_subscribers(self, cr, uid, ids, context=None):
1236 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1237 for obj in self.browse(cr, uid, ids, context=context):
1239 sub_ids.append(obj.user_id.id)
1241 sub_ids.append(obj.manager_id.id)
1242 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1244 def create_send_note(self, cr, uid, ids, context=None):
1245 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1247 def do_pending_send_note(self, cr, uid, ids, context=None):
1248 if not isinstance(ids,list): ids = [ids]
1249 msg = _('Task is now <b>pending</b>.')
1250 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1252 def do_open_send_note(self, cr, uid, ids, context=None):
1253 msg = _('Task has been <b>opened</b>.')
1254 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1256 def do_cancel_send_note(self, cr, uid, ids, context=None):
1257 msg = _('Task has been <b>canceled</b>.')
1258 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1260 def do_close_send_note(self, cr, uid, ids, context=None):
1261 msg = _('Task has been <b>closed</b>.')
1262 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1264 def do_draft_send_note(self, cr, uid, ids, context=None):
1265 msg = _('Task has been <b>renewed</b>.')
1266 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1268 def do_delegation_send_note(self, cr, uid, ids, context=None):
1269 for task in self.browse(cr, uid, ids, context=context):
1270 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1271 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1274 def state_change_send_note(self, cr, uid, ids, context=None):
1275 for task in self.browse(cr, uid, ids, context=context):
1276 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1277 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1282 class project_work(osv.osv):
1283 _name = "project.task.work"
1284 _description = "Project Task Work"
1286 'name': fields.char('Work summary', size=128),
1287 'date': fields.datetime('Date', select="1"),
1288 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1289 'hours': fields.float('Time Spent'),
1290 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1291 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1295 'user_id': lambda obj, cr, uid, context: uid,
1296 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1299 _order = "date desc"
1300 def create(self, cr, uid, vals, *args, **kwargs):
1301 if 'hours' in vals and (not vals['hours']):
1302 vals['hours'] = 0.00
1303 if 'task_id' in vals:
1304 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1305 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1307 def write(self, cr, uid, ids, vals, context=None):
1308 if 'hours' in vals and (not vals['hours']):
1309 vals['hours'] = 0.00
1311 for work in self.browse(cr, uid, ids, context=context):
1312 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))
1313 return super(project_work,self).write(cr, uid, ids, vals, context)
1315 def unlink(self, cr, uid, ids, *args, **kwargs):
1316 for work in self.browse(cr, uid, ids):
1317 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1318 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1321 class account_analytic_account(osv.osv):
1323 _inherit = 'account.analytic.account'
1324 _description = 'Analytic Account'
1326 def create(self, cr, uid, vals, context=None):
1329 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1330 vals['child_ids'] = []
1331 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1333 def unlink(self, cr, uid, ids, *args, **kwargs):
1334 project_obj = self.pool.get('project.project')
1335 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1337 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1338 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1340 account_analytic_account()
1343 # Tasks History, used for cumulative flow charts (Lean/Agile)
1346 class project_task_history(osv.osv):
1347 _name = 'project.task.history'
1348 _description = 'History of Tasks'
1349 _rec_name = 'task_id'
1351 def _get_date(self, cr, uid, ids, name, arg, context=None):
1353 for history in self.browse(cr, uid, ids, context=context):
1354 if history.state in ('done','cancelled'):
1355 result[history.id] = history.date
1357 cr.execute('''select
1360 project_task_history
1364 order by id limit 1''', (history.task_id.id, history.id))
1366 result[history.id] = res and res[0] or False
1369 def _get_related_date(self, cr, uid, ids, context=None):
1371 for history in self.browse(cr, uid, ids, context=context):
1372 cr.execute('''select
1375 project_task_history
1379 order by id desc limit 1''', (history.task_id.id, history.id))
1382 result.append(res[0])
1386 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1387 'type_id': fields.many2one('project.task.type', 'Stage'),
1388 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1389 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1390 'date': fields.date('Date', select=True),
1391 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1392 'project.task.history': (_get_related_date, None, 20)
1394 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1395 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1396 'user_id': fields.many2one('res.users', 'Responsible'),
1399 'date': fields.date.context_today,
1401 project_task_history()
1403 class project_task_history_cumulative(osv.osv):
1404 _name = 'project.task.history.cumulative'
1405 _table = 'project_task_history_cumulative'
1406 _inherit = 'project.task.history'
1409 'end_date': fields.date('End Date'),
1410 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1413 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1415 history.date::varchar||'-'||history.history_id::varchar as id,
1416 history.date as end_date,
1421 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1422 task_id, type_id, user_id, kanban_state, state,
1423 remaining_hours, planned_hours
1425 project_task_history
1429 project_task_history_cumulative()