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)
167 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
168 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
169 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
170 '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),
171 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
172 '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)]}),
174 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
175 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)]}),
176 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
177 '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.",
179 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
180 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
182 '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.",
184 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
185 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
187 '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.",
189 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
190 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
192 '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.",
194 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
195 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
197 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
198 '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)]}),
199 '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)]}),
200 '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)]}),
201 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
202 'task': fields.boolean('Task',help = "If you check this field tasks appears in kanban view"),
203 'open_task': fields.function(_open_task , type='integer',string="Open Tasks"),
204 'color': fields.integer('Color Index'),
206 def dummy(self, cr, uid, ids, context=None):
209 def open_tasks(self, cr, uid, ids, context=None):
210 #Open the View for the Tasks for the project
212 This opens Tasks views
213 @return :Dictionary value for task view
218 data_obj = self.pool.get('ir.model.data')
219 for project in self.browse(cr, uid, ids, context=context):
221 tree_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_tree2')
222 form_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_form2')
223 calander_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_calendar')
224 search_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_search_form')
225 kanban_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_kanban')
227 #'search_default_user_id': uid,
228 'search_default_project_id':project.id,
229 #'search_default_open':1,
235 'view_mode': 'form,tree',
236 'res_model': 'project.task',
238 'domain':[('project_id','in',ids)],
240 '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')],
241 'type': 'ir.actions.act_window',
242 'search_view_id': search_view and search_view[1] or False,
247 def open_users(self, cr, uid, ids, context=None):
248 #Open the View for the Tasks for the project
250 This opens Tasks views
251 @return :Dictionary value for task view
256 data_obj = self.pool.get('ir.model.data')
257 for project in self.browse(cr, uid, ids, context=context):
259 tree_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_tree')
260 form_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_form')
261 search_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_search')
267 'view_mode': 'form,tree',
268 'res_model': 'res.users',
271 'res_id': project.user_id.id,
272 'views': [(form_view and form_view[1] or False, 'form'),(tree_view and tree_view[1] or False, 'tree')],
273 'type': 'ir.actions.act_window',
274 'search_view_id': search_view and search_view[1] or False,
279 def _get_type_common(self, cr, uid, context):
280 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
288 'type_ids': _get_type_common,
292 # TODO: Why not using a SQL contraints ?
293 def _check_dates(self, cr, uid, ids, context=None):
294 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
295 if leave['date_start'] and leave['date']:
296 if leave['date_start'] > leave['date']:
301 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
304 def set_template(self, cr, uid, ids, context=None):
305 res = self.setActive(cr, uid, ids, value=False, context=context)
308 def set_done(self, cr, uid, ids, context=None):
309 task_obj = self.pool.get('project.task')
310 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
311 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
312 self.write(cr, uid, ids, {'state':'close'}, context=context)
313 self.set_close_send_note(cr, uid, ids, context=context)
316 def set_cancel(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', '!=', 'done')])
319 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
320 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
321 self.set_cancel_send_note(cr, uid, ids, context=context)
324 def set_pending(self, cr, uid, ids, context=None):
325 self.write(cr, uid, ids, {'state':'pending'}, context=context)
326 self.set_pending_send_note(cr, uid, ids, context=context)
329 def set_open(self, cr, uid, ids, context=None):
330 self.write(cr, uid, ids, {'state':'open'}, context=context)
331 self.set_open_send_note(cr, uid, ids, context=context)
334 def reset_project(self, cr, uid, ids, context=None):
335 res = self.setActive(cr, uid, ids, value=True, context=context)
336 self.set_open_send_note(cr, uid, ids, context=context)
339 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
340 """ copy and map tasks from old to new project """
344 task_obj = self.pool.get('project.task')
345 proj = self.browse(cr, uid, old_project_id, context=context)
346 for task in proj.tasks:
347 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
348 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
349 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
352 def copy(self, cr, uid, id, default={}, context=None):
356 default = default or {}
357 context['active_test'] = False
358 default['state'] = 'open'
359 default['tasks'] = []
360 proj = self.browse(cr, uid, id, context=context)
361 if not default.get('name', False):
362 default['name'] = proj.name + _(' (copy)')
364 res = super(project, self).copy(cr, uid, id, default, context)
365 self.map_tasks(cr,uid,id,res,context)
368 def duplicate_template(self, cr, uid, ids, context=None):
371 data_obj = self.pool.get('ir.model.data')
373 for proj in self.browse(cr, uid, ids, context=context):
374 parent_id = context.get('parent_id', False)
375 context.update({'analytic_project_copy': True})
376 new_date_start = time.strftime('%Y-%m-%d')
378 if proj.date_start and proj.date:
379 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
380 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
381 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
382 context.update({'copy':True})
383 new_id = self.copy(cr, uid, proj.id, default = {
384 'name': proj.name +_(' (copy)'),
386 'date_start':new_date_start,
388 'parent_id':parent_id}, context=context)
389 result.append(new_id)
391 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
392 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
394 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
396 if result and len(result):
398 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
399 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
400 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
401 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
402 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
403 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
405 'name': _('Projects'),
407 'view_mode': 'form,tree',
408 'res_model': 'project.project',
411 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
412 'type': 'ir.actions.act_window',
413 'search_view_id': search_view['res_id'],
417 # set active value for a project, its sub projects and its tasks
418 def setActive(self, cr, uid, ids, value=True, context=None):
419 task_obj = self.pool.get('project.task')
420 for proj in self.browse(cr, uid, ids, context=None):
421 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
422 cr.execute('select id from project_task where project_id=%s', (proj.id,))
423 tasks_id = [x[0] for x in cr.fetchall()]
425 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
426 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
428 self.setActive(cr, uid, child_ids, value, context=None)
431 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
432 context = context or {}
433 if type(ids) in (long, int,):
435 projects = self.browse(cr, uid, ids, context=context)
437 for project in projects:
438 if (not project.members) and force_members:
439 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
441 resource_pool = self.pool.get('resource.resource')
443 result = "from openerp.addons.resource.faces import *\n"
444 result += "import datetime\n"
445 for project in self.browse(cr, uid, ids, context=context):
446 u_ids = [i.id for i in project.members]
447 if project.user_id and (project.user_id.id not in u_ids):
448 u_ids.append(project.user_id.id)
449 for task in project.tasks:
450 if task.state in ('done','cancelled'):
452 if task.user_id and (task.user_id.id not in u_ids):
453 u_ids.append(task.user_id.id)
454 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
455 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
456 for key, vals in resource_objs.items():
458 class User_%s(Resource):
460 ''' % (key, vals.get('efficiency', False))
467 def _schedule_project(self, cr, uid, project, context=None):
468 resource_pool = self.pool.get('resource.resource')
469 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
470 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
471 # TODO: check if we need working_..., default values are ok.
472 puids = [x.id for x in project.members]
474 puids.append(project.user_id.id)
482 project.date_start, working_days,
483 '|'.join(['User_'+str(x) for x in puids])
485 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
492 #TODO: DO Resource allocation and compute availability
493 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
499 def schedule_tasks(self, cr, uid, ids, context=None):
500 context = context or {}
501 if type(ids) in (long, int,):
503 projects = self.browse(cr, uid, ids, context=context)
504 result = self._schedule_header(cr, uid, ids, False, context=context)
505 for project in projects:
506 result += self._schedule_project(cr, uid, project, context=context)
507 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
510 exec result in local_dict
511 projects_gantt = Task.BalancedProject(local_dict['Project'])
513 for project in projects:
514 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
515 for task in project.tasks:
516 if task.state in ('done','cancelled'):
519 p = getattr(project_gantt, 'Task_%d' % (task.id,))
521 self.pool.get('project.task').write(cr, uid, [task.id], {
522 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
523 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
525 if (not task.user_id) and (p.booked_resource):
526 self.pool.get('project.task').write(cr, uid, [task.id], {
527 'user_id': int(p.booked_resource[0].name[5:]),
531 # ------------------------------------------------
532 # OpenChatter methods and notifications
533 # ------------------------------------------------
535 def get_needaction_user_ids(self, cr, uid, ids, context=None):
536 result = dict.fromkeys(ids)
537 for obj in self.browse(cr, uid, ids, context=context):
539 if obj.state == 'draft' and obj.user_id:
540 result[obj.id] = [obj.user_id.id]
543 def message_get_subscribers(self, cr, uid, ids, context=None):
544 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
545 for obj in self.browse(cr, uid, ids, context=context):
547 sub_ids.append(obj.user_id.id)
548 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
550 def create(self, cr, uid, vals, context=None):
551 obj_id = super(project, self).create(cr, uid, vals, context=context)
552 self.create_send_note(cr, uid, [obj_id], context=context)
555 def create_send_note(self, cr, uid, ids, context=None):
556 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
558 def set_open_send_note(self, cr, uid, ids, context=None):
559 message = _("Project has been <b>opened</b>.")
560 return self.message_append_note(cr, uid, ids, body=message, context=context)
562 def set_pending_send_note(self, cr, uid, ids, context=None):
563 message = _("Project is now <b>pending</b>.")
564 return self.message_append_note(cr, uid, ids, body=message, context=context)
566 def set_cancel_send_note(self, cr, uid, ids, context=None):
567 message = _("Project has been <b>cancelled</b>.")
568 return self.message_append_note(cr, uid, ids, body=message, context=context)
570 def set_close_send_note(self, cr, uid, ids, context=None):
571 message = _("Project has been <b>closed</b>.")
572 return self.message_append_note(cr, uid, ids, body=message, context=context)
576 class users(osv.osv):
577 _inherit = 'res.users'
579 'context_project_id': fields.many2one('project.project', 'Project')
584 _name = "project.task"
585 _description = "Task"
587 _date_name = "date_start"
588 _inherit = ['ir.needaction_mixin', 'mail.thread']
591 def _resolve_project_id_from_context(self, cr, uid, context=None):
592 """Return ID of project based on the value of 'project_id'
593 context key, or None if it cannot be resolved to a single project.
595 if context is None: context = {}
596 if type(context.get('project_id')) in (int, long):
597 project_id = context['project_id']
599 if isinstance(context.get('project_id'), basestring):
600 project_name = context['project_id']
601 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
602 if len(project_ids) == 1:
603 return project_ids[0][0]
605 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
606 stage_obj = self.pool.get('project.task.type')
607 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
608 order = stage_obj._order
609 access_rights_uid = access_rights_uid or uid
610 if read_group_order == 'type_id desc':
611 # lame way to allow reverting search, should just work in the trivial case
612 order = '%s desc' % order
614 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
616 domain = ['|', ('id','in',ids), ('project_default','=',1)]
617 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
618 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
619 # restore order of the search
620 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
623 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
624 res_users = self.pool.get('res.users')
625 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
626 access_rights_uid = access_rights_uid or uid
628 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
629 order = res_users._order
630 # lame way to allow reverting search, should just work in the trivial case
631 if read_group_order == 'user_id desc':
632 order = '%s desc' % order
633 # de-duplicate and apply search order
634 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
635 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
636 # restore order of the search
637 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
641 'type_id': _read_group_type_id,
642 'user_id': _read_group_user_id
646 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
647 obj_project = self.pool.get('project.project')
649 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
650 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
651 if id and isinstance(id, (long, int)):
652 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
653 args.append(('active', '=', False))
654 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
656 def _str_get(self, task, level=0, border='***', context=None):
657 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'+ \
658 border[0]+' '+(task.name or '')+'\n'+ \
659 (task.description or '')+'\n\n'
661 # Compute: effective_hours, total_hours, progress
662 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
664 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
665 hours = dict(cr.fetchall())
666 for task in self.browse(cr, uid, ids, context=context):
667 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)}
668 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
669 res[task.id]['progress'] = 0.0
670 if (task.remaining_hours + hours.get(task.id, 0.0)):
671 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
672 if task.state in ('done','cancelled'):
673 res[task.id]['progress'] = 100.0
677 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
678 if remaining and not planned:
679 return {'value':{'planned_hours': remaining}}
682 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
683 return {'value':{'remaining_hours': planned - effective}}
685 def onchange_project(self, cr, uid, id, project_id):
688 data = self.pool.get('project.project').browse(cr, uid, [project_id])
689 partner_id=data and data[0].partner_id
691 return {'value':{'partner_id':partner_id.id}}
694 def duplicate_task(self, cr, uid, map_ids, context=None):
695 for new in map_ids.values():
696 task = self.browse(cr, uid, new, context)
697 child_ids = [ ch.id for ch in task.child_ids]
699 for child in task.child_ids:
700 if child.id in map_ids.keys():
701 child_ids.remove(child.id)
702 child_ids.append(map_ids[child.id])
704 parent_ids = [ ch.id for ch in task.parent_ids]
706 for parent in task.parent_ids:
707 if parent.id in map_ids.keys():
708 parent_ids.remove(parent.id)
709 parent_ids.append(map_ids[parent.id])
710 #FIXME why there is already the copy and the old one
711 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
713 def copy_data(self, cr, uid, id, default={}, context=None):
714 default = default or {}
715 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
716 if not default.get('remaining_hours', False):
717 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
718 default['active'] = True
719 default['type_id'] = False
720 if not default.get('name', False):
721 default['name'] = self.browse(cr, uid, id, context=context).name or ''
722 if not context.get('copy',False):
723 new_name = _("%s (copy)")%default.get('name','')
724 default.update({'name':new_name})
725 return super(task, self).copy_data(cr, uid, id, default, context)
728 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
730 for task in self.browse(cr, uid, ids, context=context):
733 if task.project_id.active == False or task.project_id.state == 'template':
737 def _get_task(self, cr, uid, ids, context=None):
739 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
740 if work.task_id: result[work.task_id.id] = True
744 '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."),
745 'name': fields.char('Task Summary', size=128, required=True, select=True),
746 'description': fields.text('Description'),
747 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
748 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
749 'type_id': fields.many2one('project.task.type', 'Stage'),
750 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
751 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.\
752 \n If the task is over, the states is set to \'Done\'.'),
753 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
754 help="A task's kanban state indicates special situations affecting it:\n"
755 " * Normal is the default situation\n"
756 " * Blocked indicates something is preventing the progress of this task\n"
757 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
758 readonly=True, required=False),
759 'create_date': fields.datetime('Create Date', readonly=True,select=True),
760 'date_start': fields.datetime('Starting Date',select=True),
761 'date_end': fields.datetime('Ending Date',select=True),
762 'date_deadline': fields.date('Deadline',select=True),
763 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
764 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
765 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
766 'notes': fields.text('Notes'),
767 '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.'),
768 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
770 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
771 'project.task.work': (_get_task, ['hours'], 10),
773 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
774 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
776 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
777 'project.task.work': (_get_task, ['hours'], 10),
779 '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",
781 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
782 'project.task.work': (_get_task, ['hours'], 10),
784 '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.",
786 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
787 'project.task.work': (_get_task, ['hours'], 10),
789 'user_id': fields.many2one('res.users', 'Assigned to'),
790 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
791 'partner_id': fields.many2one('res.partner', 'Partner'),
792 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
793 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
794 'company_id': fields.many2one('res.company', 'Company'),
795 'id': fields.integer('ID', readonly=True),
796 'color': fields.integer('Color Index'),
797 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
802 'kanban_state': 'normal',
807 'user_id': lambda obj, cr, uid, context: uid,
808 'project_id':lambda self, cr, uid, context: context.get('active_id',False),
809 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
812 _order = "priority, sequence, date_start, name, id"
814 def set_priority(self, cr, uid, ids, priority):
817 return self.write(cr, uid, ids, {'priority' : priority})
819 def set_high_priority(self, cr, uid, ids, *args):
820 """Set task priority to high
822 return self.set_priority(cr, uid, ids, '1')
824 def set_normal_priority(self, cr, uid, ids, *args):
825 """Set task priority to normal
827 return self.set_priority(cr, uid, ids, '2')
829 def _check_recursion(self, cr, uid, ids, context=None):
831 visited_branch = set()
833 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
839 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
840 if id in visited_branch: #Cycle
843 if id in visited_node: #Already tested don't work one more time for nothing
846 visited_branch.add(id)
849 #visit child using DFS
850 task = self.browse(cr, uid, id, context=context)
851 for child in task.child_ids:
852 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
856 visited_branch.remove(id)
859 def _check_dates(self, cr, uid, ids, context=None):
862 obj_task = self.browse(cr, uid, ids[0], context=context)
863 start = obj_task.date_start or False
864 end = obj_task.date_end or False
871 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
872 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
875 # Override view according to the company definition
877 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
878 users_obj = self.pool.get('res.users')
880 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
881 # this should be safe (no context passed to avoid side-effects)
882 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
883 tm = obj_tm and obj_tm.name or 'Hours'
885 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
887 if tm in ['Hours','Hour']:
890 eview = etree.fromstring(res['arch'])
892 def _check_rec(eview):
893 if eview.attrib.get('widget','') == 'float_time':
894 eview.set('widget','float')
901 res['arch'] = etree.tostring(eview)
903 for f in res['fields']:
904 if 'Hours' in res['fields'][f]['string']:
905 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
908 def _check_child_task(self, cr, uid, ids, context=None):
911 tasks = self.browse(cr, uid, ids, context=context)
914 for child in task.child_ids:
915 if child.state in ['draft', 'open', 'pending']:
916 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
919 def action_close(self, cr, uid, ids, context=None):
920 # This action open wizard to send email to partner or project manager after close task.
923 task_id = len(ids) and ids[0] or False
924 self._check_child_task(cr, uid, ids, context=context)
925 if not task_id: return False
926 task = self.browse(cr, uid, task_id, context=context)
927 project = task.project_id
928 res = self.do_close(cr, uid, [task_id], context=context)
929 if project.warn_manager or project.warn_customer:
931 'name': _('Send Email after close task'),
934 'res_model': 'mail.compose.message',
935 'type': 'ir.actions.act_window',
938 'context': {'active_id': task.id,
939 'active_model': 'project.task'}
943 def do_close(self, cr, uid, ids, context={}):
947 request = self.pool.get('res.request')
948 if not isinstance(ids,list): ids = [ids]
949 for task in self.browse(cr, uid, ids, context=context):
951 project = task.project_id
953 # Send request to project manager
954 if project.warn_manager and project.user_id and (project.user_id.id != uid):
955 request.create(cr, uid, {
956 'name': _("Task '%s' closed") % task.name,
959 'act_to': project.user_id.id,
960 'ref_partner_id': task.partner_id.id,
961 'ref_doc1': 'project.task,%d'% (task.id,),
962 'ref_doc2': 'project.project,%d'% (project.id,),
965 for parent_id in task.parent_ids:
966 if parent_id.state in ('pending','draft'):
968 for child in parent_id.child_ids:
969 if child.id != task.id and child.state not in ('done','cancelled'):
972 self.do_reopen(cr, uid, [parent_id.id], context=context)
973 vals.update({'state': 'done'})
974 vals.update({'remaining_hours': 0.0})
975 if not task.date_end:
976 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
977 self.write(cr, uid, [task.id],vals, context=context)
978 self.do_close_send_note(cr, uid, [task.id], context)
981 def do_reopen(self, cr, uid, ids, context=None):
982 request = self.pool.get('res.request')
984 for task in self.browse(cr, uid, ids, context=context):
985 project = task.project_id
986 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
987 request.create(cr, uid, {
988 'name': _("Task '%s' set in progress") % task.name,
991 'act_to': project.user_id.id,
992 'ref_partner_id': task.partner_id.id,
993 'ref_doc1': 'project.task,%d' % task.id,
994 'ref_doc2': 'project.project,%d' % project.id,
997 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
998 self.do_open_send_note(cr, uid, [task.id], context)
1001 def do_cancel(self, cr, uid, ids, context={}):
1002 request = self.pool.get('res.request')
1003 tasks = self.browse(cr, uid, ids, context=context)
1004 self._check_child_task(cr, uid, ids, context=context)
1006 project = task.project_id
1007 if project.warn_manager and project.user_id and (project.user_id.id != uid):
1008 request.create(cr, uid, {
1009 'name': _("Task '%s' cancelled") % task.name,
1012 'act_to': project.user_id.id,
1013 'ref_partner_id': task.partner_id.id,
1014 'ref_doc1': 'project.task,%d' % task.id,
1015 'ref_doc2': 'project.project,%d' % project.id,
1017 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
1018 self.do_cancel_send_note(cr, uid, [task.id], context)
1021 def do_open(self, cr, uid, ids, context={}):
1022 if not isinstance(ids,list): ids = [ids]
1023 tasks= self.browse(cr, uid, ids, context=context)
1025 data = {'state': 'open'}
1026 if not t.date_start:
1027 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
1028 self.write(cr, uid, [t.id], data, context=context)
1029 self.do_open_send_note(cr, uid, [t.id], context)
1032 def do_draft(self, cr, uid, ids, context={}):
1033 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
1034 self.do_draft_send_note(cr, uid, ids, context)
1038 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1039 attachment = self.pool.get('ir.attachment')
1040 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1041 new_attachment_ids = []
1042 for attachment_id in attachment_ids:
1043 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1044 return new_attachment_ids
1047 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1049 Delegate Task to another users.
1051 assert delegate_data['user_id'], _("Delegated User should be specified")
1052 delegated_tasks = {}
1053 for task in self.browse(cr, uid, ids, context=context):
1054 delegated_task_id = self.copy(cr, uid, task.id, {
1055 'name': delegate_data['name'],
1056 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1057 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1058 'planned_hours': delegate_data['planned_hours'] or 0.0,
1059 'parent_ids': [(6, 0, [task.id])],
1061 'description': delegate_data['new_task_description'] or '',
1065 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1066 newname = delegate_data['prefix'] or ''
1068 'remaining_hours': delegate_data['planned_hours_me'],
1069 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1072 if delegate_data['state'] == 'pending':
1073 self.do_pending(cr, uid, [task.id], context=context)
1074 elif delegate_data['state'] == 'done':
1075 self.do_close(cr, uid, [task.id], context=context)
1076 self.do_delegation_send_note(cr, uid, [task.id], context)
1077 delegated_tasks[task.id] = delegated_task_id
1078 return delegated_tasks
1080 def do_pending(self, cr, uid, ids, context={}):
1081 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1082 self.do_pending_send_note(cr, uid, ids, context)
1085 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1086 for task in self.browse(cr, uid, ids, context=context):
1087 if (task.state=='draft') or (task.planned_hours==0.0):
1088 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1089 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1092 def set_remaining_time_1(self, cr, uid, ids, context=None):
1093 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1095 def set_remaining_time_2(self, cr, uid, ids, context=None):
1096 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1098 def set_remaining_time_5(self, cr, uid, ids, context=None):
1099 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1101 def set_remaining_time_10(self, cr, uid, ids, context=None):
1102 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1104 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1105 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1107 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1108 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1110 def set_kanban_state_done(self, cr, uid, ids, context=None):
1111 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1113 def _change_type(self, cr, uid, ids, next, context=None):
1115 go to the next stage
1116 if next is False, go to previous stage
1118 for task in self.browse(cr, uid, ids):
1119 if task.project_id.type_ids:
1120 typeid = task.type_id.id
1122 for type in task.project_id.type_ids :
1123 types_seq[type.id] = type.sequence
1125 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1127 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1128 sorted_types = [x[0] for x in types]
1130 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1131 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1132 index = sorted_types.index(typeid)
1133 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1134 self.state_change_send_note(cr, uid, [task.id], context)
1137 def next_type(self, cr, uid, ids, context=None):
1138 return self._change_type(cr, uid, ids, True, context=context)
1140 def prev_type(self, cr, uid, ids, context=None):
1141 return self._change_type(cr, uid, ids, False, context=context)
1143 def _store_history(self, cr, uid, ids, context=None):
1144 for task in self.browse(cr, uid, ids, context=context):
1145 self.pool.get('project.task.history').create(cr, uid, {
1147 'remaining_hours': task.remaining_hours,
1148 'planned_hours': task.planned_hours,
1149 'kanban_state': task.kanban_state,
1150 'type_id': task.type_id.id,
1151 'state': task.state,
1152 'user_id': task.user_id.id
1157 def create(self, cr, uid, vals, context=None):
1158 task_id = super(task, self).create(cr, uid, vals, context=context)
1159 self._store_history(cr, uid, [task_id], context=context)
1160 self.create_send_note(cr, uid, [task_id], context=context)
1163 # Overridden to reset the kanban_state to normal whenever
1164 # the stage (type_id) of the task changes.
1165 def write(self, cr, uid, ids, vals, context=None):
1166 if isinstance(ids, (int, long)):
1168 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1169 new_stage = vals.get('type_id')
1170 vals_reset_kstate = dict(vals, kanban_state='normal')
1171 for t in self.browse(cr, uid, ids, context=context):
1172 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1173 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1176 result = super(task,self).write(cr, uid, ids, vals, context=context)
1177 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1178 self._store_history(cr, uid, ids, context=context)
1179 self.state_change_send_note(cr, uid, ids, context)
1182 def unlink(self, cr, uid, ids, context=None):
1185 self._check_child_task(cr, uid, ids, context=context)
1186 res = super(task, self).unlink(cr, uid, ids, context)
1189 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1190 context = context or {}
1194 if task.state in ('done','cancelled'):
1199 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1201 for t2 in task.parent_ids:
1202 start.append("up.Task_%s.end" % (t2.id,))
1206 ''' % (ident,','.join(start))
1211 ''' % (ident, 'User_'+str(task.user_id.id))
1216 # ---------------------------------------------------
1217 # OpenChatter methods and notifications
1218 # ---------------------------------------------------
1220 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1221 result = dict.fromkeys(ids, [])
1222 for obj in self.browse(cr, uid, ids, context=context):
1223 if obj.state == 'draft' and obj.user_id:
1224 result[obj.id] = [obj.user_id.id]
1227 def message_get_subscribers(self, cr, uid, ids, context=None):
1228 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1229 for obj in self.browse(cr, uid, ids, context=context):
1231 sub_ids.append(obj.user_id.id)
1233 sub_ids.append(obj.manager_id.id)
1234 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1236 def create_send_note(self, cr, uid, ids, context=None):
1237 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1239 def do_pending_send_note(self, cr, uid, ids, context=None):
1240 if not isinstance(ids,list): ids = [ids]
1241 msg = _('Task is now <b>pending</b>.')
1242 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1244 def do_open_send_note(self, cr, uid, ids, context=None):
1245 msg = _('Task has been <b>opened</b>.')
1246 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1248 def do_cancel_send_note(self, cr, uid, ids, context=None):
1249 msg = _('Task has been <b>canceled</b>.')
1250 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1252 def do_close_send_note(self, cr, uid, ids, context=None):
1253 msg = _('Task has been <b>closed</b>.')
1254 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1256 def do_draft_send_note(self, cr, uid, ids, context=None):
1257 msg = _('Task has been <b>renewed</b>.')
1258 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1260 def do_delegation_send_note(self, cr, uid, ids, context=None):
1261 for task in self.browse(cr, uid, ids, context=context):
1262 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1263 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1266 def state_change_send_note(self, cr, uid, ids, context=None):
1267 for task in self.browse(cr, uid, ids, context=context):
1268 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1269 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1274 class project_work(osv.osv):
1275 _name = "project.task.work"
1276 _description = "Project Task Work"
1278 'name': fields.char('Work summary', size=128),
1279 'date': fields.datetime('Date', select="1"),
1280 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1281 'hours': fields.float('Time Spent'),
1282 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1283 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1287 'user_id': lambda obj, cr, uid, context: uid,
1288 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1291 _order = "date desc"
1292 def create(self, cr, uid, vals, *args, **kwargs):
1293 if 'hours' in vals and (not vals['hours']):
1294 vals['hours'] = 0.00
1295 if 'task_id' in vals:
1296 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1297 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1299 def write(self, cr, uid, ids, vals, context=None):
1300 if 'hours' in vals and (not vals['hours']):
1301 vals['hours'] = 0.00
1303 for work in self.browse(cr, uid, ids, context=context):
1304 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))
1305 return super(project_work,self).write(cr, uid, ids, vals, context)
1307 def unlink(self, cr, uid, ids, *args, **kwargs):
1308 for work in self.browse(cr, uid, ids):
1309 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1310 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1313 class account_analytic_account(osv.osv):
1315 _inherit = 'account.analytic.account'
1316 _description = 'Analytic Account'
1318 def create(self, cr, uid, vals, context=None):
1321 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1322 vals['child_ids'] = []
1323 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1325 def unlink(self, cr, uid, ids, *args, **kwargs):
1326 project_obj = self.pool.get('project.project')
1327 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1329 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1330 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1332 account_analytic_account()
1335 # Tasks History, used for cumulative flow charts (Lean/Agile)
1338 class project_task_history(osv.osv):
1339 _name = 'project.task.history'
1340 _description = 'History of Tasks'
1341 _rec_name = 'task_id'
1343 def _get_date(self, cr, uid, ids, name, arg, context=None):
1345 for history in self.browse(cr, uid, ids, context=context):
1346 if history.state in ('done','cancelled'):
1347 result[history.id] = history.date
1349 cr.execute('''select
1352 project_task_history
1356 order by id limit 1''', (history.task_id.id, history.id))
1358 result[history.id] = res and res[0] or False
1361 def _get_related_date(self, cr, uid, ids, context=None):
1363 for history in self.browse(cr, uid, ids, context=context):
1364 cr.execute('''select
1367 project_task_history
1371 order by id desc limit 1''', (history.task_id.id, history.id))
1374 result.append(res[0])
1378 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1379 'type_id': fields.many2one('project.task.type', 'Stage'),
1380 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1381 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1382 'date': fields.date('Date', select=True),
1383 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1384 'project.task.history': (_get_related_date, None, 20)
1386 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1387 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1388 'user_id': fields.many2one('res.users', 'Responsible'),
1391 'date': fields.date.context_today,
1393 project_task_history()
1395 class project_task_history_cumulative(osv.osv):
1396 _name = 'project.task.history.cumulative'
1397 _table = 'project_task_history_cumulative'
1398 _inherit = 'project.task.history'
1401 'end_date': fields.date('End Date'),
1402 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1405 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1407 history.date::varchar||'-'||history.history_id::varchar as id,
1408 history.date as end_date,
1413 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1414 task_id, type_id, user_id, kanban_state, state,
1415 remaining_hours, planned_hours
1417 project_task_history
1421 project_task_history_cumulative()