1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from lxml import etree
24 from datetime import datetime, date
26 from tools.translate import _
27 from osv import fields, osv
28 from openerp.addons.resource.faces import task as Task
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 # _name = 'project.project'
35 class project_task_type(osv.osv):
36 _name = 'project.task.type'
37 _description = 'Task Stage'
40 'name': fields.char('Stage Name', required=True, size=64, translate=True),
41 'description': fields.text('Description'),
42 'sequence': fields.integer('Sequence'),
43 'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
52 class project(osv.osv):
53 _name = "project.project"
54 _description = "Project"
55 _inherits = {'account.analytic.account': "analytic_account_id"}
56 _inherit = ['ir.needaction_mixin', 'mail.thread']
58 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
61 if context and context.get('user_preference'):
62 cr.execute("""SELECT project.id FROM project_project project
63 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
64 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
65 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
66 return [(r[0]) for r in cr.fetchall()]
67 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
68 context=context, count=count)
70 def _complete_name(self, cr, uid, ids, name, args, context=None):
72 for m in self.browse(cr, uid, ids, context=context):
73 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
76 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
77 partner_obj = self.pool.get('res.partner')
81 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84 val['pricelist_id'] = pricelist_id
87 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
88 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
89 project_ids = [task.project_id.id for task in tasks if task.project_id]
90 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
92 def _get_project_and_parents(self, cr, uid, ids, context=None):
93 """ return the project ids and all their parent projects """
97 SELECT DISTINCT parent.id
98 FROM project_project project, project_project parent, account_analytic_account account
99 WHERE project.analytic_account_id = account.id
100 AND parent.analytic_account_id = account.parent_id
103 ids = [t[0] for t in cr.fetchall()]
107 def _get_project_and_children(self, cr, uid, ids, context=None):
108 """ retrieve all children projects of project ids;
109 return a dictionary mapping each project to its parent project (or None)
111 res = dict.fromkeys(ids, None)
114 SELECT project.id, parent.id
115 FROM project_project project, project_project parent, account_analytic_account account
116 WHERE project.analytic_account_id = account.id
117 AND parent.analytic_account_id = account.parent_id
120 dic = dict(cr.fetchall())
125 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
126 child_parent = self._get_project_and_children(cr, uid, ids, context)
127 # compute planned_hours, total_hours, effective_hours specific to each project
129 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
130 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
131 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
133 """, (tuple(child_parent.keys()),))
134 # aggregate results into res
135 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
136 for id, planned, total, effective in cr.fetchall():
137 # add the values specific to id to all parent projects of id in the result
140 res[id]['planned_hours'] += planned
141 res[id]['total_hours'] += total
142 res[id]['effective_hours'] += effective
143 id = child_parent[id]
144 # compute progress rates
146 if res[id]['total_hours']:
147 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
149 res[id]['progress_rate'] = 0.0
152 def unlink(self, cr, uid, ids, *args, **kwargs):
153 for proj in self.browse(cr, uid, ids):
155 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
156 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
158 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
159 res = dict.fromkeys(ids, 0)
160 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
161 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
162 res[task.project_id.id] += 1
166 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
167 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
168 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
169 '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),
170 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
171 'warn_manager': fields.boolean('Notify Manager', help="If you check this field, the project manager will receive an email each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
173 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
174 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)]}),
175 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
176 '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.",
178 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
179 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
181 '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.",
183 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
184 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
186 '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.",
188 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
189 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
191 '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.",
193 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
194 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
196 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
197 '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)]}),
198 '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)]}),
199 '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)]}),
200 '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 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"),
202 'color': fields.integer('Color Index'),
205 def dummy(self, cr, uid, ids, context):
208 def _get_type_common(self, cr, uid, context):
209 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
217 'type_ids': _get_type_common,
220 # TODO: Why not using a SQL contraints ?
221 def _check_dates(self, cr, uid, ids, context=None):
222 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
223 if leave['date_start'] and leave['date']:
224 if leave['date_start'] > leave['date']:
229 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
232 def set_template(self, cr, uid, ids, context=None):
233 res = self.setActive(cr, uid, ids, value=False, context=context)
236 def set_done(self, cr, uid, ids, context=None):
237 task_obj = self.pool.get('project.task')
238 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
239 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
240 self.write(cr, uid, ids, {'state':'close'}, context=context)
241 self.set_close_send_note(cr, uid, ids, context=context)
244 def set_cancel(self, cr, uid, ids, context=None):
245 task_obj = self.pool.get('project.task')
246 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
247 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
248 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
249 self.set_cancel_send_note(cr, uid, ids, context=context)
252 def set_pending(self, cr, uid, ids, context=None):
253 self.write(cr, uid, ids, {'state':'pending'}, context=context)
254 self.set_pending_send_note(cr, uid, ids, context=context)
257 def set_open(self, cr, uid, ids, context=None):
258 self.write(cr, uid, ids, {'state':'open'}, context=context)
259 self.set_open_send_note(cr, uid, ids, context=context)
262 def reset_project(self, cr, uid, ids, context=None):
263 res = self.setActive(cr, uid, ids, value=True, context=context)
264 self.set_open_send_note(cr, uid, ids, context=context)
267 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
268 """ copy and map tasks from old to new project """
272 task_obj = self.pool.get('project.task')
273 proj = self.browse(cr, uid, old_project_id, context=context)
274 for task in proj.tasks:
275 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
276 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
277 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
280 def copy(self, cr, uid, id, default={}, context=None):
284 default = default or {}
285 context['active_test'] = False
286 default['state'] = 'open'
287 default['tasks'] = []
288 proj = self.browse(cr, uid, id, context=context)
289 if not default.get('name', False):
290 default['name'] = proj.name + _(' (copy)')
292 res = super(project, self).copy(cr, uid, id, default, context)
293 self.map_tasks(cr,uid,id,res,context)
296 def duplicate_template(self, cr, uid, ids, context=None):
299 data_obj = self.pool.get('ir.model.data')
301 for proj in self.browse(cr, uid, ids, context=context):
302 parent_id = context.get('parent_id', False)
303 context.update({'analytic_project_copy': True})
304 new_date_start = time.strftime('%Y-%m-%d')
306 if proj.date_start and proj.date:
307 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
308 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
309 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
310 context.update({'copy':True})
311 new_id = self.copy(cr, uid, proj.id, default = {
312 'name': proj.name +_(' (copy)'),
314 'date_start':new_date_start,
316 'parent_id':parent_id}, context=context)
317 result.append(new_id)
319 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
320 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
322 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
324 if result and len(result):
326 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
327 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
328 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
329 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
330 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
331 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
333 'name': _('Projects'),
335 'view_mode': 'form,tree',
336 'res_model': 'project.project',
339 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
340 'type': 'ir.actions.act_window',
341 'search_view_id': search_view['res_id'],
345 # set active value for a project, its sub projects and its tasks
346 def setActive(self, cr, uid, ids, value=True, context=None):
347 task_obj = self.pool.get('project.task')
348 for proj in self.browse(cr, uid, ids, context=None):
349 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
350 cr.execute('select id from project_task where project_id=%s', (proj.id,))
351 tasks_id = [x[0] for x in cr.fetchall()]
353 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
354 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
356 self.setActive(cr, uid, child_ids, value, context=None)
359 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
360 context = context or {}
361 if type(ids) in (long, int,):
363 projects = self.browse(cr, uid, ids, context=context)
365 for project in projects:
366 if (not project.members) and force_members:
367 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
369 resource_pool = self.pool.get('resource.resource')
371 result = "from openerp.addons.resource.faces import *\n"
372 result += "import datetime\n"
373 for project in self.browse(cr, uid, ids, context=context):
374 u_ids = [i.id for i in project.members]
375 if project.user_id and (project.user_id.id not in u_ids):
376 u_ids.append(project.user_id.id)
377 for task in project.tasks:
378 if task.state in ('done','cancelled'):
380 if task.user_id and (task.user_id.id not in u_ids):
381 u_ids.append(task.user_id.id)
382 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
383 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
384 for key, vals in resource_objs.items():
386 class User_%s(Resource):
388 ''' % (key, vals.get('efficiency', False))
395 def _schedule_project(self, cr, uid, project, context=None):
396 resource_pool = self.pool.get('resource.resource')
397 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
398 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
399 # TODO: check if we need working_..., default values are ok.
400 puids = [x.id for x in project.members]
402 puids.append(project.user_id.id)
410 project.date_start, working_days,
411 '|'.join(['User_'+str(x) for x in puids])
413 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
420 #TODO: DO Resource allocation and compute availability
421 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
427 def schedule_tasks(self, cr, uid, ids, context=None):
428 context = context or {}
429 if type(ids) in (long, int,):
431 projects = self.browse(cr, uid, ids, context=context)
432 result = self._schedule_header(cr, uid, ids, False, context=context)
433 for project in projects:
434 result += self._schedule_project(cr, uid, project, context=context)
435 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
438 exec result in local_dict
439 projects_gantt = Task.BalancedProject(local_dict['Project'])
441 for project in projects:
442 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
443 for task in project.tasks:
444 if task.state in ('done','cancelled'):
447 p = getattr(project_gantt, 'Task_%d' % (task.id,))
449 self.pool.get('project.task').write(cr, uid, [task.id], {
450 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
451 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
453 if (not task.user_id) and (p.booked_resource):
454 self.pool.get('project.task').write(cr, uid, [task.id], {
455 'user_id': int(p.booked_resource[0].name[5:]),
459 # ------------------------------------------------
460 # OpenChatter methods and notifications
461 # ------------------------------------------------
463 def get_needaction_user_ids(self, cr, uid, ids, context=None):
464 result = dict.fromkeys(ids)
465 for obj in self.browse(cr, uid, ids, context=context):
467 if obj.state == 'draft' and obj.user_id:
468 result[obj.id] = [obj.user_id.id]
471 def message_get_subscribers(self, cr, uid, ids, context=None):
472 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
473 for obj in self.browse(cr, uid, ids, context=context):
475 sub_ids.append(obj.user_id.id)
476 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
478 def create(self, cr, uid, vals, context=None):
479 obj_id = super(project, self).create(cr, uid, vals, context=context)
480 self.create_send_note(cr, uid, [obj_id], context=context)
483 def create_send_note(self, cr, uid, ids, context=None):
484 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
486 def set_open_send_note(self, cr, uid, ids, context=None):
487 message = _("Project has been <b>opened</b>.")
488 return self.message_append_note(cr, uid, ids, body=message, context=context)
490 def set_pending_send_note(self, cr, uid, ids, context=None):
491 message = _("Project is now <b>pending</b>.")
492 return self.message_append_note(cr, uid, ids, body=message, context=context)
494 def set_cancel_send_note(self, cr, uid, ids, context=None):
495 message = _("Project has been <b>cancelled</b>.")
496 return self.message_append_note(cr, uid, ids, body=message, context=context)
498 def set_close_send_note(self, cr, uid, ids, context=None):
499 message = _("Project has been <b>closed</b>.")
500 return self.message_append_note(cr, uid, ids, body=message, context=context)
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('default_project_id')) in (int, long):
518 project_id = context['default_project_id']
520 if isinstance(context.get('default_project_id'), basestring):
521 project_name = context['default_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
665 '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."),
666 'name': fields.char('Task Summary', size=128, required=True, select=True),
667 'description': fields.text('Description'),
668 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
669 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
670 'type_id': fields.many2one('project.task.type', 'Stage'),
671 'state': fields.selection([('draft', 'New'),('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status', readonly=True, required=True,
672 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.\
673 \n If the task is over, the states is set to \'Done\'.'),
674 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
675 help="A task's kanban state indicates special situations affecting it:\n"
676 " * Normal is the default situation\n"
677 " * Blocked indicates something is preventing the progress of this task\n"
678 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
679 readonly=True, required=False),
680 'create_date': fields.datetime('Create Date', readonly=True,select=True),
681 'date_start': fields.datetime('Starting Date',select=True),
682 'date_end': fields.datetime('Ending Date',select=True),
683 'date_deadline': fields.date('Deadline',select=True),
684 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
685 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
686 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
687 'notes': fields.text('Notes'),
688 '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.'),
689 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
691 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
692 'project.task.work': (_get_task, ['hours'], 10),
694 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
695 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
697 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
698 'project.task.work': (_get_task, ['hours'], 10),
700 '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",
702 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
703 'project.task.work': (_get_task, ['hours'], 10),
705 '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.",
707 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
708 'project.task.work': (_get_task, ['hours'], 10),
710 'user_id': fields.many2one('res.users', 'Assigned to'),
711 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
712 'partner_id': fields.many2one('res.partner', 'Partner'),
713 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
714 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
715 'company_id': fields.many2one('res.company', 'Company'),
716 'id': fields.integer('ID', readonly=True),
717 'color': fields.integer('Color Index'),
718 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
723 'kanban_state': 'normal',
728 'user_id': lambda obj, cr, uid, context: uid,
729 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
732 _order = "priority, sequence, date_start, name, id"
734 def set_priority(self, cr, uid, ids, priority):
737 return self.write(cr, uid, ids, {'priority' : priority})
739 def set_high_priority(self, cr, uid, ids, *args):
740 """Set task priority to high
742 return self.set_priority(cr, uid, ids, '1')
744 def set_normal_priority(self, cr, uid, ids, *args):
745 """Set task priority to normal
747 return self.set_priority(cr, uid, ids, '2')
749 def _check_recursion(self, cr, uid, ids, context=None):
751 visited_branch = set()
753 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
759 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
760 if id in visited_branch: #Cycle
763 if id in visited_node: #Already tested don't work one more time for nothing
766 visited_branch.add(id)
769 #visit child using DFS
770 task = self.browse(cr, uid, id, context=context)
771 for child in task.child_ids:
772 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
776 visited_branch.remove(id)
779 def _check_dates(self, cr, uid, ids, context=None):
782 obj_task = self.browse(cr, uid, ids[0], context=context)
783 start = obj_task.date_start or False
784 end = obj_task.date_end or False
791 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
792 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
795 # Override view according to the company definition
797 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
798 users_obj = self.pool.get('res.users')
800 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
801 # this should be safe (no context passed to avoid side-effects)
802 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
803 tm = obj_tm and obj_tm.name or 'Hours'
805 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
807 if tm in ['Hours','Hour']:
810 eview = etree.fromstring(res['arch'])
812 def _check_rec(eview):
813 if eview.attrib.get('widget','') == 'float_time':
814 eview.set('widget','float')
821 res['arch'] = etree.tostring(eview)
823 for f in res['fields']:
824 if 'Hours' in res['fields'][f]['string']:
825 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
828 def _check_child_task(self, cr, uid, ids, context=None):
831 tasks = self.browse(cr, uid, ids, context=context)
834 for child in task.child_ids:
835 if child.state in ['draft', 'open', 'pending']:
836 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
839 def action_close(self, cr, uid, ids, context=None):
840 # This action open wizard to send email to partner or project manager after close task.
843 task_id = len(ids) and ids[0] or False
844 self._check_child_task(cr, uid, ids, context=context)
845 if not task_id: return False
846 task = self.browse(cr, uid, task_id, context=context)
847 project = task.project_id
848 res = self.do_close(cr, uid, [task_id], context=context)
849 if project.warn_manager or project.warn_customer:
851 'name': _('Send Email after close task'),
854 'res_model': 'mail.compose.message',
855 'type': 'ir.actions.act_window',
858 'context': {'active_id': task.id,
859 'active_model': 'project.task'}
863 def do_close(self, cr, uid, ids, context={}):
867 request = self.pool.get('res.request')
868 if not isinstance(ids,list): ids = [ids]
869 for task in self.browse(cr, uid, ids, context=context):
871 project = task.project_id
873 # Send request to project manager
874 if project.warn_manager and project.user_id and (project.user_id.id != uid):
875 request.create(cr, uid, {
876 'name': _("Task '%s' closed") % task.name,
879 'act_to': project.user_id.id,
880 'ref_partner_id': task.partner_id.id,
881 'ref_doc1': 'project.task,%d'% (task.id,),
882 'ref_doc2': 'project.project,%d'% (project.id,),
885 for parent_id in task.parent_ids:
886 if parent_id.state in ('pending','draft'):
888 for child in parent_id.child_ids:
889 if child.id != task.id and child.state not in ('done','cancelled'):
892 self.do_reopen(cr, uid, [parent_id.id], context=context)
893 vals.update({'state': 'done'})
894 vals.update({'remaining_hours': 0.0})
895 if not task.date_end:
896 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
897 self.write(cr, uid, [task.id],vals, context=context)
898 self.do_close_send_note(cr, uid, [task.id], context)
901 def do_reopen(self, cr, uid, ids, context=None):
902 request = self.pool.get('res.request')
904 for task in self.browse(cr, uid, ids, context=context):
905 project = task.project_id
906 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
907 request.create(cr, uid, {
908 'name': _("Task '%s' set in progress") % task.name,
911 'act_to': project.user_id.id,
912 'ref_partner_id': task.partner_id.id,
913 'ref_doc1': 'project.task,%d' % task.id,
914 'ref_doc2': 'project.project,%d' % project.id,
917 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
918 self.do_open_send_note(cr, uid, [task.id], context)
921 def do_cancel(self, cr, uid, ids, context={}):
922 request = self.pool.get('res.request')
923 tasks = self.browse(cr, uid, ids, context=context)
924 self._check_child_task(cr, uid, ids, context=context)
926 project = task.project_id
927 if project.warn_manager and project.user_id and (project.user_id.id != uid):
928 request.create(cr, uid, {
929 'name': _("Task '%s' cancelled") % task.name,
932 'act_to': project.user_id.id,
933 'ref_partner_id': task.partner_id.id,
934 'ref_doc1': 'project.task,%d' % task.id,
935 'ref_doc2': 'project.project,%d' % project.id,
937 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
938 self.do_cancel_send_note(cr, uid, [task.id], context)
941 def do_open(self, cr, uid, ids, context={}):
942 if not isinstance(ids,list): ids = [ids]
943 tasks= self.browse(cr, uid, ids, context=context)
945 data = {'state': 'open'}
947 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
948 self.write(cr, uid, [t.id], data, context=context)
949 self.do_open_send_note(cr, uid, [t.id], context)
952 def do_draft(self, cr, uid, ids, context={}):
953 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
954 self.do_draft_send_note(cr, uid, ids, context)
958 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
959 attachment = self.pool.get('ir.attachment')
960 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
961 new_attachment_ids = []
962 for attachment_id in attachment_ids:
963 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
964 return new_attachment_ids
967 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
969 Delegate Task to another users.
971 assert delegate_data['user_id'], _("Delegated User should be specified")
973 for task in self.browse(cr, uid, ids, context=context):
974 delegated_task_id = self.copy(cr, uid, task.id, {
975 'name': delegate_data['name'],
976 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
977 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
978 'planned_hours': delegate_data['planned_hours'] or 0.0,
979 'parent_ids': [(6, 0, [task.id])],
981 'description': delegate_data['new_task_description'] or '',
985 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
986 newname = delegate_data['prefix'] or ''
988 'remaining_hours': delegate_data['planned_hours_me'],
989 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
992 if delegate_data['state'] == 'pending':
993 self.do_pending(cr, uid, [task.id], context=context)
994 elif delegate_data['state'] == 'done':
995 self.do_close(cr, uid, [task.id], context=context)
996 self.do_delegation_send_note(cr, uid, [task.id], context)
997 delegated_tasks[task.id] = delegated_task_id
998 return delegated_tasks
1000 def do_pending(self, cr, uid, ids, context={}):
1001 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1002 self.do_pending_send_note(cr, uid, ids, context)
1005 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1006 for task in self.browse(cr, uid, ids, context=context):
1007 if (task.state=='draft') or (task.planned_hours==0.0):
1008 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1009 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1012 def set_remaining_time_1(self, cr, uid, ids, context=None):
1013 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1015 def set_remaining_time_2(self, cr, uid, ids, context=None):
1016 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1018 def set_remaining_time_5(self, cr, uid, ids, context=None):
1019 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1021 def set_remaining_time_10(self, cr, uid, ids, context=None):
1022 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1024 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1025 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1027 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1028 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1030 def set_kanban_state_done(self, cr, uid, ids, context=None):
1031 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1033 def _change_type(self, cr, uid, ids, next, context=None):
1035 go to the next stage
1036 if next is False, go to previous stage
1038 for task in self.browse(cr, uid, ids):
1039 if task.project_id.type_ids:
1040 typeid = task.type_id.id
1042 for type in task.project_id.type_ids :
1043 types_seq[type.id] = type.sequence
1045 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1047 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1048 sorted_types = [x[0] for x in types]
1050 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1051 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1052 index = sorted_types.index(typeid)
1053 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1054 self.state_change_send_note(cr, uid, [task.id], context)
1057 def next_type(self, cr, uid, ids, context=None):
1058 return self._change_type(cr, uid, ids, True, context=context)
1060 def prev_type(self, cr, uid, ids, context=None):
1061 return self._change_type(cr, uid, ids, False, context=context)
1063 def _store_history(self, cr, uid, ids, context=None):
1064 for task in self.browse(cr, uid, ids, context=context):
1065 self.pool.get('project.task.history').create(cr, uid, {
1067 'remaining_hours': task.remaining_hours,
1068 'planned_hours': task.planned_hours,
1069 'kanban_state': task.kanban_state,
1070 'type_id': task.type_id.id,
1071 'state': task.state,
1072 'user_id': task.user_id.id
1077 def create(self, cr, uid, vals, context=None):
1078 task_id = super(task, self).create(cr, uid, vals, context=context)
1079 self._store_history(cr, uid, [task_id], context=context)
1080 self.create_send_note(cr, uid, [task_id], context=context)
1083 # Overridden to reset the kanban_state to normal whenever
1084 # the stage (type_id) of the task changes.
1085 def write(self, cr, uid, ids, vals, context=None):
1086 if isinstance(ids, (int, long)):
1088 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1089 new_stage = vals.get('type_id')
1090 vals_reset_kstate = dict(vals, kanban_state='normal')
1091 for t in self.browse(cr, uid, ids, context=context):
1092 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1093 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1096 result = super(task,self).write(cr, uid, ids, vals, context=context)
1097 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1098 self._store_history(cr, uid, ids, context=context)
1099 self.state_change_send_note(cr, uid, ids, context)
1102 def unlink(self, cr, uid, ids, context=None):
1105 self._check_child_task(cr, uid, ids, context=context)
1106 res = super(task, self).unlink(cr, uid, ids, context)
1109 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1110 context = context or {}
1114 if task.state in ('done','cancelled'):
1119 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1121 for t2 in task.parent_ids:
1122 start.append("up.Task_%s.end" % (t2.id,))
1126 ''' % (ident,','.join(start))
1131 ''' % (ident, 'User_'+str(task.user_id.id))
1136 # ---------------------------------------------------
1137 # OpenChatter methods and notifications
1138 # ---------------------------------------------------
1140 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1141 result = dict.fromkeys(ids, [])
1142 for obj in self.browse(cr, uid, ids, context=context):
1143 if obj.state == 'draft' and obj.user_id:
1144 result[obj.id] = [obj.user_id.id]
1147 def message_get_subscribers(self, cr, uid, ids, context=None):
1148 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1149 for obj in self.browse(cr, uid, ids, context=context):
1151 sub_ids.append(obj.user_id.id)
1153 sub_ids.append(obj.manager_id.id)
1154 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1156 def create_send_note(self, cr, uid, ids, context=None):
1157 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1159 def do_pending_send_note(self, cr, uid, ids, context=None):
1160 if not isinstance(ids,list): ids = [ids]
1161 msg = _('Task is now <b>pending</b>.')
1162 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1164 def do_open_send_note(self, cr, uid, ids, context=None):
1165 msg = _('Task has been <b>opened</b>.')
1166 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1168 def do_cancel_send_note(self, cr, uid, ids, context=None):
1169 msg = _('Task has been <b>canceled</b>.')
1170 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1172 def do_close_send_note(self, cr, uid, ids, context=None):
1173 msg = _('Task has been <b>closed</b>.')
1174 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1176 def do_draft_send_note(self, cr, uid, ids, context=None):
1177 msg = _('Task has been <b>renewed</b>.')
1178 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1180 def do_delegation_send_note(self, cr, uid, ids, context=None):
1181 for task in self.browse(cr, uid, ids, context=context):
1182 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1183 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1186 def state_change_send_note(self, cr, uid, ids, context=None):
1187 for task in self.browse(cr, uid, ids, context=context):
1188 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1189 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1194 class project_work(osv.osv):
1195 _name = "project.task.work"
1196 _description = "Project Task Work"
1198 'name': fields.char('Work summary', size=128),
1199 'date': fields.datetime('Date', select="1"),
1200 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1201 'hours': fields.float('Time Spent'),
1202 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1203 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1207 'user_id': lambda obj, cr, uid, context: uid,
1208 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1211 _order = "date desc"
1212 def create(self, cr, uid, vals, *args, **kwargs):
1213 if 'hours' in vals and (not vals['hours']):
1214 vals['hours'] = 0.00
1215 if 'task_id' in vals:
1216 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1217 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1219 def write(self, cr, uid, ids, vals, context=None):
1220 if 'hours' in vals and (not vals['hours']):
1221 vals['hours'] = 0.00
1223 for work in self.browse(cr, uid, ids, context=context):
1224 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))
1225 return super(project_work,self).write(cr, uid, ids, vals, context)
1227 def unlink(self, cr, uid, ids, *args, **kwargs):
1228 for work in self.browse(cr, uid, ids):
1229 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1230 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1233 class account_analytic_account(osv.osv):
1234 _inherit = 'account.analytic.account'
1235 _description = 'Analytic Account'
1237 'use_tasks': fields.boolean('Tasks Management'),
1238 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1241 # 'use_tasks': True,
1244 def project_create(self,cr,uid,analytic_account_id,vals,context=None):
1246 project_pool = self.pool.get('project.project')
1247 project_id = project_pool.name_search(cr, uid, name=vals.get('name'))
1249 res['name'] = vals.get('name')
1250 res['analytic_account_id'] = analytic_account_id
1251 project_pool.create(cr, uid, res, context=context)
1254 def create(self, cr, uid, vals, context=None):
1257 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1258 vals['child_ids'] = []
1259 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1260 if vals.get('use_tasks', False):
1261 self.project_create(cr, uid, analytic_account_id, vals, context)
1262 return analytic_account_id
1264 def unlink(self, cr, uid, ids, *args, **kwargs):
1265 project_obj = self.pool.get('project.project')
1266 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1268 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1269 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1271 account_analytic_account()
1274 # Tasks History, used for cumulative flow charts (Lean/Agile)
1277 class project_task_history(osv.osv):
1278 _name = 'project.task.history'
1279 _description = 'History of Tasks'
1280 _rec_name = 'task_id'
1282 def _get_date(self, cr, uid, ids, name, arg, context=None):
1284 for history in self.browse(cr, uid, ids, context=context):
1285 if history.state in ('done','cancelled'):
1286 result[history.id] = history.date
1288 cr.execute('''select
1291 project_task_history
1295 order by id limit 1''', (history.task_id.id, history.id))
1297 result[history.id] = res and res[0] or False
1300 def _get_related_date(self, cr, uid, ids, context=None):
1302 for history in self.browse(cr, uid, ids, context=context):
1303 cr.execute('''select
1306 project_task_history
1310 order by id desc limit 1''', (history.task_id.id, history.id))
1313 result.append(res[0])
1317 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1318 'type_id': fields.many2one('project.task.type', 'Stage'),
1319 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status'),
1320 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1321 'date': fields.date('Date', select=True),
1322 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1323 'project.task.history': (_get_related_date, None, 20)
1325 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1326 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1327 'user_id': fields.many2one('res.users', 'Responsible'),
1330 'date': fields.date.context_today,
1332 project_task_history()
1334 class project_task_history_cumulative(osv.osv):
1335 _name = 'project.task.history.cumulative'
1336 _table = 'project_task_history_cumulative'
1337 _inherit = 'project.task.history'
1340 'end_date': fields.date('End Date'),
1341 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1344 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1346 history.date::varchar||'-'||history.history_id::varchar as id,
1347 history.date as end_date,
1352 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1353 task_id, type_id, user_id, kanban_state, state,
1354 greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours
1356 project_task_history
1360 project_task_history_cumulative()