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, help='This state is related to stage.'),
56 class project(osv.osv):
57 _name = "project.project"
58 _description = "Project"
59 _inherits = {'account.analytic.account': "analytic_account_id"}
60 _inherit = ['ir.needaction_mixin', 'mail.thread']
62 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
64 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
65 if context and context.get('user_preference'):
66 cr.execute("""SELECT project.id FROM project_project project
67 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
68 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
69 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
70 return [(r[0]) for r in cr.fetchall()]
71 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
72 context=context, count=count)
74 def _complete_name(self, cr, uid, ids, name, args, context=None):
76 for m in self.browse(cr, uid, ids, context=context):
77 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
80 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
81 partner_obj = self.pool.get('res.partner')
85 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
86 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
87 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
88 val['pricelist_id'] = pricelist_id
91 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
92 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
93 project_ids = [task.project_id.id for task in tasks if task.project_id]
94 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
96 def _get_project_and_parents(self, cr, uid, ids, context=None):
97 """ return the project ids and all their parent projects """
101 SELECT DISTINCT parent.id
102 FROM project_project project, project_project parent, account_analytic_account account
103 WHERE project.analytic_account_id = account.id
104 AND parent.analytic_account_id = account.parent_id
107 ids = [t[0] for t in cr.fetchall()]
111 def _get_project_and_children(self, cr, uid, ids, context=None):
112 """ retrieve all children projects of project ids;
113 return a dictionary mapping each project to its parent project (or None)
115 res = dict.fromkeys(ids, None)
118 SELECT project.id, parent.id
119 FROM project_project project, project_project parent, account_analytic_account account
120 WHERE project.analytic_account_id = account.id
121 AND parent.analytic_account_id = account.parent_id
124 dic = dict(cr.fetchall())
129 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
130 child_parent = self._get_project_and_children(cr, uid, ids, context)
131 # compute planned_hours, total_hours, effective_hours specific to each project
133 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
134 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
135 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
137 """, (tuple(child_parent.keys()),))
138 # aggregate results into res
139 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
140 for id, planned, total, effective in cr.fetchall():
141 # add the values specific to id to all parent projects of id in the result
144 res[id]['planned_hours'] += planned
145 res[id]['total_hours'] += total
146 res[id]['effective_hours'] += effective
147 id = child_parent[id]
148 # compute progress rates
150 if res[id]['total_hours']:
151 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
153 res[id]['progress_rate'] = 0.0
156 def unlink(self, cr, uid, ids, *args, **kwargs):
157 for proj in self.browse(cr, uid, ids):
159 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
160 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
163 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
164 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
165 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
166 '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),
167 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
168 '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)]}),
170 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
171 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)]}),
172 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
173 '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.",
175 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
176 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
178 '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.",
180 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
181 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
183 '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.",
185 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
186 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
188 '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.",
190 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
191 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
193 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
194 '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)]}),
195 '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)]}),
196 '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)]}),
197 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
199 def _get_type_common(self, cr, uid, context):
200 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
208 'type_ids': _get_type_common
211 # TODO: Why not using a SQL contraints ?
212 def _check_dates(self, cr, uid, ids, context=None):
213 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
214 if leave['date_start'] and leave['date']:
215 if leave['date_start'] > leave['date']:
220 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
223 def set_template(self, cr, uid, ids, context=None):
224 res = self.setActive(cr, uid, ids, value=False, context=context)
227 def set_done(self, cr, uid, ids, context=None):
228 task_obj = self.pool.get('project.task')
229 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
230 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
231 self.write(cr, uid, ids, {'state':'close'}, context=context)
232 self.set_close_send_note(cr, uid, ids, context=context)
235 def set_cancel(self, cr, uid, ids, context=None):
236 task_obj = self.pool.get('project.task')
237 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
238 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
239 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
240 self.set_cancel_send_note(cr, uid, ids, context=context)
243 def set_pending(self, cr, uid, ids, context=None):
244 self.write(cr, uid, ids, {'state':'pending'}, context=context)
245 self.set_pending_send_note(cr, uid, ids, context=context)
248 def set_open(self, cr, uid, ids, context=None):
249 self.write(cr, uid, ids, {'state':'open'}, context=context)
250 self.set_open_send_note(cr, uid, ids, context=context)
253 def reset_project(self, cr, uid, ids, context=None):
254 res = self.setActive(cr, uid, ids, value=True, context=context)
255 self.set_open_send_note(cr, uid, ids, context=context)
258 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
259 """ copy and map tasks from old to new project """
263 task_obj = self.pool.get('project.task')
264 proj = self.browse(cr, uid, old_project_id, context=context)
265 for task in proj.tasks:
266 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
267 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
268 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
271 def copy(self, cr, uid, id, default={}, context=None):
275 default = default or {}
276 context['active_test'] = False
277 default['state'] = 'open'
278 default['tasks'] = []
279 proj = self.browse(cr, uid, id, context=context)
280 if not default.get('name', False):
281 default['name'] = proj.name + _(' (copy)')
283 res = super(project, self).copy(cr, uid, id, default, context)
284 self.map_tasks(cr,uid,id,res,context)
287 def duplicate_template(self, cr, uid, ids, context=None):
290 data_obj = self.pool.get('ir.model.data')
292 for proj in self.browse(cr, uid, ids, context=context):
293 parent_id = context.get('parent_id', False)
294 context.update({'analytic_project_copy': True})
295 new_date_start = time.strftime('%Y-%m-%d')
297 if proj.date_start and proj.date:
298 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
299 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
300 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
301 context.update({'copy':True})
302 new_id = self.copy(cr, uid, proj.id, default = {
303 'name': proj.name +_(' (copy)'),
305 'date_start':new_date_start,
307 'parent_id':parent_id}, context=context)
308 result.append(new_id)
310 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
311 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
313 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
315 if result and len(result):
317 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
318 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
319 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
320 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
321 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
322 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
324 'name': _('Projects'),
326 'view_mode': 'form,tree',
327 'res_model': 'project.project',
330 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
331 'type': 'ir.actions.act_window',
332 'search_view_id': search_view['res_id'],
336 # set active value for a project, its sub projects and its tasks
337 def setActive(self, cr, uid, ids, value=True, context=None):
338 task_obj = self.pool.get('project.task')
339 for proj in self.browse(cr, uid, ids, context=None):
340 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
341 cr.execute('select id from project_task where project_id=%s', (proj.id,))
342 tasks_id = [x[0] for x in cr.fetchall()]
344 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
345 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
347 self.setActive(cr, uid, child_ids, value, context=None)
350 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
351 context = context or {}
352 if type(ids) in (long, int,):
354 projects = self.browse(cr, uid, ids, context=context)
356 for project in projects:
357 if (not project.members) and force_members:
358 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
360 resource_pool = self.pool.get('resource.resource')
362 result = "from openerp.addons.resource.faces import *\n"
363 result += "import datetime\n"
364 for project in self.browse(cr, uid, ids, context=context):
365 u_ids = [i.id for i in project.members]
366 if project.user_id and (project.user_id.id not in u_ids):
367 u_ids.append(project.user_id.id)
368 for task in project.tasks:
369 if task.state in ('done','cancelled'):
371 if task.user_id and (task.user_id.id not in u_ids):
372 u_ids.append(task.user_id.id)
373 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
374 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
375 for key, vals in resource_objs.items():
377 class User_%s(Resource):
379 ''' % (key, vals.get('efficiency', False))
386 def _schedule_project(self, cr, uid, project, context=None):
387 resource_pool = self.pool.get('resource.resource')
388 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
389 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
390 # TODO: check if we need working_..., default values are ok.
391 puids = [x.id for x in project.members]
393 puids.append(project.user_id.id)
401 project.date_start, working_days,
402 '|'.join(['User_'+str(x) for x in puids])
404 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
411 #TODO: DO Resource allocation and compute availability
412 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
418 def schedule_tasks(self, cr, uid, ids, context=None):
419 context = context or {}
420 if type(ids) in (long, int,):
422 projects = self.browse(cr, uid, ids, context=context)
423 result = self._schedule_header(cr, uid, ids, False, context=context)
424 for project in projects:
425 result += self._schedule_project(cr, uid, project, context=context)
426 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
429 exec result in local_dict
430 projects_gantt = Task.BalancedProject(local_dict['Project'])
432 for project in projects:
433 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
434 for task in project.tasks:
435 if task.state in ('done','cancelled'):
438 p = getattr(project_gantt, 'Task_%d' % (task.id,))
440 self.pool.get('project.task').write(cr, uid, [task.id], {
441 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
442 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
444 if (not task.user_id) and (p.booked_resource):
445 self.pool.get('project.task').write(cr, uid, [task.id], {
446 'user_id': int(p.booked_resource[0].name[5:]),
450 # ------------------------------------------------
451 # OpenChatter methods and notifications
452 # ------------------------------------------------
454 def get_needaction_user_ids(self, cr, uid, ids, context=None):
455 result = dict.fromkeys(ids)
456 for obj in self.browse(cr, uid, ids, context=context):
458 if obj.state == 'draft' and obj.user_id:
459 result[obj.id] = [obj.user_id.id]
462 def message_get_subscribers(self, cr, uid, ids, context=None):
463 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
464 for obj in self.browse(cr, uid, ids, context=context):
466 sub_ids.append(obj.user_id.id)
467 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
469 def create(self, cr, uid, vals, context=None):
470 obj_id = super(project, self).create(cr, uid, vals, context=context)
471 self.create_send_note(cr, uid, [obj_id], context=context)
474 def create_send_note(self, cr, uid, ids, context=None):
475 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
477 def set_open_send_note(self, cr, uid, ids, context=None):
478 message = _("Project has been <b>opened</b>.")
479 return self.message_append_note(cr, uid, ids, body=message, context=context)
481 def set_pending_send_note(self, cr, uid, ids, context=None):
482 message = _("Project is now <b>pending</b>.")
483 return self.message_append_note(cr, uid, ids, body=message, context=context)
485 def set_cancel_send_note(self, cr, uid, ids, context=None):
486 message = _("Project has been <b>cancelled</b>.")
487 return self.message_append_note(cr, uid, ids, body=message, context=context)
489 def set_close_send_note(self, cr, uid, ids, context=None):
490 message = _("Project has been <b>closed</b>.")
491 return self.message_append_note(cr, uid, ids, body=message, context=context)
495 class users(osv.osv):
496 _inherit = 'res.users'
498 'context_project_id': fields.many2one('project.project', 'Project')
503 _name = "project.task"
504 _description = "Task"
506 _date_name = "date_start"
507 _inherit = ['ir.needaction_mixin', 'mail.thread']
510 def _resolve_project_id_from_context(self, cr, uid, context=None):
511 """Return ID of project based on the value of 'project_id'
512 context key, or None if it cannot be resolved to a single project.
514 if context is None: context = {}
515 if type(context.get('project_id')) in (int, long):
516 project_id = context['project_id']
518 if isinstance(context.get('project_id'), basestring):
519 project_name = context['project_id']
520 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
521 if len(project_ids) == 1:
522 return project_ids[0][0]
524 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
525 stage_obj = self.pool.get('project.task.type')
526 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
527 order = stage_obj._order
528 access_rights_uid = access_rights_uid or uid
529 if read_group_order == 'type_id desc':
530 # lame way to allow reverting search, should just work in the trivial case
531 order = '%s desc' % order
533 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
535 domain = ['|', ('id','in',ids), ('project_default','=',1)]
536 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
537 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
538 # restore order of the search
539 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
542 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
543 res_users = self.pool.get('res.users')
544 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
545 access_rights_uid = access_rights_uid or uid
547 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
548 order = res_users._order
549 # lame way to allow reverting search, should just work in the trivial case
550 if read_group_order == 'user_id desc':
551 order = '%s desc' % order
552 # de-duplicate and apply search order
553 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
554 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
555 # restore order of the search
556 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
560 'type_id': _read_group_type_id,
561 'user_id': _read_group_user_id
565 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
566 obj_project = self.pool.get('project.project')
568 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
569 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
570 if id and isinstance(id, (long, int)):
571 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
572 args.append(('active', '=', False))
573 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
575 def _str_get(self, task, level=0, border='***', context=None):
576 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'+ \
577 border[0]+' '+(task.name or '')+'\n'+ \
578 (task.description or '')+'\n\n'
580 # Compute: effective_hours, total_hours, progress
581 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
583 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
584 hours = dict(cr.fetchall())
585 for task in self.browse(cr, uid, ids, context=context):
586 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)}
587 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
588 res[task.id]['progress'] = 0.0
589 if (task.remaining_hours + hours.get(task.id, 0.0)):
590 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
591 if task.state in ('done','cancelled'):
592 res[task.id]['progress'] = 100.0
596 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
597 if remaining and not planned:
598 return {'value':{'planned_hours': remaining}}
601 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
602 return {'value':{'remaining_hours': planned - effective}}
604 def onchange_project(self, cr, uid, id, project_id):
607 data = self.pool.get('project.project').browse(cr, uid, [project_id])
608 partner_id=data and data[0].partner_id
610 return {'value':{'partner_id':partner_id.id}}
613 def duplicate_task(self, cr, uid, map_ids, context=None):
614 for new in map_ids.values():
615 task = self.browse(cr, uid, new, context)
616 child_ids = [ ch.id for ch in task.child_ids]
618 for child in task.child_ids:
619 if child.id in map_ids.keys():
620 child_ids.remove(child.id)
621 child_ids.append(map_ids[child.id])
623 parent_ids = [ ch.id for ch in task.parent_ids]
625 for parent in task.parent_ids:
626 if parent.id in map_ids.keys():
627 parent_ids.remove(parent.id)
628 parent_ids.append(map_ids[parent.id])
629 #FIXME why there is already the copy and the old one
630 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
632 def copy_data(self, cr, uid, id, default={}, context=None):
633 default = default or {}
634 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
635 if not default.get('remaining_hours', False):
636 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
637 default['active'] = True
638 default['type_id'] = False
639 if not default.get('name', False):
640 default['name'] = self.browse(cr, uid, id, context=context).name or ''
641 if not context.get('copy',False):
642 new_name = _("%s (copy)")%default.get('name','')
643 default.update({'name':new_name})
644 return super(task, self).copy_data(cr, uid, id, default, context)
647 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
649 for task in self.browse(cr, uid, ids, context=context):
652 if task.project_id.active == False or task.project_id.state == 'template':
656 def _get_task(self, cr, uid, ids, context=None):
658 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
659 if work.task_id: result[work.task_id.id] = True
662 def _get_state(self, cr, uid, ids, name, arg, context=None):
664 for task in self.browse(cr, uid, ids, context=context):
666 res[task.id] = task.type_id.state
669 def _get_stage(self, cr, uid, ids, context=None):
670 task_obj = self.pool.get('project.task')
672 for stage in self.browse(cr, uid, ids, context=context):
674 task_ids = task_obj.search(cr, uid, [('state', '=', stage.state)], context=context)
675 for task in task_obj.browse(cr, uid, task_ids, context=context):
676 result[task.id] = True
679 def _save_state(self, cr, uid, task_id, field_name, field_value, arg, context=None):
680 stage_obj = self.pool.get('project.task.type')
681 stage_ids = stage_obj.search(cr, uid, [('state', '=', field_value)], context=context)
683 self.write(cr, uid, task_id, {'type_id': stage_ids[0]}, context=context)
685 cr.execute("""update project_task set state=%s where id=%s""", (field_value, task_id, ))
689 '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."),
690 'name': fields.char('Task Summary', size=128, required=True, select=True),
691 'description': fields.text('Description'),
692 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
693 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
694 'type_id': fields.many2one('project.task.type', 'Stage'),
695 'state': fields.function(_get_state, fnct_inv=_save_state, type='selection', selection=_TASK_STATE, string="State", readonly=True,
697 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['type_id'], 10),
698 'project.task.type': (_get_stage, ['state'], 10)
699 }, 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.\
700 \n If the task is over, the states is set to \'Done\'.'),
701 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
702 help="A task's kanban state indicates special situations affecting it:\n"
703 " * Normal is the default situation\n"
704 " * Blocked indicates something is preventing the progress of this task\n"
705 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
706 readonly=True, required=False),
707 'create_date': fields.datetime('Create Date', readonly=True,select=True),
708 'date_start': fields.datetime('Starting Date',select=True),
709 'date_end': fields.datetime('Ending Date',select=True),
710 'date_deadline': fields.date('Deadline',select=True),
711 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
712 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
713 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
714 'notes': fields.text('Notes'),
715 '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.'),
716 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
718 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
719 'project.task.work': (_get_task, ['hours'], 10),
721 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
722 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
724 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
725 'project.task.work': (_get_task, ['hours'], 10),
727 '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",
729 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
730 'project.task.work': (_get_task, ['hours'], 10),
732 '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.",
734 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
735 'project.task.work': (_get_task, ['hours'], 10),
737 'user_id': fields.many2one('res.users', 'Assigned to'),
738 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
739 'partner_id': fields.many2one('res.partner', 'Partner'),
740 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
741 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
742 'company_id': fields.many2one('res.company', 'Company'),
743 'id': fields.integer('ID', readonly=True),
744 'color': fields.integer('Color Index'),
745 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
750 'kanban_state': 'normal',
755 'user_id': lambda obj, cr, uid, context: uid,
756 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
759 _order = "priority, sequence, date_start, name, id"
761 def set_priority(self, cr, uid, ids, priority):
764 return self.write(cr, uid, ids, {'priority' : priority})
766 def set_high_priority(self, cr, uid, ids, *args):
767 """Set task priority to high
769 return self.set_priority(cr, uid, ids, '1')
771 def set_normal_priority(self, cr, uid, ids, *args):
772 """Set task priority to normal
774 return self.set_priority(cr, uid, ids, '2')
776 def _check_recursion(self, cr, uid, ids, context=None):
778 visited_branch = set()
780 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
786 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
787 if id in visited_branch: #Cycle
790 if id in visited_node: #Already tested don't work one more time for nothing
793 visited_branch.add(id)
796 #visit child using DFS
797 task = self.browse(cr, uid, id, context=context)
798 for child in task.child_ids:
799 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
803 visited_branch.remove(id)
806 def _check_dates(self, cr, uid, ids, context=None):
809 obj_task = self.browse(cr, uid, ids[0], context=context)
810 start = obj_task.date_start or False
811 end = obj_task.date_end or False
818 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
819 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
822 # Override view according to the company definition
824 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
825 users_obj = self.pool.get('res.users')
827 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
828 # this should be safe (no context passed to avoid side-effects)
829 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
830 tm = obj_tm and obj_tm.name or 'Hours'
832 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
834 if tm in ['Hours','Hour']:
837 eview = etree.fromstring(res['arch'])
839 def _check_rec(eview):
840 if eview.attrib.get('widget','') == 'float_time':
841 eview.set('widget','float')
848 res['arch'] = etree.tostring(eview)
850 for f in res['fields']:
851 if 'Hours' in res['fields'][f]['string']:
852 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
855 def _check_child_task(self, cr, uid, ids, context=None):
858 tasks = self.browse(cr, uid, ids, context=context)
861 for child in task.child_ids:
862 if child.state in ['draft', 'open', 'pending']:
863 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
866 def action_close(self, cr, uid, ids, context=None):
867 # This action open wizard to send email to partner or project manager after close task.
870 task_id = len(ids) and ids[0] or False
871 self._check_child_task(cr, uid, ids, context=context)
872 if not task_id: return False
873 task = self.browse(cr, uid, task_id, context=context)
874 project = task.project_id
875 res = self.do_close(cr, uid, [task_id], context=context)
876 if project.warn_manager or project.warn_customer:
878 'name': _('Send Email after close task'),
881 'res_model': 'mail.compose.message',
882 'type': 'ir.actions.act_window',
885 'context': {'active_id': task.id,
886 'active_model': 'project.task'}
890 def do_close(self, cr, uid, ids, context={}):
894 request = self.pool.get('res.request')
895 if not isinstance(ids,list): ids = [ids]
896 for task in self.browse(cr, uid, ids, context=context):
898 project = task.project_id
900 # Send request to project manager
901 if project.warn_manager and project.user_id and (project.user_id.id != uid):
902 request.create(cr, uid, {
903 'name': _("Task '%s' closed") % task.name,
906 'act_to': project.user_id.id,
907 'ref_partner_id': task.partner_id.id,
908 'ref_doc1': 'project.task,%d'% (task.id,),
909 'ref_doc2': 'project.project,%d'% (project.id,),
912 for parent_id in task.parent_ids:
913 if parent_id.state in ('pending','draft'):
915 for child in parent_id.child_ids:
916 if child.id != task.id and child.state not in ('done','cancelled'):
919 self.do_reopen(cr, uid, [parent_id.id], context=context)
920 vals.update({'state': 'done'})
921 vals.update({'remaining_hours': 0.0})
922 if not task.date_end:
923 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
924 self.write(cr, uid, [task.id],vals, context=context)
925 self.do_close_send_note(cr, uid, [task.id], context)
928 def do_reopen(self, cr, uid, ids, context=None):
929 request = self.pool.get('res.request')
931 for task in self.browse(cr, uid, ids, context=context):
932 project = task.project_id
933 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
934 request.create(cr, uid, {
935 'name': _("Task '%s' set in progress") % task.name,
938 'act_to': project.user_id.id,
939 'ref_partner_id': task.partner_id.id,
940 'ref_doc1': 'project.task,%d' % task.id,
941 'ref_doc2': 'project.project,%d' % project.id,
944 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
945 self.do_open_send_note(cr, uid, [task.id], context)
948 def do_cancel(self, cr, uid, ids, context={}):
949 request = self.pool.get('res.request')
950 tasks = self.browse(cr, uid, ids, context=context)
951 self._check_child_task(cr, uid, ids, context=context)
953 project = task.project_id
954 if project.warn_manager and project.user_id and (project.user_id.id != uid):
955 request.create(cr, uid, {
956 'name': _("Task '%s' cancelled") % task.name,
959 'act_to': project.user_id.id,
960 'ref_partner_id': task.partner_id.id,
961 'ref_doc1': 'project.task,%d' % task.id,
962 'ref_doc2': 'project.project,%d' % project.id,
964 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
965 self.do_cancel_send_note(cr, uid, [task.id], context)
968 def do_open(self, cr, uid, ids, context={}):
969 if not isinstance(ids,list): ids = [ids]
970 tasks= self.browse(cr, uid, ids, context=context)
972 data = {'state': 'open'}
974 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
975 self.write(cr, uid, [t.id], data, context=context)
976 self.do_open_send_note(cr, uid, [t.id], context)
979 def do_draft(self, cr, uid, ids, context={}):
980 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
981 self.do_draft_send_note(cr, uid, ids, context)
985 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
986 attachment = self.pool.get('ir.attachment')
987 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
988 new_attachment_ids = []
989 for attachment_id in attachment_ids:
990 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
991 return new_attachment_ids
994 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
996 Delegate Task to another users.
998 assert delegate_data['user_id'], _("Delegated User should be specified")
1000 for task in self.browse(cr, uid, ids, context=context):
1001 delegated_task_id = self.copy(cr, uid, task.id, {
1002 'name': delegate_data['name'],
1003 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1004 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1005 'planned_hours': delegate_data['planned_hours'] or 0.0,
1006 'parent_ids': [(6, 0, [task.id])],
1008 'description': delegate_data['new_task_description'] or '',
1012 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1013 newname = delegate_data['prefix'] or ''
1015 'remaining_hours': delegate_data['planned_hours_me'],
1016 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1019 if delegate_data['state'] == 'pending':
1020 self.do_pending(cr, uid, [task.id], context=context)
1021 elif delegate_data['state'] == 'done':
1022 self.do_close(cr, uid, [task.id], context=context)
1023 self.do_delegation_send_note(cr, uid, [task.id], context)
1024 delegated_tasks[task.id] = delegated_task_id
1025 return delegated_tasks
1027 def do_pending(self, cr, uid, ids, context={}):
1028 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1029 self.do_pending_send_note(cr, uid, ids, context)
1032 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1033 for task in self.browse(cr, uid, ids, context=context):
1034 if (task.state=='draft') or (task.planned_hours==0.0):
1035 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1036 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1039 def set_remaining_time_1(self, cr, uid, ids, context=None):
1040 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1042 def set_remaining_time_2(self, cr, uid, ids, context=None):
1043 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1045 def set_remaining_time_5(self, cr, uid, ids, context=None):
1046 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1048 def set_remaining_time_10(self, cr, uid, ids, context=None):
1049 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1051 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1052 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1054 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1055 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1057 def set_kanban_state_done(self, cr, uid, ids, context=None):
1058 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1060 def _change_type(self, cr, uid, ids, next, context=None):
1062 go to the next stage
1063 if next is False, go to previous stage
1065 for task in self.browse(cr, uid, ids):
1066 if task.project_id.type_ids:
1067 typeid = task.type_id.id
1069 for type in task.project_id.type_ids :
1070 types_seq[type.id] = type.sequence
1072 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1074 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1075 sorted_types = [x[0] for x in types]
1077 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1078 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1079 index = sorted_types.index(typeid)
1080 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1081 self.state_change_send_note(cr, uid, [task.id], context)
1084 def next_type(self, cr, uid, ids, context=None):
1085 return self._change_type(cr, uid, ids, True, context=context)
1087 def prev_type(self, cr, uid, ids, context=None):
1088 return self._change_type(cr, uid, ids, False, context=context)
1090 def _store_history(self, cr, uid, ids, context=None):
1091 for task in self.browse(cr, uid, ids, context=context):
1092 self.pool.get('project.task.history').create(cr, uid, {
1094 'remaining_hours': task.remaining_hours,
1095 'planned_hours': task.planned_hours,
1096 'kanban_state': task.kanban_state,
1097 'type_id': task.type_id.id,
1098 'state': task.state,
1099 'user_id': task.user_id.id
1104 def create(self, cr, uid, vals, context=None):
1105 task_id = super(task, self).create(cr, uid, vals, context=context)
1106 self._store_history(cr, uid, [task_id], context=context)
1107 self.create_send_note(cr, uid, [task_id], context=context)
1110 # Overridden to reset the kanban_state to normal whenever
1111 # the stage (type_id) of the task changes.
1112 def write(self, cr, uid, ids, vals, context=None):
1113 if isinstance(ids, (int, long)):
1115 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1116 new_stage = vals.get('type_id')
1117 vals_reset_kstate = dict(vals, kanban_state='normal')
1118 for t in self.browse(cr, uid, ids, context=context):
1119 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1120 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1123 result = super(task,self).write(cr, uid, ids, vals, context=context)
1124 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1125 self._store_history(cr, uid, ids, context=context)
1126 self.state_change_send_note(cr, uid, ids, context)
1129 def unlink(self, cr, uid, ids, context=None):
1132 self._check_child_task(cr, uid, ids, context=context)
1133 res = super(task, self).unlink(cr, uid, ids, context)
1136 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1137 context = context or {}
1141 if task.state in ('done','cancelled'):
1146 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1148 for t2 in task.parent_ids:
1149 start.append("up.Task_%s.end" % (t2.id,))
1153 ''' % (ident,','.join(start))
1158 ''' % (ident, 'User_'+str(task.user_id.id))
1163 # ---------------------------------------------------
1164 # OpenChatter methods and notifications
1165 # ---------------------------------------------------
1167 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1168 result = dict.fromkeys(ids, [])
1169 for obj in self.browse(cr, uid, ids, context=context):
1170 if obj.state == 'draft' and obj.user_id:
1171 result[obj.id] = [obj.user_id.id]
1174 def message_get_subscribers(self, cr, uid, ids, context=None):
1175 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1176 for obj in self.browse(cr, uid, ids, context=context):
1178 sub_ids.append(obj.user_id.id)
1180 sub_ids.append(obj.manager_id.id)
1181 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1183 def create_send_note(self, cr, uid, ids, context=None):
1184 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1186 def do_pending_send_note(self, cr, uid, ids, context=None):
1187 if not isinstance(ids,list): ids = [ids]
1188 msg = _('Task is now <b>pending</b>.')
1189 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1191 def do_open_send_note(self, cr, uid, ids, context=None):
1192 msg = _('Task has been <b>opened</b>.')
1193 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1195 def do_cancel_send_note(self, cr, uid, ids, context=None):
1196 msg = _('Task has been <b>canceled</b>.')
1197 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1199 def do_close_send_note(self, cr, uid, ids, context=None):
1200 msg = _('Task has been <b>closed</b>.')
1201 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1203 def do_draft_send_note(self, cr, uid, ids, context=None):
1204 msg = _('Task has been <b>renewed</b>.')
1205 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1207 def do_delegation_send_note(self, cr, uid, ids, context=None):
1208 for task in self.browse(cr, uid, ids, context=context):
1209 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1210 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1213 def state_change_send_note(self, cr, uid, ids, context=None):
1214 for task in self.browse(cr, uid, ids, context=context):
1215 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1216 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1221 class project_work(osv.osv):
1222 _name = "project.task.work"
1223 _description = "Project Task Work"
1225 'name': fields.char('Work summary', size=128),
1226 'date': fields.datetime('Date', select="1"),
1227 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1228 'hours': fields.float('Time Spent'),
1229 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1230 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1234 'user_id': lambda obj, cr, uid, context: uid,
1235 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1238 _order = "date desc"
1239 def create(self, cr, uid, vals, *args, **kwargs):
1240 if 'hours' in vals and (not vals['hours']):
1241 vals['hours'] = 0.00
1242 if 'task_id' in vals:
1243 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1244 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1246 def write(self, cr, uid, ids, vals, context=None):
1247 if 'hours' in vals and (not vals['hours']):
1248 vals['hours'] = 0.00
1250 for work in self.browse(cr, uid, ids, context=context):
1251 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))
1252 return super(project_work,self).write(cr, uid, ids, vals, context)
1254 def unlink(self, cr, uid, ids, *args, **kwargs):
1255 for work in self.browse(cr, uid, ids):
1256 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1257 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1260 class account_analytic_account(osv.osv):
1262 _inherit = 'account.analytic.account'
1263 _description = 'Analytic Account'
1265 def create(self, cr, uid, vals, context=None):
1268 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1269 vals['child_ids'] = []
1270 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1272 def unlink(self, cr, uid, ids, *args, **kwargs):
1273 project_obj = self.pool.get('project.project')
1274 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1276 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1277 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1279 account_analytic_account()
1282 # Tasks History, used for cumulative flow charts (Lean/Agile)
1285 class project_task_history(osv.osv):
1286 _name = 'project.task.history'
1287 _description = 'History of Tasks'
1288 _rec_name = 'task_id'
1290 def _get_date(self, cr, uid, ids, name, arg, context=None):
1292 for history in self.browse(cr, uid, ids, context=context):
1293 if history.state in ('done','cancelled'):
1294 result[history.id] = history.date
1296 cr.execute('''select
1299 project_task_history
1303 order by id limit 1''', (history.task_id.id, history.id))
1305 result[history.id] = res and res[0] or False
1308 def _get_related_date(self, cr, uid, ids, context=None):
1310 for history in self.browse(cr, uid, ids, context=context):
1311 cr.execute('''select
1314 project_task_history
1318 order by id desc limit 1''', (history.task_id.id, history.id))
1321 result.append(res[0])
1325 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1326 'type_id': fields.many2one('project.task.type', 'Stage'),
1327 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1328 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1329 'date': fields.date('Date', select=True),
1330 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1331 'project.task.history': (_get_related_date, None, 20)
1333 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1334 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1335 'user_id': fields.many2one('res.users', 'Responsible'),
1338 'date': fields.date.context_today,
1340 project_task_history()
1342 class project_task_history_cumulative(osv.osv):
1343 _name = 'project.task.history.cumulative'
1344 _table = 'project_task_history_cumulative'
1345 _inherit = 'project.task.history'
1348 'end_date': fields.date('End Date'),
1349 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1352 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1354 history.date::varchar||'-'||history.history_id::varchar as id,
1355 history.date as end_date,
1360 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1361 task_id, type_id, user_id, kanban_state, state,
1362 remaining_hours, planned_hours
1364 project_task_history
1368 project_task_history_cumulative()