1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 base_status.base_stage import base_stage
23 from datetime import datetime, date
24 from lxml import etree
25 from osv import fields, osv
26 from openerp.addons.resource.faces import task as Task
28 from tools.translate import _
30 _TASK_STATE = [('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')]
32 class project_task_type(osv.osv):
33 _name = 'project.task.type'
34 _description = 'Task Stage'
37 'name': fields.char('Stage Name', required=True, size=64, translate=True),
38 'description': fields.text('Description'),
39 'sequence': fields.integer('Sequence'),
40 'case_default': fields.boolean('Common to All Projects',
41 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."),
42 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
43 'state': fields.selection(_TASK_STATE, 'State', required=True,
44 help="The related state for the stage. The state of your document will automatically change regarding the selected stage. Example, a stage is related to the state 'Close', when your document reach this stage, it will be automatically closed."),
45 'fold': fields.boolean('Hide in views if empty',
46 help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
55 class project(osv.osv):
56 _name = "project.project"
57 _description = "Project"
58 _inherits = {'account.analytic.account': "analytic_account_id"}
59 _inherit = ['ir.needaction_mixin', 'mail.thread']
61 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
63 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
64 if context and context.get('user_preference'):
65 cr.execute("""SELECT project.id FROM project_project project
66 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
67 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
68 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
69 return [(r[0]) for r in cr.fetchall()]
70 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
71 context=context, count=count)
73 def write(self, cr, uid, ids, vals, context=None):
76 for project_id in self.browse(cr, uid, ids, context):
77 if vals.get('members'):
78 members = self.pool.get('res.users').browse(cr, uid, vals.get('members')[0][-1], context)
80 members = project_id.members or False
81 select = vals.get('privacy_visibility') or project_id.privacy_visibility or False
82 if select=='follower' and members:
83 member_list = [member.id for member in members]
84 followers = self.message_get_subscribers_ids(cr, uid, ids, context=context)
85 for member_id in member_list:
86 if not member_id in followers:
87 self.message_subscribe(cr, uid, ids, [member_id], context=context)
88 return super(project, self).write(cr, uid, ids, vals, context=context)
90 def _complete_name(self, cr, uid, ids, name, args, context=None):
92 for m in self.browse(cr, uid, ids, context=context):
93 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
96 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
97 partner_obj = self.pool.get('res.partner')
101 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
102 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
103 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
104 val['pricelist_id'] = pricelist_id
105 return {'value': val}
107 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
108 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
109 project_ids = [task.project_id.id for task in tasks if task.project_id]
110 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
112 def _get_project_and_parents(self, cr, uid, ids, context=None):
113 """ return the project ids and all their parent projects """
117 SELECT DISTINCT parent.id
118 FROM project_project project, project_project parent, account_analytic_account account
119 WHERE project.analytic_account_id = account.id
120 AND parent.analytic_account_id = account.parent_id
123 ids = [t[0] for t in cr.fetchall()]
127 def _get_project_and_children(self, cr, uid, ids, context=None):
128 """ retrieve all children projects of project ids;
129 return a dictionary mapping each project to its parent project (or None)
131 res = dict.fromkeys(ids, None)
134 SELECT project.id, parent.id
135 FROM project_project project, project_project parent, account_analytic_account account
136 WHERE project.analytic_account_id = account.id
137 AND parent.analytic_account_id = account.parent_id
140 dic = dict(cr.fetchall())
145 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
146 child_parent = self._get_project_and_children(cr, uid, ids, context)
147 # compute planned_hours, total_hours, effective_hours specific to each project
149 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
150 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
151 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
153 """, (tuple(child_parent.keys()),))
154 # aggregate results into res
155 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
156 for id, planned, total, effective in cr.fetchall():
157 # add the values specific to id to all parent projects of id in the result
160 res[id]['planned_hours'] += planned
161 res[id]['total_hours'] += total
162 res[id]['effective_hours'] += effective
163 id = child_parent[id]
164 # compute progress rates
166 if res[id]['total_hours']:
167 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
169 res[id]['progress_rate'] = 0.0
172 def unlink(self, cr, uid, ids, *args, **kwargs):
173 for proj in self.browse(cr, uid, ids):
175 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
176 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
178 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
179 res = dict.fromkeys(ids, 0)
180 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
181 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
182 res[task.project_id.id] += 1
186 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
187 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
188 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
189 '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),
190 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
191 'warn_manager': fields.boolean('Notify 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)]}),
193 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
194 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)]}),
195 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
196 '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.",
198 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
199 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
201 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
203 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
204 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
206 '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.",
208 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
209 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
211 '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.",
213 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
214 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
216 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
217 '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)]}),
218 '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)]}),
219 '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)]}),
220 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
221 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"),
222 'color': fields.integer('Color Index'),
223 'privacy_visibility': fields.selection([('public','Public'), ('follower','Followers Only')], 'Privacy / Visibility', select=True),
226 def dummy(self, cr, uid, ids, context):
229 def message_thread_followers(self, cr, uid, ids, context=None):
230 followers = super(project,self).message_thread_followers(cr, uid, ids, context=context)
231 for project in self.browse(cr, uid, followers.keys(), context=context):
232 project_followers = set(followers[project.id])
233 project_followers.add(project.user_id.user_email)
234 followers[project.id] = filter(None, project_followers)
237 def _get_type_common(self, cr, uid, context):
238 ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context)
246 'type_ids': _get_type_common,
249 # TODO: Why not using a SQL contraints ?
250 def _check_dates(self, cr, uid, ids, context=None):
251 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
252 if leave['date_start'] and leave['date']:
253 if leave['date_start'] > leave['date']:
258 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
261 def set_template(self, cr, uid, ids, context=None):
262 res = self.setActive(cr, uid, ids, value=False, context=context)
265 def set_done(self, cr, uid, ids, context=None):
266 task_obj = self.pool.get('project.task')
267 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
268 task_obj.case_close(cr, uid, task_ids, context=context)
269 self.write(cr, uid, ids, {'state':'close'}, context=context)
270 self.set_close_send_note(cr, uid, ids, context=context)
273 def set_cancel(self, cr, uid, ids, context=None):
274 task_obj = self.pool.get('project.task')
275 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
276 task_obj.case_cancel(cr, uid, task_ids, context=context)
277 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
278 self.set_cancel_send_note(cr, uid, ids, context=context)
281 def set_pending(self, cr, uid, ids, context=None):
282 self.write(cr, uid, ids, {'state':'pending'}, context=context)
283 self.set_pending_send_note(cr, uid, ids, context=context)
286 def set_open(self, cr, uid, ids, context=None):
287 self.write(cr, uid, ids, {'state':'open'}, context=context)
288 self.set_open_send_note(cr, uid, ids, context=context)
291 def reset_project(self, cr, uid, ids, context=None):
292 res = self.setActive(cr, uid, ids, value=True, context=context)
293 self.set_open_send_note(cr, uid, ids, context=context)
296 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
297 """ copy and map tasks from old to new project """
301 task_obj = self.pool.get('project.task')
302 proj = self.browse(cr, uid, old_project_id, context=context)
303 for task in proj.tasks:
304 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
305 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
306 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
309 def copy(self, cr, uid, id, default={}, context=None):
313 default = default or {}
314 context['active_test'] = False
315 default['state'] = 'open'
316 default['tasks'] = []
317 proj = self.browse(cr, uid, id, context=context)
318 if not default.get('name', False):
319 default['name'] = proj.name + _(' (copy)')
321 res = super(project, self).copy(cr, uid, id, default, context)
322 self.map_tasks(cr,uid,id,res,context)
325 def duplicate_template(self, cr, uid, ids, context=None):
328 data_obj = self.pool.get('ir.model.data')
330 for proj in self.browse(cr, uid, ids, context=context):
331 parent_id = context.get('parent_id', False)
332 context.update({'analytic_project_copy': True})
333 new_date_start = time.strftime('%Y-%m-%d')
335 if proj.date_start and proj.date:
336 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
337 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
338 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
339 context.update({'copy':True})
340 new_id = self.copy(cr, uid, proj.id, default = {
341 'name': proj.name +_(' (copy)'),
343 'date_start':new_date_start,
345 'parent_id':parent_id}, context=context)
346 result.append(new_id)
348 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
349 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
351 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
353 if result and len(result):
355 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
356 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
357 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
358 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
359 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
360 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
362 'name': _('Projects'),
364 'view_mode': 'form,tree',
365 'res_model': 'project.project',
368 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
369 'type': 'ir.actions.act_window',
370 'search_view_id': search_view['res_id'],
374 # set active value for a project, its sub projects and its tasks
375 def setActive(self, cr, uid, ids, value=True, context=None):
376 task_obj = self.pool.get('project.task')
377 for proj in self.browse(cr, uid, ids, context=None):
378 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
379 cr.execute('select id from project_task where project_id=%s', (proj.id,))
380 tasks_id = [x[0] for x in cr.fetchall()]
382 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
383 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
385 self.setActive(cr, uid, child_ids, value, context=None)
388 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
389 context = context or {}
390 if type(ids) in (long, int,):
392 projects = self.browse(cr, uid, ids, context=context)
394 for project in projects:
395 if (not project.members) and force_members:
396 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
398 resource_pool = self.pool.get('resource.resource')
400 result = "from openerp.addons.resource.faces import *\n"
401 result += "import datetime\n"
402 for project in self.browse(cr, uid, ids, context=context):
403 u_ids = [i.id for i in project.members]
404 if project.user_id and (project.user_id.id not in u_ids):
405 u_ids.append(project.user_id.id)
406 for task in project.tasks:
407 if task.state in ('done','cancelled'):
409 if task.user_id and (task.user_id.id not in u_ids):
410 u_ids.append(task.user_id.id)
411 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
412 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
413 for key, vals in resource_objs.items():
415 class User_%s(Resource):
417 ''' % (key, vals.get('efficiency', False))
424 def _schedule_project(self, cr, uid, project, context=None):
425 resource_pool = self.pool.get('resource.resource')
426 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
427 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
428 # TODO: check if we need working_..., default values are ok.
429 puids = [x.id for x in project.members]
431 puids.append(project.user_id.id)
439 project.date_start, working_days,
440 '|'.join(['User_'+str(x) for x in puids])
442 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
449 #TODO: DO Resource allocation and compute availability
450 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
456 def schedule_tasks(self, cr, uid, ids, context=None):
457 context = context or {}
458 if type(ids) in (long, int,):
460 projects = self.browse(cr, uid, ids, context=context)
461 result = self._schedule_header(cr, uid, ids, False, context=context)
462 for project in projects:
463 result += self._schedule_project(cr, uid, project, context=context)
464 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
467 exec result in local_dict
468 projects_gantt = Task.BalancedProject(local_dict['Project'])
470 for project in projects:
471 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
472 for task in project.tasks:
473 if task.state in ('done','cancelled'):
476 p = getattr(project_gantt, 'Task_%d' % (task.id,))
478 self.pool.get('project.task').write(cr, uid, [task.id], {
479 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
480 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
482 if (not task.user_id) and (p.booked_resource):
483 self.pool.get('project.task').write(cr, uid, [task.id], {
484 'user_id': int(p.booked_resource[0].name[5:]),
488 # ------------------------------------------------
489 # OpenChatter methods and notifications
490 # ------------------------------------------------
492 def get_needaction_user_ids(self, cr, uid, ids, context=None):
493 result = dict.fromkeys(ids)
494 for obj in self.browse(cr, uid, ids, context=context):
496 if obj.state == 'draft' and obj.user_id:
497 result[obj.id] = [obj.user_id.id]
500 def message_get_subscribers(self, cr, uid, ids, context=None):
501 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
502 for obj in self.browse(cr, uid, ids, context=context):
504 sub_ids.append(obj.user_id.id)
505 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
507 def create(self, cr, uid, vals, context=None):
508 obj_id = super(project, self).create(cr, uid, vals, context=context)
509 self.create_send_note(cr, uid, [obj_id], context=context)
512 def create_send_note(self, cr, uid, ids, context=None):
513 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
515 def set_open_send_note(self, cr, uid, ids, context=None):
516 message = _("Project has been <b>opened</b>.")
517 return self.message_append_note(cr, uid, ids, body=message, context=context)
519 def set_pending_send_note(self, cr, uid, ids, context=None):
520 message = _("Project is now <b>pending</b>.")
521 return self.message_append_note(cr, uid, ids, body=message, context=context)
523 def set_cancel_send_note(self, cr, uid, ids, context=None):
524 message = _("Project has been <b>cancelled</b>.")
525 return self.message_append_note(cr, uid, ids, body=message, context=context)
527 def set_close_send_note(self, cr, uid, ids, context=None):
528 message = _("Project has been <b>closed</b>.")
529 return self.message_append_note(cr, uid, ids, body=message, context=context)
532 class task(base_stage, osv.osv):
533 _name = "project.task"
534 _description = "Task"
535 _date_name = "date_start"
536 _inherit = ['ir.needaction_mixin', 'mail.thread']
538 def _get_default_project_id(self, cr, uid, context=None):
539 """ Gives default section by checking if present in the context """
540 return (self._resolve_project_id_from_context(cr, uid, context=context) or False)
542 def _get_default_stage_id(self, cr, uid, context=None):
543 """ Gives default stage_id """
544 project_id = self._get_default_project_id(cr, uid, context=context)
545 return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
547 def _resolve_project_id_from_context(self, cr, uid, context=None):
548 """ Returns ID of project based on the value of 'default_project_id'
549 context key, or None if it cannot be resolved to a single
552 if context is None: context = {}
553 if type(context.get('default_project_id')) in (int, long):
554 return context['default_project_id']
555 if isinstance(context.get('default_project_id'), basestring):
556 project_name = context['default_project_id']
557 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
558 if len(project_ids) == 1:
559 return project_ids[0][0]
562 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
563 stage_obj = self.pool.get('project.task.type')
564 order = stage_obj._order
565 access_rights_uid = access_rights_uid or uid
566 # lame way to allow reverting search, should just work in the trivial case
567 if read_group_order == 'stage_id desc':
568 order = '%s desc' % order
569 # retrieve section_id from the context and write the domain
570 # - ('id', 'in', 'ids'): add columns that should be present
571 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
572 # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
574 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
576 search_domain += ['|', '&', ('project_ids', '=', project_id), ('fold', '=', False)]
577 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
578 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
579 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
580 # restore order of the search
581 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
584 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
585 res_users = self.pool.get('res.users')
586 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
587 access_rights_uid = access_rights_uid or uid
589 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
590 order = res_users._order
591 # lame way to allow reverting search, should just work in the trivial case
592 if read_group_order == 'user_id desc':
593 order = '%s desc' % order
594 # de-duplicate and apply search order
595 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
596 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
597 # restore order of the search
598 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
602 'stage_id': _read_group_stage_ids,
603 'user_id': _read_group_user_id,
606 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
607 obj_project = self.pool.get('project.project')
609 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
610 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
611 if id and isinstance(id, (long, int)):
612 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
613 args.append(('active', '=', False))
614 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
616 def _str_get(self, task, level=0, border='***', context=None):
617 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'+ \
618 border[0]+' '+(task.name or '')+'\n'+ \
619 (task.description or '')+'\n\n'
621 # Compute: effective_hours, total_hours, progress
622 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
624 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
625 hours = dict(cr.fetchall())
626 for task in self.browse(cr, uid, ids, context=context):
627 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)}
628 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
629 res[task.id]['progress'] = 0.0
630 if (task.remaining_hours + hours.get(task.id, 0.0)):
631 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
632 if task.state in ('done','cancelled'):
633 res[task.id]['progress'] = 100.0
636 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
637 if remaining and not planned:
638 return {'value':{'planned_hours': remaining}}
641 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
642 return {'value':{'remaining_hours': planned - effective}}
644 def onchange_project(self, cr, uid, id, project_id):
647 data = self.pool.get('project.project').browse(cr, uid, [project_id])
648 partner_id=data and data[0].partner_id
650 return {'value':{'partner_id':partner_id.id}}
653 def duplicate_task(self, cr, uid, map_ids, context=None):
654 for new in map_ids.values():
655 task = self.browse(cr, uid, new, context)
656 child_ids = [ ch.id for ch in task.child_ids]
658 for child in task.child_ids:
659 if child.id in map_ids.keys():
660 child_ids.remove(child.id)
661 child_ids.append(map_ids[child.id])
663 parent_ids = [ ch.id for ch in task.parent_ids]
665 for parent in task.parent_ids:
666 if parent.id in map_ids.keys():
667 parent_ids.remove(parent.id)
668 parent_ids.append(map_ids[parent.id])
669 #FIXME why there is already the copy and the old one
670 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
672 def copy_data(self, cr, uid, id, default={}, context=None):
673 default = default or {}
674 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
675 if not default.get('remaining_hours', False):
676 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
677 default['active'] = True
678 default['stage_id'] = False
679 if not default.get('name', False):
680 default['name'] = self.browse(cr, uid, id, context=context).name or ''
681 if not context.get('copy',False):
682 new_name = _("%s (copy)")%default.get('name','')
683 default.update({'name':new_name})
684 return super(task, self).copy_data(cr, uid, id, default, context)
687 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
689 for task in self.browse(cr, uid, ids, context=context):
692 if task.project_id.active == False or task.project_id.state == 'template':
696 def _get_task(self, cr, uid, ids, context=None):
698 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
699 if work.task_id: result[work.task_id.id] = True
703 '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."),
704 'name': fields.char('Task Summary', size=128, required=True, select=True),
705 'description': fields.text('Description'),
706 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
707 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
708 'stage_id': fields.many2one('project.task.type', 'Stage',
709 domain="['|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
710 'state': fields.related('stage_id', 'state', type="selection", store=True,
711 selection=_TASK_STATE, string="State", readonly=True,
712 help='The state is set to \'Draft\', when a case is created.\
713 If the case is in progress the state is set to \'Open\'.\
714 When the case is over, the state is set to \'Done\'.\
715 If the case needs to be reviewed then the state is \
716 set to \'Pending\'.'),
717 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
718 help="A task's kanban state indicates special situations affecting it:\n"
719 " * Normal is the default situation\n"
720 " * Blocked indicates something is preventing the progress of this task\n"
721 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
722 readonly=True, required=False),
723 'create_date': fields.datetime('Create Date', readonly=True,select=True),
724 'date_start': fields.datetime('Starting Date',select=True),
725 'date_end': fields.datetime('Ending Date',select=True),
726 'date_deadline': fields.date('Deadline',select=True),
727 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
728 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
729 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
730 'notes': fields.text('Notes'),
731 '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.'),
732 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
734 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
735 'project.task.work': (_get_task, ['hours'], 10),
737 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
738 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
740 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
741 'project.task.work': (_get_task, ['hours'], 10),
743 '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",
745 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
746 'project.task.work': (_get_task, ['hours'], 10),
748 '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.",
750 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
751 'project.task.work': (_get_task, ['hours'], 10),
753 'user_id': fields.many2one('res.users', 'Assigned to'),
754 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
755 'partner_id': fields.many2one('res.partner', 'Partner'),
756 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
757 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
758 'company_id': fields.many2one('res.company', 'Company'),
759 'id': fields.integer('ID', readonly=True),
760 'color': fields.integer('Color Index'),
761 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
765 'stage_id': _get_default_stage_id,
766 'project_id': _get_default_project_id,
768 'kanban_state': 'normal',
773 'user_id': lambda obj, cr, uid, context: uid,
774 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c),
777 _order = "priority, sequence, date_start, name, id"
779 def set_priority(self, cr, uid, ids, priority):
782 return self.write(cr, uid, ids, {'priority' : priority})
784 def set_high_priority(self, cr, uid, ids, *args):
785 """Set task priority to high
787 return self.set_priority(cr, uid, ids, '1')
789 def set_normal_priority(self, cr, uid, ids, *args):
790 """Set task priority to normal
792 return self.set_priority(cr, uid, ids, '2')
794 def _check_recursion(self, cr, uid, ids, context=None):
796 visited_branch = set()
798 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
804 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
805 if id in visited_branch: #Cycle
808 if id in visited_node: #Already tested don't work one more time for nothing
811 visited_branch.add(id)
814 #visit child using DFS
815 task = self.browse(cr, uid, id, context=context)
816 for child in task.child_ids:
817 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
821 visited_branch.remove(id)
824 def _check_dates(self, cr, uid, ids, context=None):
827 obj_task = self.browse(cr, uid, ids[0], context=context)
828 start = obj_task.date_start or False
829 end = obj_task.date_end or False
836 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
837 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
840 # Override view according to the company definition
842 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
843 users_obj = self.pool.get('res.users')
845 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
846 # this should be safe (no context passed to avoid side-effects)
847 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
848 tm = obj_tm and obj_tm.name or 'Hours'
850 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
852 if tm in ['Hours','Hour']:
855 eview = etree.fromstring(res['arch'])
857 def _check_rec(eview):
858 if eview.attrib.get('widget','') == 'float_time':
859 eview.set('widget','float')
866 res['arch'] = etree.tostring(eview)
868 for f in res['fields']:
869 if 'Hours' in res['fields'][f]['string']:
870 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
873 # ****************************************
875 # ****************************************
877 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
878 """ Override of the base.stage method
879 Parameter of the stage search taken from the lead:
880 - section_id: if set, stages must belong to this section or
881 be a default stage; if not set, stages must be default
884 if isinstance(cases, (int, long)):
885 cases = self.browse(cr, uid, cases, context=context)
886 # collect all section_ids
889 section_ids.append(section_id)
892 section_ids.append(task.project_id.id)
893 # OR all section_ids and OR with case_default
896 search_domain += [('|')] * len(section_ids)
897 for section_id in section_ids:
898 search_domain.append(('project_ids', '=', section_id))
899 search_domain.append(('case_default', '=', True))
900 # AND with the domain in parameter
901 search_domain += list(domain)
902 # perform search, return the first found
903 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
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 closes the task, then opens the wizard to send an
921 email to the partner or the project manager.
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=None):
944 """ Compatibility when changing to case_close. """
945 return self.case_close(cr, uid, ids, context=context)
947 def case_close(self, cr, uid, ids, context=None):
949 request = self.pool.get('res.request')
950 if not isinstance(ids, list): ids = [ids]
951 for task in self.browse(cr, uid, ids, context=context):
953 project = task.project_id
954 for parent_id in task.parent_ids:
955 if parent_id.state in ('pending','draft'):
957 for child in parent_id.child_ids:
958 if child.id != task.id and child.state not in ('done','cancelled'):
961 self.do_reopen(cr, uid, [parent_id.id], context=context)
963 vals['remaining_hours'] = 0.0
964 if not task.date_end:
965 vals['date_end'] = fields.datetime.now()
966 self.case_set(cr, uid, [task.id], 'done', vals, context=context)
967 self.case_close_send_note(cr, uid, [task.id], context=context)
970 def do_reopen(self, cr, uid, ids, context=None):
971 for task in self.browse(cr, uid, ids, context=context):
972 project = task.project_id
973 self.case_set(cr, uid, [task.id], 'open', {}, context=context)
974 self.case_open_send_note(cr, uid, [task.id], context)
977 def do_cancel(self, cr, uid, ids, context=None):
978 """ Compatibility when changing to case_cancel. """
979 return self.case_cancel(cr, uid, ids, context=context)
981 def case_cancel(self, cr, uid, ids, context=None):
982 request = self.pool.get('res.request')
983 tasks = self.browse(cr, uid, ids, context=context)
984 self._check_child_task(cr, uid, ids, context=context)
986 self.case_set(cr, uid, [task.id], 'cancelled', {'remaining_hours': 0.0}, context=context)
987 self.case_cancel_send_note(cr, uid, [task.id], context=context)
990 def do_open(self, cr, uid, ids, context=None):
991 """ Compatibility when changing to case_open. """
992 return self.case_open(cr, uid, ids, context=context)
994 def case_open(self, cr, uid, ids, context=None):
995 if not isinstance(ids,list): ids = [ids]
996 self.case_set(cr, uid, ids, 'open', {'date_start': fields.datetime.now()}, context=context)
997 self.case_open_send_note(cr, uid, ids, context)
1000 def do_draft(self, cr, uid, ids, context=None):
1001 """ Compatibility when changing to case_draft. """
1002 return self.case_draft(cr, uid, ids, context=context)
1004 def case_draft(self, cr, uid, ids, context=None):
1005 self.case_set(cr, uid, ids, 'draft', {}, context=context)
1006 self.case_draft_send_note(cr, uid, ids, context=context)
1009 def do_pending(self, cr, uid, ids, context=None):
1010 """ Compatibility when changing to case_pending. """
1011 return self.case_pending(cr, uid, ids, context=context)
1013 def case_pending(self, cr, uid, ids, context=None):
1014 self.case_set(cr, uid, ids, 'pending', {}, context=context)
1015 return self.case_pending_send_note(cr, uid, ids, context=context)
1017 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1018 attachment = self.pool.get('ir.attachment')
1019 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1020 new_attachment_ids = []
1021 for attachment_id in attachment_ids:
1022 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1023 return new_attachment_ids
1025 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1027 Delegate Task to another users.
1029 assert delegate_data['user_id'], _("Delegated User should be specified")
1030 delegated_tasks = {}
1031 for task in self.browse(cr, uid, ids, context=context):
1032 delegated_task_id = self.copy(cr, uid, task.id, {
1033 'name': delegate_data['name'],
1034 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1035 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1036 'planned_hours': delegate_data['planned_hours'] or 0.0,
1037 'parent_ids': [(6, 0, [task.id])],
1039 'description': delegate_data['new_task_description'] or '',
1043 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1044 newname = delegate_data['prefix'] or ''
1046 'remaining_hours': delegate_data['planned_hours_me'],
1047 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1050 if delegate_data['state'] == 'pending':
1051 self.do_pending(cr, uid, [task.id], context=context)
1052 elif delegate_data['state'] == 'done':
1053 self.do_close(cr, uid, [task.id], context=context)
1054 self.do_delegation_send_note(cr, uid, [task.id], context)
1055 delegated_tasks[task.id] = delegated_task_id
1056 return delegated_tasks
1058 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1059 for task in self.browse(cr, uid, ids, context=context):
1060 if (task.state=='draft') or (task.planned_hours==0.0):
1061 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1062 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1065 def set_remaining_time_1(self, cr, uid, ids, context=None):
1066 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1068 def set_remaining_time_2(self, cr, uid, ids, context=None):
1069 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1071 def set_remaining_time_5(self, cr, uid, ids, context=None):
1072 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1074 def set_remaining_time_10(self, cr, uid, ids, context=None):
1075 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1077 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1078 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1080 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1081 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1083 def set_kanban_state_done(self, cr, uid, ids, context=None):
1084 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1086 def _store_history(self, cr, uid, ids, context=None):
1087 for task in self.browse(cr, uid, ids, context=context):
1088 self.pool.get('project.task.history').create(cr, uid, {
1090 'remaining_hours': task.remaining_hours,
1091 'planned_hours': task.planned_hours,
1092 'kanban_state': task.kanban_state,
1093 'type_id': task.stage_id.id,
1094 'state': task.state,
1095 'user_id': task.user_id.id
1100 def create(self, cr, uid, vals, context=None):
1101 task_id = super(task, self).create(cr, uid, vals, context=context)
1102 self._store_history(cr, uid, [task_id], context=context)
1103 self.create_send_note(cr, uid, [task_id], context=context)
1106 # Overridden to reset the kanban_state to normal whenever
1107 # the stage (stage_id) of the task changes.
1108 def write(self, cr, uid, ids, vals, context=None):
1109 if isinstance(ids, (int, long)):
1111 if vals and not 'kanban_state' in vals and 'stage_id' in vals:
1112 new_stage = vals.get('stage_id')
1113 vals_reset_kstate = dict(vals, kanban_state='normal')
1114 for t in self.browse(cr, uid, ids, context=context):
1115 #TO FIX:Kanban view doesn't raise warning
1116 #stages = [stage.id for stage in t.project_id.type_ids]
1117 #if new_stage not in stages:
1118 #raise osv.except_osv(_('Warning !'), _('Stage is not defined in the project.'))
1119 write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
1120 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1123 result = super(task,self).write(cr, uid, ids, vals, context=context)
1124 if ('stage_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1125 self._store_history(cr, uid, ids, context=context)
1128 def unlink(self, cr, uid, ids, context=None):
1131 self._check_child_task(cr, uid, ids, context=context)
1132 res = super(task, self).unlink(cr, uid, ids, context)
1135 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1136 context = context or {}
1140 if task.state in ('done','cancelled'):
1145 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1147 for t2 in task.parent_ids:
1148 start.append("up.Task_%s.end" % (t2.id,))
1152 ''' % (ident,','.join(start))
1157 ''' % (ident, 'User_'+str(task.user_id.id))
1162 # ---------------------------------------------------
1163 # OpenChatter methods and notifications
1164 # ---------------------------------------------------
1166 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
1167 """ Override of default prefix for notifications. """
1170 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1171 result = dict.fromkeys(ids, [])
1172 for obj in self.browse(cr, uid, ids, context=context):
1173 if obj.state == 'draft' and obj.user_id:
1174 result[obj.id] = [obj.user_id.id]
1177 def message_get_subscribers(self, cr, uid, ids, context=None):
1178 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1179 for obj in self.browse(cr, uid, ids, context=context):
1181 sub_ids.append(obj.user_id.id)
1183 sub_ids.append(obj.manager_id.id)
1184 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1186 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
1187 """ Override of the (void) default notification method. """
1188 stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
1189 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
1191 def create_send_note(self, cr, uid, ids, context=None):
1192 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1194 def case_draft_send_note(self, cr, uid, ids, context=None):
1195 msg = _('Task has been set as <b>draft</b>.')
1196 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1198 def do_delegation_send_note(self, cr, uid, ids, context=None):
1199 for task in self.browse(cr, uid, ids, context=context):
1200 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1201 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1205 class project_work(osv.osv):
1206 _name = "project.task.work"
1207 _description = "Project Task Work"
1209 'name': fields.char('Work summary', size=128),
1210 'date': fields.datetime('Date', select="1"),
1211 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1212 'hours': fields.float('Time Spent'),
1213 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1214 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1218 'user_id': lambda obj, cr, uid, context: uid,
1219 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1222 _order = "date desc"
1223 def create(self, cr, uid, vals, *args, **kwargs):
1224 if 'hours' in vals and (not vals['hours']):
1225 vals['hours'] = 0.00
1226 if 'task_id' in vals:
1227 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1228 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1230 def write(self, cr, uid, ids, vals, context=None):
1231 if 'hours' in vals and (not vals['hours']):
1232 vals['hours'] = 0.00
1234 for work in self.browse(cr, uid, ids, context=context):
1235 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))
1236 return super(project_work,self).write(cr, uid, ids, vals, context)
1238 def unlink(self, cr, uid, ids, *args, **kwargs):
1239 for work in self.browse(cr, uid, ids):
1240 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1241 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1244 class account_analytic_account(osv.osv):
1245 _inherit = 'account.analytic.account'
1246 _description = 'Analytic Account'
1248 'use_tasks': fields.boolean('Tasks Management'),
1249 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1252 # 'use_tasks': True,
1255 def project_create(self,cr,uid,analytic_account_id,vals,context=None):
1257 project_pool = self.pool.get('project.project')
1258 project_id = project_pool.search(cr, uid, [('name','=',vals.get('name'))])
1260 res['name'] = vals.get('name')
1261 res['analytic_account_id'] = analytic_account_id
1262 project_pool.create(cr, uid, res, context=context)
1265 def create(self, cr, uid, vals, context=None):
1268 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1269 vals['child_ids'] = []
1270 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1271 if vals.get('use_tasks', False):
1272 self.project_create(cr, uid, analytic_account_id, vals, context)
1273 return analytic_account_id
1275 def unlink(self, cr, uid, ids, *args, **kwargs):
1276 project_obj = self.pool.get('project.project')
1277 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1279 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1280 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1284 # Tasks History, used for cumulative flow charts (Lean/Agile)
1287 class project_task_history(osv.osv):
1288 _name = 'project.task.history'
1289 _description = 'History of Tasks'
1290 _rec_name = 'task_id'
1292 def _get_date(self, cr, uid, ids, name, arg, context=None):
1294 for history in self.browse(cr, uid, ids, context=context):
1295 if history.state in ('done','cancelled'):
1296 result[history.id] = history.date
1298 cr.execute('''select
1301 project_task_history
1305 order by id limit 1''', (history.task_id.id, history.id))
1307 result[history.id] = res and res[0] or False
1310 def _get_related_date(self, cr, uid, ids, context=None):
1312 for history in self.browse(cr, uid, ids, context=context):
1313 cr.execute('''select
1316 project_task_history
1320 order by id desc limit 1''', (history.task_id.id, history.id))
1323 result.append(res[0])
1327 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1328 'type_id': fields.many2one('project.task.type', 'Stage'),
1329 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status'),
1330 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1331 'date': fields.date('Date', select=True),
1332 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1333 'project.task.history': (_get_related_date, None, 20)
1335 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1336 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1337 'user_id': fields.many2one('res.users', 'Responsible'),
1340 'date': fields.date.context_today,
1344 class project_task_history_cumulative(osv.osv):
1345 _name = 'project.task.history.cumulative'
1346 _table = 'project_task_history_cumulative'
1347 _inherit = 'project.task.history'
1350 'end_date': fields.date('End Date'),
1351 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1354 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1356 history.date::varchar||'-'||history.history_id::varchar as id,
1357 history.date as end_date,
1362 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1363 task_id, type_id, user_id, kanban_state, state,
1364 greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours
1366 project_task_history