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 _TASK_STATE = [('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')]
37 class project_task_type(osv.osv):
38 _name = 'project.task.type'
39 _description = 'Task Stage'
42 'name': fields.char('Stage Name', required=True, size=64, translate=True),
43 'description': fields.text('Description'),
44 'sequence': fields.integer('Sequence'),
45 '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."),
46 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
47 'state': fields.selection(_TASK_STATE, 'State', required=True,
48 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.\
49 \n If the task is over, the states is set to \'Done\'.'),
58 class project(osv.osv):
59 _name = "project.project"
60 _description = "Project"
61 _inherits = {'account.analytic.account': "analytic_account_id"}
62 _inherit = ['ir.needaction_mixin', 'mail.thread']
64 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
66 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
67 if context and context.get('user_preference'):
68 cr.execute("""SELECT project.id FROM project_project project
69 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
70 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
71 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
72 return [(r[0]) for r in cr.fetchall()]
73 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
74 context=context, count=count)
76 def _complete_name(self, cr, uid, ids, name, args, context=None):
78 for m in self.browse(cr, uid, ids, context=context):
79 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
82 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
83 partner_obj = self.pool.get('res.partner')
87 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
88 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
89 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
90 val['pricelist_id'] = pricelist_id
93 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
94 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
95 project_ids = [task.project_id.id for task in tasks if task.project_id]
96 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
98 def _get_project_and_parents(self, cr, uid, ids, context=None):
99 """ return the project ids and all their parent projects """
103 SELECT DISTINCT parent.id
104 FROM project_project project, project_project parent, account_analytic_account account
105 WHERE project.analytic_account_id = account.id
106 AND parent.analytic_account_id = account.parent_id
109 ids = [t[0] for t in cr.fetchall()]
113 def _get_project_and_children(self, cr, uid, ids, context=None):
114 """ retrieve all children projects of project ids;
115 return a dictionary mapping each project to its parent project (or None)
117 res = dict.fromkeys(ids, None)
120 SELECT project.id, parent.id
121 FROM project_project project, project_project parent, account_analytic_account account
122 WHERE project.analytic_account_id = account.id
123 AND parent.analytic_account_id = account.parent_id
126 dic = dict(cr.fetchall())
131 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
132 child_parent = self._get_project_and_children(cr, uid, ids, context)
133 # compute planned_hours, total_hours, effective_hours specific to each project
135 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
136 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
137 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
139 """, (tuple(child_parent.keys()),))
140 # aggregate results into res
141 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
142 for id, planned, total, effective in cr.fetchall():
143 # add the values specific to id to all parent projects of id in the result
146 res[id]['planned_hours'] += planned
147 res[id]['total_hours'] += total
148 res[id]['effective_hours'] += effective
149 id = child_parent[id]
150 # compute progress rates
152 if res[id]['total_hours']:
153 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
155 res[id]['progress_rate'] = 0.0
158 def unlink(self, cr, uid, ids, *args, **kwargs):
159 for proj in self.browse(cr, uid, ids):
161 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
162 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
165 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
166 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
167 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
168 '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),
169 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
170 '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)]}),
172 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
173 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)]}),
174 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
175 '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.",
177 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
178 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
180 '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.",
182 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
183 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
185 '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.",
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 '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.",
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 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
196 '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)]}),
197 '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)]}),
198 '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)]}),
199 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
201 def _get_type_common(self, cr, uid, context):
202 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
210 'type_ids': _get_type_common
213 # TODO: Why not using a SQL contraints ?
214 def _check_dates(self, cr, uid, ids, context=None):
215 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
216 if leave['date_start'] and leave['date']:
217 if leave['date_start'] > leave['date']:
222 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
225 def set_template(self, cr, uid, ids, context=None):
226 res = self.setActive(cr, uid, ids, value=False, context=context)
229 def set_done(self, cr, uid, ids, context=None):
230 task_obj = self.pool.get('project.task')
231 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
232 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
233 self.write(cr, uid, ids, {'state':'close'}, context=context)
234 self.set_close_send_note(cr, uid, ids, context=context)
237 def set_cancel(self, cr, uid, ids, context=None):
238 task_obj = self.pool.get('project.task')
239 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
240 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
241 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
242 self.set_cancel_send_note(cr, uid, ids, context=context)
245 def set_pending(self, cr, uid, ids, context=None):
246 self.write(cr, uid, ids, {'state':'pending'}, context=context)
247 self.set_pending_send_note(cr, uid, ids, context=context)
250 def set_open(self, cr, uid, ids, context=None):
251 self.write(cr, uid, ids, {'state':'open'}, context=context)
252 self.set_open_send_note(cr, uid, ids, context=context)
255 def reset_project(self, cr, uid, ids, context=None):
256 res = self.setActive(cr, uid, ids, value=True, context=context)
257 self.set_open_send_note(cr, uid, ids, context=context)
260 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
261 """ copy and map tasks from old to new project """
265 task_obj = self.pool.get('project.task')
266 proj = self.browse(cr, uid, old_project_id, context=context)
267 for task in proj.tasks:
268 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
269 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
270 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
273 def copy(self, cr, uid, id, default={}, context=None):
277 default = default or {}
278 context['active_test'] = False
279 default['state'] = 'open'
280 default['tasks'] = []
281 proj = self.browse(cr, uid, id, context=context)
282 if not default.get('name', False):
283 default['name'] = proj.name + _(' (copy)')
285 res = super(project, self).copy(cr, uid, id, default, context)
286 self.map_tasks(cr,uid,id,res,context)
289 def duplicate_template(self, cr, uid, ids, context=None):
292 data_obj = self.pool.get('ir.model.data')
294 for proj in self.browse(cr, uid, ids, context=context):
295 parent_id = context.get('parent_id', False)
296 context.update({'analytic_project_copy': True})
297 new_date_start = time.strftime('%Y-%m-%d')
299 if proj.date_start and proj.date:
300 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
301 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
302 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
303 context.update({'copy':True})
304 new_id = self.copy(cr, uid, proj.id, default = {
305 'name': proj.name +_(' (copy)'),
307 'date_start':new_date_start,
309 'parent_id':parent_id}, context=context)
310 result.append(new_id)
312 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
313 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
315 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
317 if result and len(result):
319 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
320 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
321 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
322 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
323 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
324 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
326 'name': _('Projects'),
328 'view_mode': 'form,tree',
329 'res_model': 'project.project',
332 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
333 'type': 'ir.actions.act_window',
334 'search_view_id': search_view['res_id'],
338 # set active value for a project, its sub projects and its tasks
339 def setActive(self, cr, uid, ids, value=True, context=None):
340 task_obj = self.pool.get('project.task')
341 for proj in self.browse(cr, uid, ids, context=None):
342 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
343 cr.execute('select id from project_task where project_id=%s', (proj.id,))
344 tasks_id = [x[0] for x in cr.fetchall()]
346 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
347 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
349 self.setActive(cr, uid, child_ids, value, context=None)
352 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
353 context = context or {}
354 if type(ids) in (long, int,):
356 projects = self.browse(cr, uid, ids, context=context)
358 for project in projects:
359 if (not project.members) and force_members:
360 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
362 resource_pool = self.pool.get('resource.resource')
364 result = "from openerp.addons.resource.faces import *\n"
365 result += "import datetime\n"
366 for project in self.browse(cr, uid, ids, context=context):
367 u_ids = [i.id for i in project.members]
368 if project.user_id and (project.user_id.id not in u_ids):
369 u_ids.append(project.user_id.id)
370 for task in project.tasks:
371 if task.state in ('done','cancelled'):
373 if task.user_id and (task.user_id.id not in u_ids):
374 u_ids.append(task.user_id.id)
375 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
376 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
377 for key, vals in resource_objs.items():
379 class User_%s(Resource):
381 ''' % (key, vals.get('efficiency', False))
388 def _schedule_project(self, cr, uid, project, context=None):
389 resource_pool = self.pool.get('resource.resource')
390 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
391 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
392 # TODO: check if we need working_..., default values are ok.
393 puids = [x.id for x in project.members]
395 puids.append(project.user_id.id)
403 project.date_start, working_days,
404 '|'.join(['User_'+str(x) for x in puids])
406 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
413 #TODO: DO Resource allocation and compute availability
414 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
420 def schedule_tasks(self, cr, uid, ids, context=None):
421 context = context or {}
422 if type(ids) in (long, int,):
424 projects = self.browse(cr, uid, ids, context=context)
425 result = self._schedule_header(cr, uid, ids, False, context=context)
426 for project in projects:
427 result += self._schedule_project(cr, uid, project, context=context)
428 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
431 exec result in local_dict
432 projects_gantt = Task.BalancedProject(local_dict['Project'])
434 for project in projects:
435 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
436 for task in project.tasks:
437 if task.state in ('done','cancelled'):
440 p = getattr(project_gantt, 'Task_%d' % (task.id,))
442 self.pool.get('project.task').write(cr, uid, [task.id], {
443 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
444 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
446 if (not task.user_id) and (p.booked_resource):
447 self.pool.get('project.task').write(cr, uid, [task.id], {
448 'user_id': int(p.booked_resource[0].name[5:]),
452 # ------------------------------------------------
453 # OpenChatter methods and notifications
454 # ------------------------------------------------
456 def get_needaction_user_ids(self, cr, uid, ids, context=None):
457 result = dict.fromkeys(ids)
458 for obj in self.browse(cr, uid, ids, context=context):
460 if obj.state == 'draft' and obj.user_id:
461 result[obj.id] = [obj.user_id.id]
464 def message_get_subscribers(self, cr, uid, ids, context=None):
465 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
466 for obj in self.browse(cr, uid, ids, context=context):
468 sub_ids.append(obj.user_id.id)
469 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
471 def create(self, cr, uid, vals, context=None):
472 obj_id = super(project, self).create(cr, uid, vals, context=context)
473 self.create_send_note(cr, uid, [obj_id], context=context)
476 def create_send_note(self, cr, uid, ids, context=None):
477 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
479 def set_open_send_note(self, cr, uid, ids, context=None):
480 message = _("Project has been <b>opened</b>.")
481 return self.message_append_note(cr, uid, ids, body=message, context=context)
483 def set_pending_send_note(self, cr, uid, ids, context=None):
484 message = _("Project is now <b>pending</b>.")
485 return self.message_append_note(cr, uid, ids, body=message, context=context)
487 def set_cancel_send_note(self, cr, uid, ids, context=None):
488 message = _("Project has been <b>cancelled</b>.")
489 return self.message_append_note(cr, uid, ids, body=message, context=context)
491 def set_close_send_note(self, cr, uid, ids, context=None):
492 message = _("Project has been <b>closed</b>.")
493 return self.message_append_note(cr, uid, ids, body=message, context=context)
497 class users(osv.osv):
498 _inherit = 'res.users'
500 'context_project_id': fields.many2one('project.project', 'Project')
505 _name = "project.task"
506 _description = "Task"
508 _date_name = "date_start"
509 _inherit = ['ir.needaction_mixin', 'mail.thread']
512 def _resolve_project_id_from_context(self, cr, uid, context=None):
513 """Return ID of project based on the value of 'project_id'
514 context key, or None if it cannot be resolved to a single project.
516 if context is None: context = {}
517 if type(context.get('project_id')) in (int, long):
518 project_id = context['project_id']
520 if isinstance(context.get('project_id'), basestring):
521 project_name = context['project_id']
522 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
523 if len(project_ids) == 1:
524 return project_ids[0][0]
526 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
527 stage_obj = self.pool.get('project.task.type')
528 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
529 order = stage_obj._order
530 access_rights_uid = access_rights_uid or uid
531 if read_group_order == 'type_id desc':
532 # lame way to allow reverting search, should just work in the trivial case
533 order = '%s desc' % order
535 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
537 domain = ['|', ('id','in',ids), ('project_default','=',1)]
538 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
539 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
540 # restore order of the search
541 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
544 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
545 res_users = self.pool.get('res.users')
546 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
547 access_rights_uid = access_rights_uid or uid
549 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
550 order = res_users._order
551 # lame way to allow reverting search, should just work in the trivial case
552 if read_group_order == 'user_id desc':
553 order = '%s desc' % order
554 # de-duplicate and apply search order
555 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
556 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
557 # restore order of the search
558 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
562 'type_id': _read_group_type_id,
563 'user_id': _read_group_user_id
567 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
568 obj_project = self.pool.get('project.project')
570 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
571 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
572 if id and isinstance(id, (long, int)):
573 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
574 args.append(('active', '=', False))
575 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
577 def _str_get(self, task, level=0, border='***', context=None):
578 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'+ \
579 border[0]+' '+(task.name or '')+'\n'+ \
580 (task.description or '')+'\n\n'
582 # Compute: effective_hours, total_hours, progress
583 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
585 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
586 hours = dict(cr.fetchall())
587 for task in self.browse(cr, uid, ids, context=context):
588 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)}
589 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
590 res[task.id]['progress'] = 0.0
591 if (task.remaining_hours + hours.get(task.id, 0.0)):
592 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
593 if task.state in ('done','cancelled'):
594 res[task.id]['progress'] = 100.0
598 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
599 if remaining and not planned:
600 return {'value':{'planned_hours': remaining}}
603 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
604 return {'value':{'remaining_hours': planned - effective}}
606 def onchange_project(self, cr, uid, id, project_id):
609 data = self.pool.get('project.project').browse(cr, uid, [project_id])
610 partner_id=data and data[0].partner_id
612 return {'value':{'partner_id':partner_id.id}}
615 def duplicate_task(self, cr, uid, map_ids, context=None):
616 for new in map_ids.values():
617 task = self.browse(cr, uid, new, context)
618 child_ids = [ ch.id for ch in task.child_ids]
620 for child in task.child_ids:
621 if child.id in map_ids.keys():
622 child_ids.remove(child.id)
623 child_ids.append(map_ids[child.id])
625 parent_ids = [ ch.id for ch in task.parent_ids]
627 for parent in task.parent_ids:
628 if parent.id in map_ids.keys():
629 parent_ids.remove(parent.id)
630 parent_ids.append(map_ids[parent.id])
631 #FIXME why there is already the copy and the old one
632 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
634 def copy_data(self, cr, uid, id, default={}, context=None):
635 default = default or {}
636 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
637 if not default.get('remaining_hours', False):
638 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
639 default['active'] = True
640 default['type_id'] = False
641 if not default.get('name', False):
642 default['name'] = self.browse(cr, uid, id, context=context).name or ''
643 if not context.get('copy',False):
644 new_name = _("%s (copy)")%default.get('name','')
645 default.update({'name':new_name})
646 return super(task, self).copy_data(cr, uid, id, default, context)
649 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
651 for task in self.browse(cr, uid, ids, context=context):
654 if task.project_id.active == False or task.project_id.state == 'template':
658 def _get_task(self, cr, uid, ids, context=None):
660 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
661 if work.task_id: result[work.task_id.id] = True
664 def _get_state(self, cr, uid, ids, name, arg, context=None):
666 for task in self.browse(cr, uid, ids, context=context):
668 res[task.id] = task.type_id.state
671 def _get_stage(self, cr, uid, ids, context=None):
672 task_obj = self.pool.get('project.task')
674 for stage in self.browse(cr, uid, ids, context=context):
676 task_ids = task_obj.search(cr, uid, [('state', '=', stage.state)], context=context)
677 for task in task_obj.browse(cr, uid, task_ids, context=context):
678 result[task.id] = True
681 def _save_state(self, cr, uid, task_id, field_name, field_value, arg, context=None):
682 stage_obj = self.pool.get('project.task.type')
683 stage_ids = stage_obj.search(cr, uid, [('state', '=', field_value)], context=context)
685 self.write(cr, uid, task_id, {'type_id': stage_ids[0]}, context=context)
687 cr.execute("""update project_task set state=%s where id=%s""", (field_value, task_id, ))
691 '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."),
692 'name': fields.char('Task Summary', size=128, required=True, select=True),
693 'description': fields.text('Description'),
694 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
695 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
696 'type_id': fields.many2one('project.task.type', 'Stage'),
697 'state': fields.function(_get_state, fnct_inv=_save_state, type='selection', selection=_TASK_STATE, string="State", readonly=True,
699 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['type_id'], 10),
700 'project.task.type': (_get_stage, ['state'], 10)
702 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
703 help="A task's kanban state indicates special situations affecting it:\n"
704 " * Normal is the default situation\n"
705 " * Blocked indicates something is preventing the progress of this task\n"
706 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
707 readonly=True, required=False),
708 'create_date': fields.datetime('Create Date', readonly=True,select=True),
709 'date_start': fields.datetime('Starting Date',select=True),
710 'date_end': fields.datetime('Ending Date',select=True),
711 'date_deadline': fields.date('Deadline',select=True),
712 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
713 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
714 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
715 'notes': fields.text('Notes'),
716 '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.'),
717 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
719 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
720 'project.task.work': (_get_task, ['hours'], 10),
722 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
723 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
725 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
726 'project.task.work': (_get_task, ['hours'], 10),
728 '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",
730 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
731 'project.task.work': (_get_task, ['hours'], 10),
733 '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.",
735 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
736 'project.task.work': (_get_task, ['hours'], 10),
738 'user_id': fields.many2one('res.users', 'Assigned to'),
739 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
740 'partner_id': fields.many2one('res.partner', 'Partner'),
741 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
742 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
743 'company_id': fields.many2one('res.company', 'Company'),
744 'id': fields.integer('ID', readonly=True),
745 'color': fields.integer('Color Index'),
746 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
751 'kanban_state': 'normal',
756 'user_id': lambda obj, cr, uid, context: uid,
757 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
760 _order = "priority, sequence, date_start, name, id"
762 def set_priority(self, cr, uid, ids, priority):
765 return self.write(cr, uid, ids, {'priority' : priority})
767 def set_high_priority(self, cr, uid, ids, *args):
768 """Set task priority to high
770 return self.set_priority(cr, uid, ids, '1')
772 def set_normal_priority(self, cr, uid, ids, *args):
773 """Set task priority to normal
775 return self.set_priority(cr, uid, ids, '2')
777 def _check_recursion(self, cr, uid, ids, context=None):
779 visited_branch = set()
781 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
787 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
788 if id in visited_branch: #Cycle
791 if id in visited_node: #Already tested don't work one more time for nothing
794 visited_branch.add(id)
797 #visit child using DFS
798 task = self.browse(cr, uid, id, context=context)
799 for child in task.child_ids:
800 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
804 visited_branch.remove(id)
807 def _check_dates(self, cr, uid, ids, context=None):
810 obj_task = self.browse(cr, uid, ids[0], context=context)
811 start = obj_task.date_start or False
812 end = obj_task.date_end or False
819 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
820 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
823 # Override view according to the company definition
825 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
826 users_obj = self.pool.get('res.users')
828 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
829 # this should be safe (no context passed to avoid side-effects)
830 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
831 tm = obj_tm and obj_tm.name or 'Hours'
833 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
835 if tm in ['Hours','Hour']:
838 eview = etree.fromstring(res['arch'])
840 def _check_rec(eview):
841 if eview.attrib.get('widget','') == 'float_time':
842 eview.set('widget','float')
849 res['arch'] = etree.tostring(eview)
851 for f in res['fields']:
852 if 'Hours' in res['fields'][f]['string']:
853 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
856 def _check_child_task(self, cr, uid, ids, context=None):
859 tasks = self.browse(cr, uid, ids, context=context)
862 for child in task.child_ids:
863 if child.state in ['draft', 'open', 'pending']:
864 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
867 def action_close(self, cr, uid, ids, context=None):
868 # This action open wizard to send email to partner or project manager after close task.
871 task_id = len(ids) and ids[0] or False
872 self._check_child_task(cr, uid, ids, context=context)
873 if not task_id: return False
874 task = self.browse(cr, uid, task_id, context=context)
875 project = task.project_id
876 res = self.do_close(cr, uid, [task_id], context=context)
877 if project.warn_manager or project.warn_customer:
879 'name': _('Send Email after close task'),
882 'res_model': 'mail.compose.message',
883 'type': 'ir.actions.act_window',
886 'context': {'active_id': task.id,
887 'active_model': 'project.task'}
891 def do_close(self, cr, uid, ids, context={}):
895 request = self.pool.get('res.request')
896 if not isinstance(ids,list): ids = [ids]
897 for task in self.browse(cr, uid, ids, context=context):
899 project = task.project_id
901 # Send request to project manager
902 if project.warn_manager and project.user_id and (project.user_id.id != uid):
903 request.create(cr, uid, {
904 'name': _("Task '%s' closed") % task.name,
907 'act_to': project.user_id.id,
908 'ref_partner_id': task.partner_id.id,
909 'ref_doc1': 'project.task,%d'% (task.id,),
910 'ref_doc2': 'project.project,%d'% (project.id,),
913 for parent_id in task.parent_ids:
914 if parent_id.state in ('pending','draft'):
916 for child in parent_id.child_ids:
917 if child.id != task.id and child.state not in ('done','cancelled'):
920 self.do_reopen(cr, uid, [parent_id.id], context=context)
921 vals.update({'state': 'done'})
922 vals.update({'remaining_hours': 0.0})
923 if not task.date_end:
924 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
925 self.write(cr, uid, [task.id],vals, context=context)
926 self.do_close_send_note(cr, uid, [task.id], context)
929 def do_reopen(self, cr, uid, ids, context=None):
930 request = self.pool.get('res.request')
932 for task in self.browse(cr, uid, ids, context=context):
933 project = task.project_id
934 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
935 request.create(cr, uid, {
936 'name': _("Task '%s' set in progress") % task.name,
939 'act_to': project.user_id.id,
940 'ref_partner_id': task.partner_id.id,
941 'ref_doc1': 'project.task,%d' % task.id,
942 'ref_doc2': 'project.project,%d' % project.id,
945 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
946 self.do_open_send_note(cr, uid, [task.id], context)
949 def do_cancel(self, cr, uid, ids, context={}):
950 request = self.pool.get('res.request')
951 tasks = self.browse(cr, uid, ids, context=context)
952 self._check_child_task(cr, uid, ids, context=context)
954 project = task.project_id
955 if project.warn_manager and project.user_id and (project.user_id.id != uid):
956 request.create(cr, uid, {
957 'name': _("Task '%s' cancelled") % task.name,
960 'act_to': project.user_id.id,
961 'ref_partner_id': task.partner_id.id,
962 'ref_doc1': 'project.task,%d' % task.id,
963 'ref_doc2': 'project.project,%d' % project.id,
965 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
966 self.do_cancel_send_note(cr, uid, [task.id], context)
969 def do_open(self, cr, uid, ids, context={}):
970 if not isinstance(ids,list): ids = [ids]
971 tasks= self.browse(cr, uid, ids, context=context)
973 data = {'state': 'open'}
975 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
976 self.write(cr, uid, [t.id], data, context=context)
977 self.do_open_send_note(cr, uid, [t.id], context)
980 def do_draft(self, cr, uid, ids, context={}):
981 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
982 self.do_draft_send_note(cr, uid, ids, context)
986 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
987 attachment = self.pool.get('ir.attachment')
988 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
989 new_attachment_ids = []
990 for attachment_id in attachment_ids:
991 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
992 return new_attachment_ids
995 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
997 Delegate Task to another users.
999 assert delegate_data['user_id'], _("Delegated User should be specified")
1000 delegated_tasks = {}
1001 for task in self.browse(cr, uid, ids, context=context):
1002 delegated_task_id = self.copy(cr, uid, task.id, {
1003 'name': delegate_data['name'],
1004 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1005 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1006 'planned_hours': delegate_data['planned_hours'] or 0.0,
1007 'parent_ids': [(6, 0, [task.id])],
1009 'description': delegate_data['new_task_description'] or '',
1013 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1014 newname = delegate_data['prefix'] or ''
1016 'remaining_hours': delegate_data['planned_hours_me'],
1017 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1020 if delegate_data['state'] == 'pending':
1021 self.do_pending(cr, uid, [task.id], context=context)
1022 elif delegate_data['state'] == 'done':
1023 self.do_close(cr, uid, [task.id], context=context)
1024 self.do_delegation_send_note(cr, uid, [task.id], context)
1025 delegated_tasks[task.id] = delegated_task_id
1026 return delegated_tasks
1028 def do_pending(self, cr, uid, ids, context={}):
1029 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1030 self.do_pending_send_note(cr, uid, ids, context)
1033 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1034 for task in self.browse(cr, uid, ids, context=context):
1035 if (task.state=='draft') or (task.planned_hours==0.0):
1036 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1037 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1040 def set_remaining_time_1(self, cr, uid, ids, context=None):
1041 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1043 def set_remaining_time_2(self, cr, uid, ids, context=None):
1044 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1046 def set_remaining_time_5(self, cr, uid, ids, context=None):
1047 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1049 def set_remaining_time_10(self, cr, uid, ids, context=None):
1050 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1052 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1053 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1055 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1056 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1058 def set_kanban_state_done(self, cr, uid, ids, context=None):
1059 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1061 def _change_type(self, cr, uid, ids, next, context=None):
1063 go to the next stage
1064 if next is False, go to previous stage
1066 for task in self.browse(cr, uid, ids):
1067 if task.project_id.type_ids:
1068 typeid = task.type_id.id
1070 for type in task.project_id.type_ids :
1071 types_seq[type.id] = type.sequence
1073 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1075 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1076 sorted_types = [x[0] for x in types]
1078 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1079 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1080 index = sorted_types.index(typeid)
1081 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1082 self.state_change_send_note(cr, uid, [task.id], context)
1085 def next_type(self, cr, uid, ids, context=None):
1086 return self._change_type(cr, uid, ids, True, context=context)
1088 def prev_type(self, cr, uid, ids, context=None):
1089 return self._change_type(cr, uid, ids, False, context=context)
1091 def _store_history(self, cr, uid, ids, context=None):
1092 for task in self.browse(cr, uid, ids, context=context):
1093 self.pool.get('project.task.history').create(cr, uid, {
1095 'remaining_hours': task.remaining_hours,
1096 'planned_hours': task.planned_hours,
1097 'kanban_state': task.kanban_state,
1098 'type_id': task.type_id.id,
1099 'state': task.state,
1100 'user_id': task.user_id.id
1105 def create(self, cr, uid, vals, context=None):
1106 task_id = super(task, self).create(cr, uid, vals, context=context)
1107 self._store_history(cr, uid, [task_id], context=context)
1108 self.create_send_note(cr, uid, [task_id], context=context)
1111 # Overridden to reset the kanban_state to normal whenever
1112 # the stage (type_id) of the task changes.
1113 def write(self, cr, uid, ids, vals, context=None):
1114 if isinstance(ids, (int, long)):
1116 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1117 new_stage = vals.get('type_id')
1118 vals_reset_kstate = dict(vals, kanban_state='normal')
1119 for t in self.browse(cr, uid, ids, context=context):
1120 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1121 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1124 result = super(task,self).write(cr, uid, ids, vals, context=context)
1125 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1126 self._store_history(cr, uid, ids, context=context)
1127 self.state_change_send_note(cr, uid, ids, context)
1130 def unlink(self, cr, uid, ids, context=None):
1133 self._check_child_task(cr, uid, ids, context=context)
1134 res = super(task, self).unlink(cr, uid, ids, context)
1137 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1138 context = context or {}
1142 if task.state in ('done','cancelled'):
1147 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1149 for t2 in task.parent_ids:
1150 start.append("up.Task_%s.end" % (t2.id,))
1154 ''' % (ident,','.join(start))
1159 ''' % (ident, 'User_'+str(task.user_id.id))
1164 # ---------------------------------------------------
1165 # OpenChatter methods and notifications
1166 # ---------------------------------------------------
1168 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1169 result = dict.fromkeys(ids, [])
1170 for obj in self.browse(cr, uid, ids, context=context):
1171 if obj.state == 'draft' and obj.user_id:
1172 result[obj.id] = [obj.user_id.id]
1175 def message_get_subscribers(self, cr, uid, ids, context=None):
1176 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1177 for obj in self.browse(cr, uid, ids, context=context):
1179 sub_ids.append(obj.user_id.id)
1181 sub_ids.append(obj.manager_id.id)
1182 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1184 def create_send_note(self, cr, uid, ids, context=None):
1185 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1187 def do_pending_send_note(self, cr, uid, ids, context=None):
1188 if not isinstance(ids,list): ids = [ids]
1189 msg = _('Task is now <b>pending</b>.')
1190 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1192 def do_open_send_note(self, cr, uid, ids, context=None):
1193 msg = _('Task has been <b>opened</b>.')
1194 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1196 def do_cancel_send_note(self, cr, uid, ids, context=None):
1197 msg = _('Task has been <b>canceled</b>.')
1198 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1200 def do_close_send_note(self, cr, uid, ids, context=None):
1201 msg = _('Task has been <b>closed</b>.')
1202 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1204 def do_draft_send_note(self, cr, uid, ids, context=None):
1205 msg = _('Task has been <b>renewed</b>.')
1206 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1208 def do_delegation_send_note(self, cr, uid, ids, context=None):
1209 for task in self.browse(cr, uid, ids, context=context):
1210 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1211 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1214 def state_change_send_note(self, cr, uid, ids, context=None):
1215 for task in self.browse(cr, uid, ids, context=context):
1216 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1217 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1222 class project_work(osv.osv):
1223 _name = "project.task.work"
1224 _description = "Project Task Work"
1226 'name': fields.char('Work summary', size=128),
1227 'date': fields.datetime('Date', select="1"),
1228 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1229 'hours': fields.float('Time Spent'),
1230 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1231 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1235 'user_id': lambda obj, cr, uid, context: uid,
1236 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1239 _order = "date desc"
1240 def create(self, cr, uid, vals, *args, **kwargs):
1241 if 'hours' in vals and (not vals['hours']):
1242 vals['hours'] = 0.00
1243 if 'task_id' in vals:
1244 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1245 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1247 def write(self, cr, uid, ids, vals, context=None):
1248 if 'hours' in vals and (not vals['hours']):
1249 vals['hours'] = 0.00
1251 for work in self.browse(cr, uid, ids, context=context):
1252 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))
1253 return super(project_work,self).write(cr, uid, ids, vals, context)
1255 def unlink(self, cr, uid, ids, *args, **kwargs):
1256 for work in self.browse(cr, uid, ids):
1257 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1258 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1261 class account_analytic_account(osv.osv):
1263 _inherit = 'account.analytic.account'
1264 _description = 'Analytic Account'
1266 def create(self, cr, uid, vals, context=None):
1269 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1270 vals['child_ids'] = []
1271 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1273 def unlink(self, cr, uid, ids, *args, **kwargs):
1274 project_obj = self.pool.get('project.project')
1275 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1277 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1278 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1280 account_analytic_account()
1283 # Tasks History, used for cumulative flow charts (Lean/Agile)
1286 class project_task_history(osv.osv):
1287 _name = 'project.task.history'
1288 _description = 'History of Tasks'
1289 _rec_name = 'task_id'
1291 def _get_date(self, cr, uid, ids, name, arg, context=None):
1293 for history in self.browse(cr, uid, ids, context=context):
1294 if history.state in ('done','cancelled'):
1295 result[history.id] = history.date
1297 cr.execute('''select
1300 project_task_history
1304 order by id limit 1''', (history.task_id.id, history.id))
1306 result[history.id] = res and res[0] or False
1309 def _get_related_date(self, cr, uid, ids, context=None):
1311 for history in self.browse(cr, uid, ids, context=context):
1312 cr.execute('''select
1315 project_task_history
1319 order by id desc limit 1''', (history.task_id.id, history.id))
1322 result.append(res[0])
1326 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1327 'type_id': fields.many2one('project.task.type', 'Stage'),
1328 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1329 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1330 'date': fields.date('Date', select=True),
1331 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1332 'project.task.history': (_get_related_date, None, 20)
1334 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1335 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1336 'user_id': fields.many2one('res.users', 'Responsible'),
1339 'date': fields.date.context_today,
1341 project_task_history()
1343 class project_task_history_cumulative(osv.osv):
1344 _name = 'project.task.history.cumulative'
1345 _table = 'project_task_history_cumulative'
1346 _inherit = 'project.task.history'
1349 'end_date': fields.date('End Date'),
1350 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1353 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1355 history.date::varchar||'-'||history.history_id::varchar as id,
1356 history.date as end_date,
1361 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1362 task_id, type_id, user_id, kanban_state, state,
1363 remaining_hours, planned_hours
1365 project_task_history
1369 project_task_history_cumulative()