1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from lxml import etree
24 from datetime import datetime, date
26 from tools.translate import _
27 from osv import fields, osv
28 from openerp.addons.resource.faces import task as Task
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 # _name = 'project.project'
35 class project_task_type(osv.osv):
36 _name = 'project.task.type'
37 _description = 'Task Stage'
40 'name': fields.char('Stage Name', required=True, size=64, translate=True),
41 'description': fields.text('Description'),
42 'sequence': fields.integer('Sequence'),
43 'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
52 class project(osv.osv):
53 _name = "project.project"
54 _description = "Project"
55 _inherits = {'account.analytic.account': "analytic_account_id"}
56 _inherit = ['ir.needaction_mixin', 'mail.thread']
58 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
61 if context and context.get('user_preference'):
62 cr.execute("""SELECT project.id FROM project_project project
63 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
64 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
65 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
66 return [(r[0]) for r in cr.fetchall()]
67 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
68 context=context, count=count)
70 def _complete_name(self, cr, uid, ids, name, args, context=None):
72 for m in self.browse(cr, uid, ids, context=context):
73 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
76 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
77 partner_obj = self.pool.get('res.partner')
81 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84 val['pricelist_id'] = pricelist_id
87 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
88 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
89 project_ids = [task.project_id.id for task in tasks if task.project_id]
90 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
92 def _get_project_and_parents(self, cr, uid, ids, context=None):
93 """ return the project ids and all their parent projects """
97 SELECT DISTINCT parent.id
98 FROM project_project project, project_project parent, account_analytic_account account
99 WHERE project.analytic_account_id = account.id
100 AND parent.analytic_account_id = account.parent_id
103 ids = [t[0] for t in cr.fetchall()]
107 def _get_project_and_children(self, cr, uid, ids, context=None):
108 """ retrieve all children projects of project ids;
109 return a dictionary mapping each project to its parent project (or None)
111 res = dict.fromkeys(ids, None)
114 SELECT project.id, parent.id
115 FROM project_project project, project_project parent, account_analytic_account account
116 WHERE project.analytic_account_id = account.id
117 AND parent.analytic_account_id = account.parent_id
120 dic = dict(cr.fetchall())
125 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
126 child_parent = self._get_project_and_children(cr, uid, ids, context)
127 # compute planned_hours, total_hours, effective_hours specific to each project
129 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
130 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
131 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
133 """, (tuple(child_parent.keys()),))
134 # aggregate results into res
135 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
136 for id, planned, total, effective in cr.fetchall():
137 # add the values specific to id to all parent projects of id in the result
140 res[id]['planned_hours'] += planned
141 res[id]['total_hours'] += total
142 res[id]['effective_hours'] += effective
143 id = child_parent[id]
144 # compute progress rates
146 if res[id]['total_hours']:
147 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
149 res[id]['progress_rate'] = 0.0
152 def unlink(self, cr, uid, ids, *args, **kwargs):
153 for proj in self.browse(cr, uid, ids):
155 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
156 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
158 def _open_task(self, cr, uid, ids, field_name, arg, context=None):
160 task_pool=self.pool.get('project.task')
162 task_ids = task_pool.search(cr, uid, [('project_id', '=', id)])
163 open_task[id] = len(task_ids)
166 def company_uom_id(self, cr, uid, ids, field_name, arg, context=None):
168 for project in self.browse(cr,uid,ids):
169 user_browse = self.pool.get('res.users').browse(cr,uid,project.user_id)
170 uom_company[project.id] = project.company_id.project_time_mode_id.name or user_browse.company_id.project_time_mode_id.name
175 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
176 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
177 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
178 '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),
179 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
180 '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)]}),
182 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
183 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)]}),
184 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
185 '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.",
187 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
188 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
190 '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.",
192 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
193 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
195 '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.",
197 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
198 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
200 '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.",
202 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
203 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
205 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
206 '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)]}),
207 '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)]}),
208 '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)]}),
209 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
210 'task': fields.boolean('Task',help = "If you check this field tasks appears in kanban view"),
211 'open_task': fields.function(_open_task , type='integer',string="Open Tasks"),
212 'color': fields.integer('Color Index'),
213 'company_uom_id': fields.function(company_uom_id,type="char"),
215 def dummy(self, cr, uid, ids, context=None):
218 def open_tasks(self, cr, uid, ids, context=None):
219 #Open the View for the Tasks for the project
221 This opens Tasks views
222 @return :Dictionary value for task view
227 context = dict(context, search_default_project_id=ids[0])
232 'view_mode': 'kanban,tree,calendar,form',
233 'res_model': 'project.task',
235 'domain':[('project_id','in',ids)],
236 'type': 'ir.actions.act_window',
240 def _get_type_common(self, cr, uid, context):
241 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
249 'type_ids': _get_type_common,
253 # TODO: Why not using a SQL contraints ?
254 def _check_dates(self, cr, uid, ids, context=None):
255 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
256 if leave['date_start'] and leave['date']:
257 if leave['date_start'] > leave['date']:
262 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
265 def set_template(self, cr, uid, ids, context=None):
266 res = self.setActive(cr, uid, ids, value=False, context=context)
269 def set_done(self, cr, uid, ids, context=None):
270 task_obj = self.pool.get('project.task')
271 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
272 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
273 self.write(cr, uid, ids, {'state':'close'}, context=context)
274 self.set_close_send_note(cr, uid, ids, context=context)
277 def set_cancel(self, cr, uid, ids, context=None):
278 task_obj = self.pool.get('project.task')
279 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
280 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
281 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
282 self.set_cancel_send_note(cr, uid, ids, context=context)
285 def set_pending(self, cr, uid, ids, context=None):
286 self.write(cr, uid, ids, {'state':'pending'}, context=context)
287 self.set_pending_send_note(cr, uid, ids, context=context)
290 def set_open(self, cr, uid, ids, context=None):
291 self.write(cr, uid, ids, {'state':'open'}, context=context)
292 self.set_open_send_note(cr, uid, ids, context=context)
295 def reset_project(self, cr, uid, ids, context=None):
296 res = self.setActive(cr, uid, ids, value=True, context=context)
297 self.set_open_send_note(cr, uid, ids, context=context)
300 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
301 """ copy and map tasks from old to new project """
305 task_obj = self.pool.get('project.task')
306 proj = self.browse(cr, uid, old_project_id, context=context)
307 for task in proj.tasks:
308 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
309 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
310 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
313 def copy(self, cr, uid, id, default={}, context=None):
317 default = default or {}
318 context['active_test'] = False
319 default['state'] = 'open'
320 default['tasks'] = []
321 proj = self.browse(cr, uid, id, context=context)
322 if not default.get('name', False):
323 default['name'] = proj.name + _(' (copy)')
325 res = super(project, self).copy(cr, uid, id, default, context)
326 self.map_tasks(cr,uid,id,res,context)
329 def duplicate_template(self, cr, uid, ids, context=None):
332 data_obj = self.pool.get('ir.model.data')
334 for proj in self.browse(cr, uid, ids, context=context):
335 parent_id = context.get('parent_id', False)
336 context.update({'analytic_project_copy': True})
337 new_date_start = time.strftime('%Y-%m-%d')
339 if proj.date_start and proj.date:
340 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
341 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
342 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
343 context.update({'copy':True})
344 new_id = self.copy(cr, uid, proj.id, default = {
345 'name': proj.name +_(' (copy)'),
347 'date_start':new_date_start,
349 'parent_id':parent_id}, context=context)
350 result.append(new_id)
352 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
353 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
355 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
357 if result and len(result):
359 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
360 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
361 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
362 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
363 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
364 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
366 'name': _('Projects'),
368 'view_mode': 'form,tree',
369 'res_model': 'project.project',
372 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
373 'type': 'ir.actions.act_window',
374 'search_view_id': search_view['res_id'],
378 # set active value for a project, its sub projects and its tasks
379 def setActive(self, cr, uid, ids, value=True, context=None):
380 task_obj = self.pool.get('project.task')
381 for proj in self.browse(cr, uid, ids, context=None):
382 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
383 cr.execute('select id from project_task where project_id=%s', (proj.id,))
384 tasks_id = [x[0] for x in cr.fetchall()]
386 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
387 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
389 self.setActive(cr, uid, child_ids, value, context=None)
392 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
393 context = context or {}
394 if type(ids) in (long, int,):
396 projects = self.browse(cr, uid, ids, context=context)
398 for project in projects:
399 if (not project.members) and force_members:
400 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
402 resource_pool = self.pool.get('resource.resource')
404 result = "from openerp.addons.resource.faces import *\n"
405 result += "import datetime\n"
406 for project in self.browse(cr, uid, ids, context=context):
407 u_ids = [i.id for i in project.members]
408 if project.user_id and (project.user_id.id not in u_ids):
409 u_ids.append(project.user_id.id)
410 for task in project.tasks:
411 if task.state in ('done','cancelled'):
413 if task.user_id and (task.user_id.id not in u_ids):
414 u_ids.append(task.user_id.id)
415 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
416 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
417 for key, vals in resource_objs.items():
419 class User_%s(Resource):
421 ''' % (key, vals.get('efficiency', False))
428 def _schedule_project(self, cr, uid, project, context=None):
429 resource_pool = self.pool.get('resource.resource')
430 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
431 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
432 # TODO: check if we need working_..., default values are ok.
433 puids = [x.id for x in project.members]
435 puids.append(project.user_id.id)
443 project.date_start, working_days,
444 '|'.join(['User_'+str(x) for x in puids])
446 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
453 #TODO: DO Resource allocation and compute availability
454 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
460 def schedule_tasks(self, cr, uid, ids, context=None):
461 context = context or {}
462 if type(ids) in (long, int,):
464 projects = self.browse(cr, uid, ids, context=context)
465 result = self._schedule_header(cr, uid, ids, False, context=context)
466 for project in projects:
467 result += self._schedule_project(cr, uid, project, context=context)
468 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
471 exec result in local_dict
472 projects_gantt = Task.BalancedProject(local_dict['Project'])
474 for project in projects:
475 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
476 for task in project.tasks:
477 if task.state in ('done','cancelled'):
480 p = getattr(project_gantt, 'Task_%d' % (task.id,))
482 self.pool.get('project.task').write(cr, uid, [task.id], {
483 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
484 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
486 if (not task.user_id) and (p.booked_resource):
487 self.pool.get('project.task').write(cr, uid, [task.id], {
488 'user_id': int(p.booked_resource[0].name[5:]),
492 # ------------------------------------------------
493 # OpenChatter methods and notifications
494 # ------------------------------------------------
496 def get_needaction_user_ids(self, cr, uid, ids, context=None):
497 result = dict.fromkeys(ids)
498 for obj in self.browse(cr, uid, ids, context=context):
500 if obj.state == 'draft' and obj.user_id:
501 result[obj.id] = [obj.user_id.id]
504 def message_get_subscribers(self, cr, uid, ids, context=None):
505 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
506 for obj in self.browse(cr, uid, ids, context=context):
508 sub_ids.append(obj.user_id.id)
509 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
511 def create(self, cr, uid, vals, context=None):
512 obj_id = super(project, self).create(cr, uid, vals, context=context)
513 self.create_send_note(cr, uid, [obj_id], context=context)
516 def create_send_note(self, cr, uid, ids, context=None):
517 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
519 def set_open_send_note(self, cr, uid, ids, context=None):
520 message = _("Project has been <b>opened</b>.")
521 return self.message_append_note(cr, uid, ids, body=message, context=context)
523 def set_pending_send_note(self, cr, uid, ids, context=None):
524 message = _("Project is now <b>pending</b>.")
525 return self.message_append_note(cr, uid, ids, body=message, context=context)
527 def set_cancel_send_note(self, cr, uid, ids, context=None):
528 message = _("Project has been <b>cancelled</b>.")
529 return self.message_append_note(cr, uid, ids, body=message, context=context)
531 def set_close_send_note(self, cr, uid, ids, context=None):
532 message = _("Project has been <b>closed</b>.")
533 return self.message_append_note(cr, uid, ids, body=message, context=context)
537 class users(osv.osv):
538 _inherit = 'res.users'
540 'context_project_id': fields.many2one('project.project', 'Project')
545 _name = "project.task"
546 _description = "Task"
548 _date_name = "date_start"
549 _inherit = ['ir.needaction_mixin', 'mail.thread']
552 def _resolve_project_id_from_context(self, cr, uid, context=None):
553 """Return ID of project based on the value of 'project_id'
554 context key, or None if it cannot be resolved to a single project.
556 if context is None: context = {}
557 if type(context.get('project_id')) in (int, long):
558 project_id = context['project_id']
560 if isinstance(context.get('project_id'), basestring):
561 project_name = context['project_id']
562 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
563 if len(project_ids) == 1:
564 return project_ids[0][0]
566 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
567 stage_obj = self.pool.get('project.task.type')
568 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
569 order = stage_obj._order
570 access_rights_uid = access_rights_uid or uid
571 if read_group_order == 'type_id desc':
572 # lame way to allow reverting search, should just work in the trivial case
573 order = '%s desc' % order
575 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
577 domain = ['|', ('id','in',ids), ('project_default','=',1)]
578 stage_ids = stage_obj._search(cr, uid, 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 'type_id': _read_group_type_id,
603 'user_id': _read_group_user_id
607 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
608 obj_project = self.pool.get('project.project')
610 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
611 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
612 if id and isinstance(id, (long, int)):
613 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
614 args.append(('active', '=', False))
615 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
617 def _str_get(self, task, level=0, border='***', context=None):
618 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'+ \
619 border[0]+' '+(task.name or '')+'\n'+ \
620 (task.description or '')+'\n\n'
622 # Compute: effective_hours, total_hours, progress
623 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
625 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
626 hours = dict(cr.fetchall())
627 for task in self.browse(cr, uid, ids, context=context):
628 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)}
629 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
630 res[task.id]['progress'] = 0.0
631 if (task.remaining_hours + hours.get(task.id, 0.0)):
632 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
633 if task.state in ('done','cancelled'):
634 res[task.id]['progress'] = 100.0
638 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
639 if remaining and not planned:
640 return {'value':{'planned_hours': remaining}}
643 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
644 return {'value':{'remaining_hours': planned - effective}}
646 def onchange_project(self, cr, uid, id, project_id):
649 data = self.pool.get('project.project').browse(cr, uid, [project_id])
650 partner_id=data and data[0].partner_id
652 return {'value':{'partner_id':partner_id.id}}
655 def duplicate_task(self, cr, uid, map_ids, context=None):
656 for new in map_ids.values():
657 task = self.browse(cr, uid, new, context)
658 child_ids = [ ch.id for ch in task.child_ids]
660 for child in task.child_ids:
661 if child.id in map_ids.keys():
662 child_ids.remove(child.id)
663 child_ids.append(map_ids[child.id])
665 parent_ids = [ ch.id for ch in task.parent_ids]
667 for parent in task.parent_ids:
668 if parent.id in map_ids.keys():
669 parent_ids.remove(parent.id)
670 parent_ids.append(map_ids[parent.id])
671 #FIXME why there is already the copy and the old one
672 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
674 def copy_data(self, cr, uid, id, default={}, context=None):
675 default = default or {}
676 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
677 if not default.get('remaining_hours', False):
678 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
679 default['active'] = True
680 default['type_id'] = False
681 if not default.get('name', False):
682 default['name'] = self.browse(cr, uid, id, context=context).name or ''
683 if not context.get('copy',False):
684 new_name = _("%s (copy)")%default.get('name','')
685 default.update({'name':new_name})
686 return super(task, self).copy_data(cr, uid, id, default, context)
689 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
691 for task in self.browse(cr, uid, ids, context=context):
694 if task.project_id.active == False or task.project_id.state == 'template':
698 def _get_task(self, cr, uid, ids, context=None):
700 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
701 if work.task_id: result[work.task_id.id] = True
705 '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."),
706 'name': fields.char('Task Summary', size=128, required=True, select=True),
707 'description': fields.text('Description'),
708 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
709 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
710 'type_id': fields.many2one('project.task.type', 'Stage'),
711 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
712 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.\
713 \n If the task is over, the states is set to \'Done\'.'),
714 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
715 help="A task's kanban state indicates special situations affecting it:\n"
716 " * Normal is the default situation\n"
717 " * Blocked indicates something is preventing the progress of this task\n"
718 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
719 readonly=True, required=False),
720 'create_date': fields.datetime('Create Date', readonly=True,select=True),
721 'date_start': fields.datetime('Starting Date',select=True),
722 'date_end': fields.datetime('Ending Date',select=True),
723 'date_deadline': fields.date('Deadline',select=True),
724 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
725 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
726 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
727 'notes': fields.text('Notes'),
728 '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.'),
729 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
731 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
732 'project.task.work': (_get_task, ['hours'], 10),
734 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
735 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
737 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
738 'project.task.work': (_get_task, ['hours'], 10),
740 '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",
742 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
743 'project.task.work': (_get_task, ['hours'], 10),
745 '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.",
747 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
748 'project.task.work': (_get_task, ['hours'], 10),
750 'user_id': fields.many2one('res.users', 'Assigned to'),
751 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
752 'partner_id': fields.many2one('res.partner', 'Partner'),
753 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
754 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
755 'company_id': fields.many2one('res.company', 'Company'),
756 'id': fields.integer('ID', readonly=True),
757 'color': fields.integer('Color Index'),
758 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
763 'kanban_state': 'normal',
768 'user_id': lambda obj, cr, uid, context: uid,
769 'project_id':lambda self, cr, uid, context: context.get('active_id',False),
770 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
773 _order = "priority, sequence, date_start, name, id"
775 def set_priority(self, cr, uid, ids, priority):
778 return self.write(cr, uid, ids, {'priority' : priority})
780 def set_high_priority(self, cr, uid, ids, *args):
781 """Set task priority to high
783 return self.set_priority(cr, uid, ids, '1')
785 def set_normal_priority(self, cr, uid, ids, *args):
786 """Set task priority to normal
788 return self.set_priority(cr, uid, ids, '2')
790 def _check_recursion(self, cr, uid, ids, context=None):
792 visited_branch = set()
794 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
800 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
801 if id in visited_branch: #Cycle
804 if id in visited_node: #Already tested don't work one more time for nothing
807 visited_branch.add(id)
810 #visit child using DFS
811 task = self.browse(cr, uid, id, context=context)
812 for child in task.child_ids:
813 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
817 visited_branch.remove(id)
820 def _check_dates(self, cr, uid, ids, context=None):
823 obj_task = self.browse(cr, uid, ids[0], context=context)
824 start = obj_task.date_start or False
825 end = obj_task.date_end or False
832 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
833 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
836 # Override view according to the company definition
838 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
839 users_obj = self.pool.get('res.users')
841 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
842 # this should be safe (no context passed to avoid side-effects)
843 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
844 tm = obj_tm and obj_tm.name or 'Hours'
846 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
848 if tm in ['Hours','Hour']:
851 eview = etree.fromstring(res['arch'])
853 def _check_rec(eview):
854 if eview.attrib.get('widget','') == 'float_time':
855 eview.set('widget','float')
862 res['arch'] = etree.tostring(eview)
864 for f in res['fields']:
865 if 'Hours' in res['fields'][f]['string']:
866 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
869 def _check_child_task(self, cr, uid, ids, context=None):
872 tasks = self.browse(cr, uid, ids, context=context)
875 for child in task.child_ids:
876 if child.state in ['draft', 'open', 'pending']:
877 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
880 def action_close(self, cr, uid, ids, context=None):
881 # This action open wizard to send email to partner or project manager after close task.
884 task_id = len(ids) and ids[0] or False
885 self._check_child_task(cr, uid, ids, context=context)
886 if not task_id: return False
887 task = self.browse(cr, uid, task_id, context=context)
888 project = task.project_id
889 res = self.do_close(cr, uid, [task_id], context=context)
890 if project.warn_manager or project.warn_customer:
892 'name': _('Send Email after close task'),
895 'res_model': 'mail.compose.message',
896 'type': 'ir.actions.act_window',
899 'context': {'active_id': task.id,
900 'active_model': 'project.task'}
904 def do_close(self, cr, uid, ids, context={}):
908 request = self.pool.get('res.request')
909 if not isinstance(ids,list): ids = [ids]
910 for task in self.browse(cr, uid, ids, context=context):
912 project = task.project_id
914 # Send request to project manager
915 if project.warn_manager and project.user_id and (project.user_id.id != uid):
916 request.create(cr, uid, {
917 'name': _("Task '%s' closed") % task.name,
920 'act_to': project.user_id.id,
921 'ref_partner_id': task.partner_id.id,
922 'ref_doc1': 'project.task,%d'% (task.id,),
923 'ref_doc2': 'project.project,%d'% (project.id,),
926 for parent_id in task.parent_ids:
927 if parent_id.state in ('pending','draft'):
929 for child in parent_id.child_ids:
930 if child.id != task.id and child.state not in ('done','cancelled'):
933 self.do_reopen(cr, uid, [parent_id.id], context=context)
934 vals.update({'state': 'done'})
935 vals.update({'remaining_hours': 0.0})
936 if not task.date_end:
937 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
938 self.write(cr, uid, [task.id],vals, context=context)
939 self.do_close_send_note(cr, uid, [task.id], context)
942 def do_reopen(self, cr, uid, ids, context=None):
943 request = self.pool.get('res.request')
945 for task in self.browse(cr, uid, ids, context=context):
946 project = task.project_id
947 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
948 request.create(cr, uid, {
949 'name': _("Task '%s' set in progress") % task.name,
952 'act_to': project.user_id.id,
953 'ref_partner_id': task.partner_id.id,
954 'ref_doc1': 'project.task,%d' % task.id,
955 'ref_doc2': 'project.project,%d' % project.id,
958 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
959 self.do_open_send_note(cr, uid, [task.id], context)
962 def do_cancel(self, cr, uid, ids, context={}):
963 request = self.pool.get('res.request')
964 tasks = self.browse(cr, uid, ids, context=context)
965 self._check_child_task(cr, uid, ids, context=context)
967 project = task.project_id
968 if project.warn_manager and project.user_id and (project.user_id.id != uid):
969 request.create(cr, uid, {
970 'name': _("Task '%s' cancelled") % task.name,
973 'act_to': project.user_id.id,
974 'ref_partner_id': task.partner_id.id,
975 'ref_doc1': 'project.task,%d' % task.id,
976 'ref_doc2': 'project.project,%d' % project.id,
978 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
979 self.do_cancel_send_note(cr, uid, [task.id], context)
982 def do_open(self, cr, uid, ids, context={}):
983 if not isinstance(ids,list): ids = [ids]
984 tasks= self.browse(cr, uid, ids, context=context)
986 data = {'state': 'open'}
988 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
989 self.write(cr, uid, [t.id], data, context=context)
990 self.do_open_send_note(cr, uid, [t.id], context)
993 def do_draft(self, cr, uid, ids, context={}):
994 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
995 self.do_draft_send_note(cr, uid, ids, context)
999 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1000 attachment = self.pool.get('ir.attachment')
1001 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1002 new_attachment_ids = []
1003 for attachment_id in attachment_ids:
1004 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1005 return new_attachment_ids
1008 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1010 Delegate Task to another users.
1012 assert delegate_data['user_id'], _("Delegated User should be specified")
1013 delegated_tasks = {}
1014 for task in self.browse(cr, uid, ids, context=context):
1015 delegated_task_id = self.copy(cr, uid, task.id, {
1016 'name': delegate_data['name'],
1017 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1018 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1019 'planned_hours': delegate_data['planned_hours'] or 0.0,
1020 'parent_ids': [(6, 0, [task.id])],
1022 'description': delegate_data['new_task_description'] or '',
1026 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1027 newname = delegate_data['prefix'] or ''
1029 'remaining_hours': delegate_data['planned_hours_me'],
1030 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1033 if delegate_data['state'] == 'pending':
1034 self.do_pending(cr, uid, [task.id], context=context)
1035 elif delegate_data['state'] == 'done':
1036 self.do_close(cr, uid, [task.id], context=context)
1037 self.do_delegation_send_note(cr, uid, [task.id], context)
1038 delegated_tasks[task.id] = delegated_task_id
1039 return delegated_tasks
1041 def do_pending(self, cr, uid, ids, context={}):
1042 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1043 self.do_pending_send_note(cr, uid, ids, context)
1046 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1047 for task in self.browse(cr, uid, ids, context=context):
1048 if (task.state=='draft') or (task.planned_hours==0.0):
1049 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1050 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1053 def set_remaining_time_1(self, cr, uid, ids, context=None):
1054 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1056 def set_remaining_time_2(self, cr, uid, ids, context=None):
1057 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1059 def set_remaining_time_5(self, cr, uid, ids, context=None):
1060 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1062 def set_remaining_time_10(self, cr, uid, ids, context=None):
1063 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1065 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1066 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1068 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1069 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1071 def set_kanban_state_done(self, cr, uid, ids, context=None):
1072 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1074 def _change_type(self, cr, uid, ids, next, context=None):
1076 go to the next stage
1077 if next is False, go to previous stage
1079 for task in self.browse(cr, uid, ids):
1080 if task.project_id.type_ids:
1081 typeid = task.type_id.id
1083 for type in task.project_id.type_ids :
1084 types_seq[type.id] = type.sequence
1086 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1088 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1089 sorted_types = [x[0] for x in types]
1091 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1092 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1093 index = sorted_types.index(typeid)
1094 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1095 self.state_change_send_note(cr, uid, [task.id], context)
1098 def next_type(self, cr, uid, ids, context=None):
1099 return self._change_type(cr, uid, ids, True, context=context)
1101 def prev_type(self, cr, uid, ids, context=None):
1102 return self._change_type(cr, uid, ids, False, context=context)
1104 def _store_history(self, cr, uid, ids, context=None):
1105 for task in self.browse(cr, uid, ids, context=context):
1106 self.pool.get('project.task.history').create(cr, uid, {
1108 'remaining_hours': task.remaining_hours,
1109 'planned_hours': task.planned_hours,
1110 'kanban_state': task.kanban_state,
1111 'type_id': task.type_id.id,
1112 'state': task.state,
1113 'user_id': task.user_id.id
1118 def create(self, cr, uid, vals, context=None):
1119 task_id = super(task, self).create(cr, uid, vals, context=context)
1120 self._store_history(cr, uid, [task_id], context=context)
1121 self.create_send_note(cr, uid, [task_id], context=context)
1124 # Overridden to reset the kanban_state to normal whenever
1125 # the stage (type_id) of the task changes.
1126 def write(self, cr, uid, ids, vals, context=None):
1127 if isinstance(ids, (int, long)):
1129 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1130 new_stage = vals.get('type_id')
1131 vals_reset_kstate = dict(vals, kanban_state='normal')
1132 for t in self.browse(cr, uid, ids, context=context):
1133 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1134 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1137 result = super(task,self).write(cr, uid, ids, vals, context=context)
1138 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1139 self._store_history(cr, uid, ids, context=context)
1140 self.state_change_send_note(cr, uid, ids, context)
1143 def unlink(self, cr, uid, ids, context=None):
1146 self._check_child_task(cr, uid, ids, context=context)
1147 res = super(task, self).unlink(cr, uid, ids, context)
1150 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1151 context = context or {}
1155 if task.state in ('done','cancelled'):
1160 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1162 for t2 in task.parent_ids:
1163 start.append("up.Task_%s.end" % (t2.id,))
1167 ''' % (ident,','.join(start))
1172 ''' % (ident, 'User_'+str(task.user_id.id))
1177 # ---------------------------------------------------
1178 # OpenChatter methods and notifications
1179 # ---------------------------------------------------
1181 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1182 result = dict.fromkeys(ids, [])
1183 for obj in self.browse(cr, uid, ids, context=context):
1184 if obj.state == 'draft' and obj.user_id:
1185 result[obj.id] = [obj.user_id.id]
1188 def message_get_subscribers(self, cr, uid, ids, context=None):
1189 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1190 for obj in self.browse(cr, uid, ids, context=context):
1192 sub_ids.append(obj.user_id.id)
1194 sub_ids.append(obj.manager_id.id)
1195 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1197 def create_send_note(self, cr, uid, ids, context=None):
1198 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1200 def do_pending_send_note(self, cr, uid, ids, context=None):
1201 if not isinstance(ids,list): ids = [ids]
1202 msg = _('Task is now <b>pending</b>.')
1203 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1205 def do_open_send_note(self, cr, uid, ids, context=None):
1206 msg = _('Task has been <b>opened</b>.')
1207 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1209 def do_cancel_send_note(self, cr, uid, ids, context=None):
1210 msg = _('Task has been <b>canceled</b>.')
1211 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1213 def do_close_send_note(self, cr, uid, ids, context=None):
1214 msg = _('Task has been <b>closed</b>.')
1215 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1217 def do_draft_send_note(self, cr, uid, ids, context=None):
1218 msg = _('Task has been <b>renewed</b>.')
1219 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1221 def do_delegation_send_note(self, cr, uid, ids, context=None):
1222 for task in self.browse(cr, uid, ids, context=context):
1223 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1224 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1227 def state_change_send_note(self, cr, uid, ids, context=None):
1228 for task in self.browse(cr, uid, ids, context=context):
1229 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1230 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1235 class project_work(osv.osv):
1236 _name = "project.task.work"
1237 _description = "Project Task Work"
1239 'name': fields.char('Work summary', size=128),
1240 'date': fields.datetime('Date', select="1"),
1241 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1242 'hours': fields.float('Time Spent'),
1243 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1244 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1248 'user_id': lambda obj, cr, uid, context: uid,
1249 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1252 _order = "date desc"
1253 def create(self, cr, uid, vals, *args, **kwargs):
1254 if 'hours' in vals and (not vals['hours']):
1255 vals['hours'] = 0.00
1256 if 'task_id' in vals:
1257 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1258 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1260 def write(self, cr, uid, ids, vals, context=None):
1261 if 'hours' in vals and (not vals['hours']):
1262 vals['hours'] = 0.00
1264 for work in self.browse(cr, uid, ids, context=context):
1265 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))
1266 return super(project_work,self).write(cr, uid, ids, vals, context)
1268 def unlink(self, cr, uid, ids, *args, **kwargs):
1269 for work in self.browse(cr, uid, ids):
1270 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1271 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1274 class account_analytic_account(osv.osv):
1276 _inherit = 'account.analytic.account'
1277 _description = 'Analytic Account'
1279 def create(self, cr, uid, vals, context=None):
1282 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1283 vals['child_ids'] = []
1284 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1286 def unlink(self, cr, uid, ids, *args, **kwargs):
1287 project_obj = self.pool.get('project.project')
1288 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1290 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1291 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1293 account_analytic_account()
1296 # Tasks History, used for cumulative flow charts (Lean/Agile)
1299 class project_task_history(osv.osv):
1300 _name = 'project.task.history'
1301 _description = 'History of Tasks'
1302 _rec_name = 'task_id'
1304 def _get_date(self, cr, uid, ids, name, arg, context=None):
1306 for history in self.browse(cr, uid, ids, context=context):
1307 if history.state in ('done','cancelled'):
1308 result[history.id] = history.date
1310 cr.execute('''select
1313 project_task_history
1317 order by id limit 1''', (history.task_id.id, history.id))
1319 result[history.id] = res and res[0] or False
1322 def _get_related_date(self, cr, uid, ids, context=None):
1324 for history in self.browse(cr, uid, ids, context=context):
1325 cr.execute('''select
1328 project_task_history
1332 order by id desc limit 1''', (history.task_id.id, history.id))
1335 result.append(res[0])
1339 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1340 'type_id': fields.many2one('project.task.type', 'Stage'),
1341 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1342 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1343 'date': fields.date('Date', select=True),
1344 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1345 'project.task.history': (_get_related_date, None, 20)
1347 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1348 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1349 'user_id': fields.many2one('res.users', 'Responsible'),
1352 'date': fields.date.context_today,
1354 project_task_history()
1356 class project_task_history_cumulative(osv.osv):
1357 _name = 'project.task.history.cumulative'
1358 _table = 'project_task_history_cumulative'
1359 _inherit = 'project.task.history'
1362 'end_date': fields.date('End Date'),
1363 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1366 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1368 history.date::varchar||'-'||history.history_id::varchar as id,
1369 history.date as end_date,
1374 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1375 task_id, type_id, user_id, kanban_state, state,
1376 remaining_hours, planned_hours
1378 project_task_history
1382 project_task_history_cumulative()