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"),
205 def dummy(self, cr, uid, ids, context=None):
208 def open_tasks(self, cr, uid, ids, context=None):
209 #Open the View for the Tasks for the project
211 This opens Tasks views
212 @return :Dictionary value for task view
217 data_obj = self.pool.get('ir.model.data')
218 for project in self.browse(cr, uid, ids, context=context):
220 tree_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_tree2')
221 form_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_form2')
222 calander_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_calendar')
223 search_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_search_form')
224 kanban_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_kanban')
226 #'search_default_user_id': uid,
227 'search_default_project_id':project.id,
228 #'search_default_open':1,
234 'view_mode': 'form,tree',
235 'res_model': 'project.task',
237 'domain':[('project_id','in',ids)],
239 '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')],
240 'type': 'ir.actions.act_window',
241 'search_view_id': search_view and search_view[1] or False,
246 def open_users(self, cr, uid, ids, context=None):
247 #Open the View for the Tasks for the project
249 This opens Tasks views
250 @return :Dictionary value for task view
255 data_obj = self.pool.get('ir.model.data')
256 for project in self.browse(cr, uid, ids, context=context):
258 tree_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_tree')
259 form_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_form')
260 search_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_search')
266 'view_mode': 'form,tree',
267 'res_model': 'res.users',
270 'res_id': project.user_id.id,
271 'views': [(form_view and form_view[1] or False, 'form'),(tree_view and tree_view[1] or False, 'tree')],
272 'type': 'ir.actions.act_window',
273 'search_view_id': search_view and search_view[1] or False,
278 def _get_type_common(self, cr, uid, context):
279 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
287 'type_ids': _get_type_common,
291 # TODO: Why not using a SQL contraints ?
292 def _check_dates(self, cr, uid, ids, context=None):
293 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
294 if leave['date_start'] and leave['date']:
295 if leave['date_start'] > leave['date']:
300 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
303 def set_template(self, cr, uid, ids, context=None):
304 res = self.setActive(cr, uid, ids, value=False, context=context)
307 def set_done(self, cr, uid, ids, context=None):
308 task_obj = self.pool.get('project.task')
309 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
310 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
311 self.write(cr, uid, ids, {'state':'close'}, context=context)
312 self.set_close_send_note(cr, uid, ids, context=context)
315 def set_cancel(self, cr, uid, ids, context=None):
316 task_obj = self.pool.get('project.task')
317 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
318 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
319 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
320 self.set_cancel_send_note(cr, uid, ids, context=context)
323 def set_pending(self, cr, uid, ids, context=None):
324 self.write(cr, uid, ids, {'state':'pending'}, context=context)
325 self.set_pending_send_note(cr, uid, ids, context=context)
328 def set_open(self, cr, uid, ids, context=None):
329 self.write(cr, uid, ids, {'state':'open'}, context=context)
330 self.set_open_send_note(cr, uid, ids, context=context)
333 def reset_project(self, cr, uid, ids, context=None):
334 res = self.setActive(cr, uid, ids, value=True, context=context)
335 self.set_open_send_note(cr, uid, ids, context=context)
338 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
339 """ copy and map tasks from old to new project """
343 task_obj = self.pool.get('project.task')
344 proj = self.browse(cr, uid, old_project_id, context=context)
345 for task in proj.tasks:
346 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
347 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
348 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
351 def copy(self, cr, uid, id, default={}, context=None):
355 default = default or {}
356 context['active_test'] = False
357 default['state'] = 'open'
358 default['tasks'] = []
359 proj = self.browse(cr, uid, id, context=context)
360 if not default.get('name', False):
361 default['name'] = proj.name + _(' (copy)')
363 res = super(project, self).copy(cr, uid, id, default, context)
364 self.map_tasks(cr,uid,id,res,context)
367 def duplicate_template(self, cr, uid, ids, context=None):
370 data_obj = self.pool.get('ir.model.data')
372 for proj in self.browse(cr, uid, ids, context=context):
373 parent_id = context.get('parent_id', False)
374 context.update({'analytic_project_copy': True})
375 new_date_start = time.strftime('%Y-%m-%d')
377 if proj.date_start and proj.date:
378 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
379 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
380 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
381 context.update({'copy':True})
382 new_id = self.copy(cr, uid, proj.id, default = {
383 'name': proj.name +_(' (copy)'),
385 'date_start':new_date_start,
387 'parent_id':parent_id}, context=context)
388 result.append(new_id)
390 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
391 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
393 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
395 if result and len(result):
397 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
398 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
399 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
400 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
401 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
402 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
404 'name': _('Projects'),
406 'view_mode': 'form,tree',
407 'res_model': 'project.project',
410 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
411 'type': 'ir.actions.act_window',
412 'search_view_id': search_view['res_id'],
416 # set active value for a project, its sub projects and its tasks
417 def setActive(self, cr, uid, ids, value=True, context=None):
418 task_obj = self.pool.get('project.task')
419 for proj in self.browse(cr, uid, ids, context=None):
420 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
421 cr.execute('select id from project_task where project_id=%s', (proj.id,))
422 tasks_id = [x[0] for x in cr.fetchall()]
424 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
425 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
427 self.setActive(cr, uid, child_ids, value, context=None)
430 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
431 context = context or {}
432 if type(ids) in (long, int,):
434 projects = self.browse(cr, uid, ids, context=context)
436 for project in projects:
437 if (not project.members) and force_members:
438 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
440 resource_pool = self.pool.get('resource.resource')
442 result = "from openerp.addons.resource.faces import *\n"
443 result += "import datetime\n"
444 for project in self.browse(cr, uid, ids, context=context):
445 u_ids = [i.id for i in project.members]
446 if project.user_id and (project.user_id.id not in u_ids):
447 u_ids.append(project.user_id.id)
448 for task in project.tasks:
449 if task.state in ('done','cancelled'):
451 if task.user_id and (task.user_id.id not in u_ids):
452 u_ids.append(task.user_id.id)
453 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
454 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
455 for key, vals in resource_objs.items():
457 class User_%s(Resource):
459 ''' % (key, vals.get('efficiency', False))
466 def _schedule_project(self, cr, uid, project, context=None):
467 resource_pool = self.pool.get('resource.resource')
468 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
469 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
470 # TODO: check if we need working_..., default values are ok.
471 puids = [x.id for x in project.members]
473 puids.append(project.user_id.id)
481 project.date_start, working_days,
482 '|'.join(['User_'+str(x) for x in puids])
484 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
491 #TODO: DO Resource allocation and compute availability
492 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
498 def schedule_tasks(self, cr, uid, ids, context=None):
499 context = context or {}
500 if type(ids) in (long, int,):
502 projects = self.browse(cr, uid, ids, context=context)
503 result = self._schedule_header(cr, uid, ids, False, context=context)
504 for project in projects:
505 result += self._schedule_project(cr, uid, project, context=context)
506 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
509 exec result in local_dict
510 projects_gantt = Task.BalancedProject(local_dict['Project'])
512 for project in projects:
513 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
514 for task in project.tasks:
515 if task.state in ('done','cancelled'):
518 p = getattr(project_gantt, 'Task_%d' % (task.id,))
520 self.pool.get('project.task').write(cr, uid, [task.id], {
521 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
522 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
524 if (not task.user_id) and (p.booked_resource):
525 self.pool.get('project.task').write(cr, uid, [task.id], {
526 'user_id': int(p.booked_resource[0].name[5:]),
530 # ------------------------------------------------
531 # OpenChatter methods and notifications
532 # ------------------------------------------------
534 def get_needaction_user_ids(self, cr, uid, ids, context=None):
535 result = dict.fromkeys(ids)
536 for obj in self.browse(cr, uid, ids, context=context):
538 if obj.state == 'draft' and obj.user_id:
539 result[obj.id] = [obj.user_id.id]
542 def message_get_subscribers(self, cr, uid, ids, context=None):
543 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
544 for obj in self.browse(cr, uid, ids, context=context):
546 sub_ids.append(obj.user_id.id)
547 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
549 def create(self, cr, uid, vals, context=None):
550 obj_id = super(project, self).create(cr, uid, vals, context=context)
551 self.create_send_note(cr, uid, [obj_id], context=context)
554 def create_send_note(self, cr, uid, ids, context=None):
555 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
557 def set_open_send_note(self, cr, uid, ids, context=None):
558 message = _("Project has been <b>opened</b>.")
559 return self.message_append_note(cr, uid, ids, body=message, context=context)
561 def set_pending_send_note(self, cr, uid, ids, context=None):
562 message = _("Project is now <b>pending</b>.")
563 return self.message_append_note(cr, uid, ids, body=message, context=context)
565 def set_cancel_send_note(self, cr, uid, ids, context=None):
566 message = _("Project has been <b>cancelled</b>.")
567 return self.message_append_note(cr, uid, ids, body=message, context=context)
569 def set_close_send_note(self, cr, uid, ids, context=None):
570 message = _("Project has been <b>closed</b>.")
571 return self.message_append_note(cr, uid, ids, body=message, context=context)
575 class users(osv.osv):
576 _inherit = 'res.users'
578 'context_project_id': fields.many2one('project.project', 'Project')
583 _name = "project.task"
584 _description = "Task"
586 _date_name = "date_start"
587 _inherit = ['ir.needaction_mixin', 'mail.thread']
590 def _resolve_project_id_from_context(self, cr, uid, context=None):
591 """Return ID of project based on the value of 'project_id'
592 context key, or None if it cannot be resolved to a single project.
594 if context is None: context = {}
595 if type(context.get('project_id')) in (int, long):
596 project_id = context['project_id']
598 if isinstance(context.get('project_id'), basestring):
599 project_name = context['project_id']
600 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
601 if len(project_ids) == 1:
602 return project_ids[0][0]
604 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
605 stage_obj = self.pool.get('project.task.type')
606 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
607 order = stage_obj._order
608 access_rights_uid = access_rights_uid or uid
609 if read_group_order == 'type_id desc':
610 # lame way to allow reverting search, should just work in the trivial case
611 order = '%s desc' % order
613 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
615 domain = ['|', ('id','in',ids), ('project_default','=',1)]
616 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
617 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
618 # restore order of the search
619 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
622 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
623 res_users = self.pool.get('res.users')
624 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
625 access_rights_uid = access_rights_uid or uid
627 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
628 order = res_users._order
629 # lame way to allow reverting search, should just work in the trivial case
630 if read_group_order == 'user_id desc':
631 order = '%s desc' % order
632 # de-duplicate and apply search order
633 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
634 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
635 # restore order of the search
636 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
640 'type_id': _read_group_type_id,
641 'user_id': _read_group_user_id
645 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
646 obj_project = self.pool.get('project.project')
648 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
649 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
650 if id and isinstance(id, (long, int)):
651 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
652 args.append(('active', '=', False))
653 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
655 def _str_get(self, task, level=0, border='***', context=None):
656 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'+ \
657 border[0]+' '+(task.name or '')+'\n'+ \
658 (task.description or '')+'\n\n'
660 # Compute: effective_hours, total_hours, progress
661 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
663 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
664 hours = dict(cr.fetchall())
665 for task in self.browse(cr, uid, ids, context=context):
666 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)}
667 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
668 res[task.id]['progress'] = 0.0
669 if (task.remaining_hours + hours.get(task.id, 0.0)):
670 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
671 if task.state in ('done','cancelled'):
672 res[task.id]['progress'] = 100.0
676 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
677 if remaining and not planned:
678 return {'value':{'planned_hours': remaining}}
681 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
682 return {'value':{'remaining_hours': planned - effective}}
684 def onchange_project(self, cr, uid, id, project_id):
687 data = self.pool.get('project.project').browse(cr, uid, [project_id])
688 partner_id=data and data[0].partner_id
690 return {'value':{'partner_id':partner_id.id}}
693 def duplicate_task(self, cr, uid, map_ids, context=None):
694 for new in map_ids.values():
695 task = self.browse(cr, uid, new, context)
696 child_ids = [ ch.id for ch in task.child_ids]
698 for child in task.child_ids:
699 if child.id in map_ids.keys():
700 child_ids.remove(child.id)
701 child_ids.append(map_ids[child.id])
703 parent_ids = [ ch.id for ch in task.parent_ids]
705 for parent in task.parent_ids:
706 if parent.id in map_ids.keys():
707 parent_ids.remove(parent.id)
708 parent_ids.append(map_ids[parent.id])
709 #FIXME why there is already the copy and the old one
710 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
712 def copy_data(self, cr, uid, id, default={}, context=None):
713 default = default or {}
714 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
715 if not default.get('remaining_hours', False):
716 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
717 default['active'] = True
718 default['type_id'] = False
719 if not default.get('name', False):
720 default['name'] = self.browse(cr, uid, id, context=context).name or ''
721 if not context.get('copy',False):
722 new_name = _("%s (copy)")%default.get('name','')
723 default.update({'name':new_name})
724 return super(task, self).copy_data(cr, uid, id, default, context)
727 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
729 for task in self.browse(cr, uid, ids, context=context):
732 if task.project_id.active == False or task.project_id.state == 'template':
736 def _get_task(self, cr, uid, ids, context=None):
738 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
739 if work.task_id: result[work.task_id.id] = True
743 '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."),
744 'name': fields.char('Task Summary', size=128, required=True, select=True),
745 'description': fields.text('Description'),
746 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
747 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
748 'type_id': fields.many2one('project.task.type', 'Stage'),
749 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
750 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.\
751 \n If the task is over, the states is set to \'Done\'.'),
752 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
753 help="A task's kanban state indicates special situations affecting it:\n"
754 " * Normal is the default situation\n"
755 " * Blocked indicates something is preventing the progress of this task\n"
756 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
757 readonly=True, required=False),
758 'create_date': fields.datetime('Create Date', readonly=True,select=True),
759 'date_start': fields.datetime('Starting Date',select=True),
760 'date_end': fields.datetime('Ending Date',select=True),
761 'date_deadline': fields.date('Deadline',select=True),
762 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
763 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
764 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
765 'notes': fields.text('Notes'),
766 '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.'),
767 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
769 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
770 'project.task.work': (_get_task, ['hours'], 10),
772 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
773 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
775 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
776 'project.task.work': (_get_task, ['hours'], 10),
778 '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",
780 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
781 'project.task.work': (_get_task, ['hours'], 10),
783 '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.",
785 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
786 'project.task.work': (_get_task, ['hours'], 10),
788 'user_id': fields.many2one('res.users', 'Assigned to'),
789 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
790 'partner_id': fields.many2one('res.partner', 'Partner'),
791 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
792 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
793 'company_id': fields.many2one('res.company', 'Company'),
794 'id': fields.integer('ID', readonly=True),
795 'color': fields.integer('Color Index'),
796 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
801 'kanban_state': 'normal',
806 'user_id': lambda obj, cr, uid, context: uid,
807 'project_id':lambda self, cr, uid, context: context.get('active_id',False),
808 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
811 _order = "priority, sequence, date_start, name, id"
813 def set_priority(self, cr, uid, ids, priority):
816 return self.write(cr, uid, ids, {'priority' : priority})
818 def set_high_priority(self, cr, uid, ids, *args):
819 """Set task priority to high
821 return self.set_priority(cr, uid, ids, '1')
823 def set_normal_priority(self, cr, uid, ids, *args):
824 """Set task priority to normal
826 return self.set_priority(cr, uid, ids, '2')
828 def _check_recursion(self, cr, uid, ids, context=None):
830 visited_branch = set()
832 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
838 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
839 if id in visited_branch: #Cycle
842 if id in visited_node: #Already tested don't work one more time for nothing
845 visited_branch.add(id)
848 #visit child using DFS
849 task = self.browse(cr, uid, id, context=context)
850 for child in task.child_ids:
851 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
855 visited_branch.remove(id)
858 def _check_dates(self, cr, uid, ids, context=None):
861 obj_task = self.browse(cr, uid, ids[0], context=context)
862 start = obj_task.date_start or False
863 end = obj_task.date_end or False
870 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
871 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
874 # Override view according to the company definition
876 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
877 users_obj = self.pool.get('res.users')
879 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
880 # this should be safe (no context passed to avoid side-effects)
881 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
882 tm = obj_tm and obj_tm.name or 'Hours'
884 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
886 if tm in ['Hours','Hour']:
889 eview = etree.fromstring(res['arch'])
891 def _check_rec(eview):
892 if eview.attrib.get('widget','') == 'float_time':
893 eview.set('widget','float')
900 res['arch'] = etree.tostring(eview)
902 for f in res['fields']:
903 if 'Hours' in res['fields'][f]['string']:
904 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
907 def _check_child_task(self, cr, uid, ids, context=None):
910 tasks = self.browse(cr, uid, ids, context=context)
913 for child in task.child_ids:
914 if child.state in ['draft', 'open', 'pending']:
915 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
918 def action_close(self, cr, uid, ids, context=None):
919 # This action open wizard to send email to partner or project manager after close task.
922 task_id = len(ids) and ids[0] or False
923 self._check_child_task(cr, uid, ids, context=context)
924 if not task_id: return False
925 task = self.browse(cr, uid, task_id, context=context)
926 project = task.project_id
927 res = self.do_close(cr, uid, [task_id], context=context)
928 if project.warn_manager or project.warn_customer:
930 'name': _('Send Email after close task'),
933 'res_model': 'mail.compose.message',
934 'type': 'ir.actions.act_window',
937 'context': {'active_id': task.id,
938 'active_model': 'project.task'}
942 def do_close(self, cr, uid, ids, context={}):
946 request = self.pool.get('res.request')
947 if not isinstance(ids,list): ids = [ids]
948 for task in self.browse(cr, uid, ids, context=context):
950 project = task.project_id
952 # Send request to project manager
953 if project.warn_manager and project.user_id and (project.user_id.id != uid):
954 request.create(cr, uid, {
955 'name': _("Task '%s' closed") % task.name,
958 'act_to': project.user_id.id,
959 'ref_partner_id': task.partner_id.id,
960 'ref_doc1': 'project.task,%d'% (task.id,),
961 'ref_doc2': 'project.project,%d'% (project.id,),
964 for parent_id in task.parent_ids:
965 if parent_id.state in ('pending','draft'):
967 for child in parent_id.child_ids:
968 if child.id != task.id and child.state not in ('done','cancelled'):
971 self.do_reopen(cr, uid, [parent_id.id], context=context)
972 vals.update({'state': 'done'})
973 vals.update({'remaining_hours': 0.0})
974 if not task.date_end:
975 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
976 self.write(cr, uid, [task.id],vals, context=context)
977 self.do_close_send_note(cr, uid, [task.id], context)
980 def do_reopen(self, cr, uid, ids, context=None):
981 request = self.pool.get('res.request')
983 for task in self.browse(cr, uid, ids, context=context):
984 project = task.project_id
985 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
986 request.create(cr, uid, {
987 'name': _("Task '%s' set in progress") % task.name,
990 'act_to': project.user_id.id,
991 'ref_partner_id': task.partner_id.id,
992 'ref_doc1': 'project.task,%d' % task.id,
993 'ref_doc2': 'project.project,%d' % project.id,
996 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
997 self.do_open_send_note(cr, uid, [task.id], context)
1000 def do_cancel(self, cr, uid, ids, context={}):
1001 request = self.pool.get('res.request')
1002 tasks = self.browse(cr, uid, ids, context=context)
1003 self._check_child_task(cr, uid, ids, context=context)
1005 project = task.project_id
1006 if project.warn_manager and project.user_id and (project.user_id.id != uid):
1007 request.create(cr, uid, {
1008 'name': _("Task '%s' cancelled") % task.name,
1011 'act_to': project.user_id.id,
1012 'ref_partner_id': task.partner_id.id,
1013 'ref_doc1': 'project.task,%d' % task.id,
1014 'ref_doc2': 'project.project,%d' % project.id,
1016 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
1017 self.do_cancel_send_note(cr, uid, [task.id], context)
1020 def do_open(self, cr, uid, ids, context={}):
1021 if not isinstance(ids,list): ids = [ids]
1022 tasks= self.browse(cr, uid, ids, context=context)
1024 data = {'state': 'open'}
1025 if not t.date_start:
1026 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
1027 self.write(cr, uid, [t.id], data, context=context)
1028 self.do_open_send_note(cr, uid, [t.id], context)
1031 def do_draft(self, cr, uid, ids, context={}):
1032 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
1033 self.do_draft_send_note(cr, uid, ids, context)
1037 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1038 attachment = self.pool.get('ir.attachment')
1039 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1040 new_attachment_ids = []
1041 for attachment_id in attachment_ids:
1042 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1043 return new_attachment_ids
1046 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1048 Delegate Task to another users.
1050 assert delegate_data['user_id'], _("Delegated User should be specified")
1051 delegated_tasks = {}
1052 for task in self.browse(cr, uid, ids, context=context):
1053 delegated_task_id = self.copy(cr, uid, task.id, {
1054 'name': delegate_data['name'],
1055 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1056 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1057 'planned_hours': delegate_data['planned_hours'] or 0.0,
1058 'parent_ids': [(6, 0, [task.id])],
1060 'description': delegate_data['new_task_description'] or '',
1064 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1065 newname = delegate_data['prefix'] or ''
1067 'remaining_hours': delegate_data['planned_hours_me'],
1068 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1071 if delegate_data['state'] == 'pending':
1072 self.do_pending(cr, uid, [task.id], context=context)
1073 elif delegate_data['state'] == 'done':
1074 self.do_close(cr, uid, [task.id], context=context)
1075 self.do_delegation_send_note(cr, uid, [task.id], context)
1076 delegated_tasks[task.id] = delegated_task_id
1077 return delegated_tasks
1079 def do_pending(self, cr, uid, ids, context={}):
1080 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1081 self.do_pending_send_note(cr, uid, ids, context)
1084 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1085 for task in self.browse(cr, uid, ids, context=context):
1086 if (task.state=='draft') or (task.planned_hours==0.0):
1087 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1088 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1091 def set_remaining_time_1(self, cr, uid, ids, context=None):
1092 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1094 def set_remaining_time_2(self, cr, uid, ids, context=None):
1095 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1097 def set_remaining_time_5(self, cr, uid, ids, context=None):
1098 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1100 def set_remaining_time_10(self, cr, uid, ids, context=None):
1101 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1103 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1104 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1106 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1107 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1109 def set_kanban_state_done(self, cr, uid, ids, context=None):
1110 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1112 def _change_type(self, cr, uid, ids, next, context=None):
1114 go to the next stage
1115 if next is False, go to previous stage
1117 for task in self.browse(cr, uid, ids):
1118 if task.project_id.type_ids:
1119 typeid = task.type_id.id
1121 for type in task.project_id.type_ids :
1122 types_seq[type.id] = type.sequence
1124 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1126 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1127 sorted_types = [x[0] for x in types]
1129 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1130 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1131 index = sorted_types.index(typeid)
1132 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1133 self.state_change_send_note(cr, uid, [task.id], context)
1136 def next_type(self, cr, uid, ids, context=None):
1137 return self._change_type(cr, uid, ids, True, context=context)
1139 def prev_type(self, cr, uid, ids, context=None):
1140 return self._change_type(cr, uid, ids, False, context=context)
1142 def _store_history(self, cr, uid, ids, context=None):
1143 for task in self.browse(cr, uid, ids, context=context):
1144 self.pool.get('project.task.history').create(cr, uid, {
1146 'remaining_hours': task.remaining_hours,
1147 'planned_hours': task.planned_hours,
1148 'kanban_state': task.kanban_state,
1149 'type_id': task.type_id.id,
1150 'state': task.state,
1151 'user_id': task.user_id.id
1156 def create(self, cr, uid, vals, context=None):
1157 task_id = super(task, self).create(cr, uid, vals, context=context)
1158 self._store_history(cr, uid, [task_id], context=context)
1159 self.create_send_note(cr, uid, [task_id], context=context)
1162 # Overridden to reset the kanban_state to normal whenever
1163 # the stage (type_id) of the task changes.
1164 def write(self, cr, uid, ids, vals, context=None):
1165 if isinstance(ids, (int, long)):
1167 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1168 new_stage = vals.get('type_id')
1169 vals_reset_kstate = dict(vals, kanban_state='normal')
1170 for t in self.browse(cr, uid, ids, context=context):
1171 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1172 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1175 result = super(task,self).write(cr, uid, ids, vals, context=context)
1176 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1177 self._store_history(cr, uid, ids, context=context)
1178 self.state_change_send_note(cr, uid, ids, context)
1181 def unlink(self, cr, uid, ids, context=None):
1184 self._check_child_task(cr, uid, ids, context=context)
1185 res = super(task, self).unlink(cr, uid, ids, context)
1188 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1189 context = context or {}
1193 if task.state in ('done','cancelled'):
1198 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1200 for t2 in task.parent_ids:
1201 start.append("up.Task_%s.end" % (t2.id,))
1205 ''' % (ident,','.join(start))
1210 ''' % (ident, 'User_'+str(task.user_id.id))
1215 # ---------------------------------------------------
1216 # OpenChatter methods and notifications
1217 # ---------------------------------------------------
1219 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1220 result = dict.fromkeys(ids, [])
1221 for obj in self.browse(cr, uid, ids, context=context):
1222 if obj.state == 'draft' and obj.user_id:
1223 result[obj.id] = [obj.user_id.id]
1226 def message_get_subscribers(self, cr, uid, ids, context=None):
1227 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1228 for obj in self.browse(cr, uid, ids, context=context):
1230 sub_ids.append(obj.user_id.id)
1232 sub_ids.append(obj.manager_id.id)
1233 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1235 def create_send_note(self, cr, uid, ids, context=None):
1236 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1238 def do_pending_send_note(self, cr, uid, ids, context=None):
1239 if not isinstance(ids,list): ids = [ids]
1240 msg = _('Task is now <b>pending</b>.')
1241 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1243 def do_open_send_note(self, cr, uid, ids, context=None):
1244 msg = _('Task has been <b>opened</b>.')
1245 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1247 def do_cancel_send_note(self, cr, uid, ids, context=None):
1248 msg = _('Task has been <b>canceled</b>.')
1249 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1251 def do_close_send_note(self, cr, uid, ids, context=None):
1252 msg = _('Task has been <b>closed</b>.')
1253 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1255 def do_draft_send_note(self, cr, uid, ids, context=None):
1256 msg = _('Task has been <b>renewed</b>.')
1257 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1259 def do_delegation_send_note(self, cr, uid, ids, context=None):
1260 for task in self.browse(cr, uid, ids, context=context):
1261 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1262 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1265 def state_change_send_note(self, cr, uid, ids, context=None):
1266 for task in self.browse(cr, uid, ids, context=context):
1267 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1268 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1273 class project_work(osv.osv):
1274 _name = "project.task.work"
1275 _description = "Project Task Work"
1277 'name': fields.char('Work summary', size=128),
1278 'date': fields.datetime('Date', select="1"),
1279 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1280 'hours': fields.float('Time Spent'),
1281 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1282 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1286 'user_id': lambda obj, cr, uid, context: uid,
1287 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1290 _order = "date desc"
1291 def create(self, cr, uid, vals, *args, **kwargs):
1292 if 'hours' in vals and (not vals['hours']):
1293 vals['hours'] = 0.00
1294 if 'task_id' in vals:
1295 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1296 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1298 def write(self, cr, uid, ids, vals, context=None):
1299 if 'hours' in vals and (not vals['hours']):
1300 vals['hours'] = 0.00
1302 for work in self.browse(cr, uid, ids, context=context):
1303 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))
1304 return super(project_work,self).write(cr, uid, ids, vals, context)
1306 def unlink(self, cr, uid, ids, *args, **kwargs):
1307 for work in self.browse(cr, uid, ids):
1308 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1309 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1312 class account_analytic_account(osv.osv):
1314 _inherit = 'account.analytic.account'
1315 _description = 'Analytic Account'
1317 def create(self, cr, uid, vals, context=None):
1320 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1321 vals['child_ids'] = []
1322 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1324 def unlink(self, cr, uid, ids, *args, **kwargs):
1325 project_obj = self.pool.get('project.project')
1326 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1328 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1329 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1331 account_analytic_account()
1334 # Tasks History, used for cumulative flow charts (Lean/Agile)
1337 class project_task_history(osv.osv):
1338 _name = 'project.task.history'
1339 _description = 'History of Tasks'
1340 _rec_name = 'task_id'
1342 def _get_date(self, cr, uid, ids, name, arg, context=None):
1344 for history in self.browse(cr, uid, ids, context=context):
1345 if history.state in ('done','cancelled'):
1346 result[history.id] = history.date
1348 cr.execute('''select
1351 project_task_history
1355 order by id limit 1''', (history.task_id.id, history.id))
1357 result[history.id] = res and res[0] or False
1360 def _get_related_date(self, cr, uid, ids, context=None):
1362 for history in self.browse(cr, uid, ids, context=context):
1363 cr.execute('''select
1366 project_task_history
1370 order by id desc limit 1''', (history.task_id.id, history.id))
1373 result.append(res[0])
1377 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1378 'type_id': fields.many2one('project.task.type', 'Stage'),
1379 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1380 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1381 'date': fields.date('Date', select=True),
1382 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1383 'project.task.history': (_get_related_date, None, 20)
1385 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1386 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1387 'user_id': fields.many2one('res.users', 'Responsible'),
1390 'date': fields.date.context_today,
1392 project_task_history()
1394 class project_task_history_cumulative(osv.osv):
1395 _name = 'project.task.history.cumulative'
1396 _table = 'project_task_history_cumulative'
1397 _inherit = 'project.task.history'
1400 'end_date': fields.date('End Date'),
1401 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1404 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1406 history.date::varchar||'-'||history.history_id::varchar as id,
1407 history.date as end_date,
1412 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1413 task_id, type_id, user_id, kanban_state, state,
1414 remaining_hours, planned_hours
1416 project_task_history
1420 project_task_history_cumulative()