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="The related state for the stage. The state of your document will automatically change regarding the selected stage. Example, a stage is related to the state 'Close', when your document reach this stage, it will be automatically closed."),
48 'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
58 class project(osv.osv):
59 _name = "project.project"
60 _description = "Project"
61 _inherits = {'account.analytic.account': "analytic_account_id"}
62 _inherit = ['ir.needaction_mixin', 'mail.thread']
64 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
66 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
67 if context and context.get('user_preference'):
68 cr.execute("""SELECT project.id FROM project_project project
69 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
70 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
71 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
72 return [(r[0]) for r in cr.fetchall()]
73 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
74 context=context, count=count)
76 def _complete_name(self, cr, uid, ids, name, args, context=None):
78 for m in self.browse(cr, uid, ids, context=context):
79 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
82 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
83 partner_obj = self.pool.get('res.partner')
87 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
88 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
89 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
90 val['pricelist_id'] = pricelist_id
93 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
94 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
95 project_ids = [task.project_id.id for task in tasks if task.project_id]
96 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
98 def _get_project_and_parents(self, cr, uid, ids, context=None):
99 """ return the project ids and all their parent projects """
103 SELECT DISTINCT parent.id
104 FROM project_project project, project_project parent, account_analytic_account account
105 WHERE project.analytic_account_id = account.id
106 AND parent.analytic_account_id = account.parent_id
109 ids = [t[0] for t in cr.fetchall()]
113 def _get_project_and_children(self, cr, uid, ids, context=None):
114 """ retrieve all children projects of project ids;
115 return a dictionary mapping each project to its parent project (or None)
117 res = dict.fromkeys(ids, None)
120 SELECT project.id, parent.id
121 FROM project_project project, project_project parent, account_analytic_account account
122 WHERE project.analytic_account_id = account.id
123 AND parent.analytic_account_id = account.parent_id
126 dic = dict(cr.fetchall())
131 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
132 child_parent = self._get_project_and_children(cr, uid, ids, context)
133 # compute planned_hours, total_hours, effective_hours specific to each project
135 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
136 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
137 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
139 """, (tuple(child_parent.keys()),))
140 # aggregate results into res
141 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
142 for id, planned, total, effective in cr.fetchall():
143 # add the values specific to id to all parent projects of id in the result
146 res[id]['planned_hours'] += planned
147 res[id]['total_hours'] += total
148 res[id]['effective_hours'] += effective
149 id = child_parent[id]
150 # compute progress rates
152 if res[id]['total_hours']:
153 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
155 res[id]['progress_rate'] = 0.0
158 def unlink(self, cr, uid, ids, *args, **kwargs):
159 for proj in self.browse(cr, uid, ids):
161 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
162 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
164 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
165 res = dict.fromkeys(ids, 0)
166 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
167 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
168 res[task.project_id.id] += 1
172 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
173 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
174 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
175 '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),
176 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
177 '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)]}),
179 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
180 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)]}),
181 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
182 '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.",
184 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
185 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
187 '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.",
189 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
190 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
192 '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.",
194 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
195 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
197 '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.",
199 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
200 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
202 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
203 '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)]}),
204 '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)]}),
205 '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)]}),
206 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
207 'use_tasks': fields.boolean('Use Tasks', help="Check this field if this project is aimed at managing tasks"),
208 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"),
209 'color': fields.integer('Color Index'),
210 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
213 def dummy(self, cr, uid, ids, context):
216 def _get_type_common(self, cr, uid, context):
217 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
225 'type_ids': _get_type_common,
229 # TODO: Why not using a SQL contraints ?
230 def _check_dates(self, cr, uid, ids, context=None):
231 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
232 if leave['date_start'] and leave['date']:
233 if leave['date_start'] > leave['date']:
238 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
241 def set_template(self, cr, uid, ids, context=None):
242 res = self.setActive(cr, uid, ids, value=False, context=context)
245 def set_done(self, cr, uid, ids, context=None):
246 task_obj = self.pool.get('project.task')
247 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
248 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
249 self.write(cr, uid, ids, {'state':'close'}, context=context)
250 self.set_close_send_note(cr, uid, ids, context=context)
253 def set_cancel(self, cr, uid, ids, context=None):
254 task_obj = self.pool.get('project.task')
255 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
256 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
257 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
258 self.set_cancel_send_note(cr, uid, ids, context=context)
261 def set_pending(self, cr, uid, ids, context=None):
262 self.write(cr, uid, ids, {'state':'pending'}, context=context)
263 self.set_pending_send_note(cr, uid, ids, context=context)
266 def set_open(self, cr, uid, ids, context=None):
267 self.write(cr, uid, ids, {'state':'open'}, context=context)
268 self.set_open_send_note(cr, uid, ids, context=context)
271 def reset_project(self, cr, uid, ids, context=None):
272 res = self.setActive(cr, uid, ids, value=True, context=context)
273 self.set_open_send_note(cr, uid, ids, context=context)
276 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
277 """ copy and map tasks from old to new project """
281 task_obj = self.pool.get('project.task')
282 proj = self.browse(cr, uid, old_project_id, context=context)
283 for task in proj.tasks:
284 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
285 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
286 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
289 def copy(self, cr, uid, id, default={}, context=None):
293 default = default or {}
294 context['active_test'] = False
295 default['state'] = 'open'
296 default['tasks'] = []
297 proj = self.browse(cr, uid, id, context=context)
298 if not default.get('name', False):
299 default['name'] = proj.name + _(' (copy)')
301 res = super(project, self).copy(cr, uid, id, default, context)
302 self.map_tasks(cr,uid,id,res,context)
305 def duplicate_template(self, cr, uid, ids, context=None):
308 data_obj = self.pool.get('ir.model.data')
310 for proj in self.browse(cr, uid, ids, context=context):
311 parent_id = context.get('parent_id', False)
312 context.update({'analytic_project_copy': True})
313 new_date_start = time.strftime('%Y-%m-%d')
315 if proj.date_start and proj.date:
316 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
317 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
318 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
319 context.update({'copy':True})
320 new_id = self.copy(cr, uid, proj.id, default = {
321 'name': proj.name +_(' (copy)'),
323 'date_start':new_date_start,
325 'parent_id':parent_id}, context=context)
326 result.append(new_id)
328 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
329 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
331 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
333 if result and len(result):
335 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
336 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
337 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
338 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
339 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
340 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
342 'name': _('Projects'),
344 'view_mode': 'form,tree',
345 'res_model': 'project.project',
348 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
349 'type': 'ir.actions.act_window',
350 'search_view_id': search_view['res_id'],
354 # set active value for a project, its sub projects and its tasks
355 def setActive(self, cr, uid, ids, value=True, context=None):
356 task_obj = self.pool.get('project.task')
357 for proj in self.browse(cr, uid, ids, context=None):
358 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
359 cr.execute('select id from project_task where project_id=%s', (proj.id,))
360 tasks_id = [x[0] for x in cr.fetchall()]
362 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
363 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
365 self.setActive(cr, uid, child_ids, value, context=None)
368 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
369 context = context or {}
370 if type(ids) in (long, int,):
372 projects = self.browse(cr, uid, ids, context=context)
374 for project in projects:
375 if (not project.members) and force_members:
376 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
378 resource_pool = self.pool.get('resource.resource')
380 result = "from openerp.addons.resource.faces import *\n"
381 result += "import datetime\n"
382 for project in self.browse(cr, uid, ids, context=context):
383 u_ids = [i.id for i in project.members]
384 if project.user_id and (project.user_id.id not in u_ids):
385 u_ids.append(project.user_id.id)
386 for task in project.tasks:
387 if task.state in ('done','cancelled'):
389 if task.user_id and (task.user_id.id not in u_ids):
390 u_ids.append(task.user_id.id)
391 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
392 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
393 for key, vals in resource_objs.items():
395 class User_%s(Resource):
397 ''' % (key, vals.get('efficiency', False))
404 def _schedule_project(self, cr, uid, project, context=None):
405 resource_pool = self.pool.get('resource.resource')
406 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
407 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
408 # TODO: check if we need working_..., default values are ok.
409 puids = [x.id for x in project.members]
411 puids.append(project.user_id.id)
419 project.date_start, working_days,
420 '|'.join(['User_'+str(x) for x in puids])
422 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
429 #TODO: DO Resource allocation and compute availability
430 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
436 def schedule_tasks(self, cr, uid, ids, context=None):
437 context = context or {}
438 if type(ids) in (long, int,):
440 projects = self.browse(cr, uid, ids, context=context)
441 result = self._schedule_header(cr, uid, ids, False, context=context)
442 for project in projects:
443 result += self._schedule_project(cr, uid, project, context=context)
444 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
447 exec result in local_dict
448 projects_gantt = Task.BalancedProject(local_dict['Project'])
450 for project in projects:
451 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
452 for task in project.tasks:
453 if task.state in ('done','cancelled'):
456 p = getattr(project_gantt, 'Task_%d' % (task.id,))
458 self.pool.get('project.task').write(cr, uid, [task.id], {
459 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
460 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
462 if (not task.user_id) and (p.booked_resource):
463 self.pool.get('project.task').write(cr, uid, [task.id], {
464 'user_id': int(p.booked_resource[0].name[5:]),
468 # ------------------------------------------------
469 # OpenChatter methods and notifications
470 # ------------------------------------------------
472 def get_needaction_user_ids(self, cr, uid, ids, context=None):
473 result = dict.fromkeys(ids)
474 for obj in self.browse(cr, uid, ids, context=context):
476 if obj.state == 'draft' and obj.user_id:
477 result[obj.id] = [obj.user_id.id]
480 def message_get_subscribers(self, cr, uid, ids, context=None):
481 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
482 for obj in self.browse(cr, uid, ids, context=context):
484 sub_ids.append(obj.user_id.id)
485 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
487 def create(self, cr, uid, vals, context=None):
488 obj_id = super(project, self).create(cr, uid, vals, context=context)
489 self.create_send_note(cr, uid, [obj_id], context=context)
492 def create_send_note(self, cr, uid, ids, context=None):
493 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
495 def set_open_send_note(self, cr, uid, ids, context=None):
496 message = _("Project has been <b>opened</b>.")
497 return self.message_append_note(cr, uid, ids, body=message, context=context)
499 def set_pending_send_note(self, cr, uid, ids, context=None):
500 message = _("Project is now <b>pending</b>.")
501 return self.message_append_note(cr, uid, ids, body=message, context=context)
503 def set_cancel_send_note(self, cr, uid, ids, context=None):
504 message = _("Project has been <b>cancelled</b>.")
505 return self.message_append_note(cr, uid, ids, body=message, context=context)
507 def set_close_send_note(self, cr, uid, ids, context=None):
508 message = _("Project has been <b>closed</b>.")
509 return self.message_append_note(cr, uid, ids, body=message, context=context)
514 _name = "project.task"
515 _description = "Task"
517 _date_name = "date_start"
518 _inherit = ['ir.needaction_mixin', 'mail.thread']
521 def _resolve_project_id_from_context(self, cr, uid, context=None):
522 """Return ID of project based on the value of 'project_id'
523 context key, or None if it cannot be resolved to a single project.
525 if context is None: context = {}
526 if type(context.get('default_project_id')) in (int, long):
527 project_id = context['default_project_id']
529 if isinstance(context.get('default_project_id'), basestring):
530 project_name = context['default_project_id']
531 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
532 if len(project_ids) == 1:
533 return project_ids[0][0]
535 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
536 stage_obj = self.pool.get('project.task.type')
537 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
538 order = stage_obj._order
539 access_rights_uid = access_rights_uid or uid
540 if read_group_order == 'type_id desc':
541 # lame way to allow reverting search, should just work in the trivial case
542 order = '%s desc' % order
544 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
546 domain = ['|', ('id','in',ids), ('project_default','=',1)]
547 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
548 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
549 # restore order of the search
550 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
553 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
554 res_users = self.pool.get('res.users')
555 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
556 access_rights_uid = access_rights_uid or uid
558 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
559 order = res_users._order
560 # lame way to allow reverting search, should just work in the trivial case
561 if read_group_order == 'user_id desc':
562 order = '%s desc' % order
563 # de-duplicate and apply search order
564 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
565 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
566 # restore order of the search
567 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
571 'type_id': _read_group_type_id,
572 'user_id': _read_group_user_id
576 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
577 obj_project = self.pool.get('project.project')
579 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
580 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
581 if id and isinstance(id, (long, int)):
582 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
583 args.append(('active', '=', False))
584 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
586 def _str_get(self, task, level=0, border='***', context=None):
587 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'+ \
588 border[0]+' '+(task.name or '')+'\n'+ \
589 (task.description or '')+'\n\n'
591 # Compute: effective_hours, total_hours, progress
592 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
594 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
595 hours = dict(cr.fetchall())
596 for task in self.browse(cr, uid, ids, context=context):
597 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)}
598 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
599 res[task.id]['progress'] = 0.0
600 if (task.remaining_hours + hours.get(task.id, 0.0)):
601 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
602 if task.state in ('done','cancelled'):
603 res[task.id]['progress'] = 100.0
607 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
608 if remaining and not planned:
609 return {'value':{'planned_hours': remaining}}
612 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
613 return {'value':{'remaining_hours': planned - effective}}
615 def onchange_project(self, cr, uid, id, project_id):
618 data = self.pool.get('project.project').browse(cr, uid, [project_id])
619 partner_id=data and data[0].partner_id
621 return {'value':{'partner_id':partner_id.id}}
624 def duplicate_task(self, cr, uid, map_ids, context=None):
625 for new in map_ids.values():
626 task = self.browse(cr, uid, new, context)
627 child_ids = [ ch.id for ch in task.child_ids]
629 for child in task.child_ids:
630 if child.id in map_ids.keys():
631 child_ids.remove(child.id)
632 child_ids.append(map_ids[child.id])
634 parent_ids = [ ch.id for ch in task.parent_ids]
636 for parent in task.parent_ids:
637 if parent.id in map_ids.keys():
638 parent_ids.remove(parent.id)
639 parent_ids.append(map_ids[parent.id])
640 #FIXME why there is already the copy and the old one
641 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
643 def copy_data(self, cr, uid, id, default={}, context=None):
644 default = default or {}
645 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
646 if not default.get('remaining_hours', False):
647 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
648 default['active'] = True
649 default['type_id'] = False
650 if not default.get('name', False):
651 default['name'] = self.browse(cr, uid, id, context=context).name or ''
652 if not context.get('copy',False):
653 new_name = _("%s (copy)")%default.get('name','')
654 default.update({'name':new_name})
655 return super(task, self).copy_data(cr, uid, id, default, context)
658 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
660 for task in self.browse(cr, uid, ids, context=context):
663 if task.project_id.active == False or task.project_id.state == 'template':
667 def _get_task(self, cr, uid, ids, context=None):
669 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
670 if work.task_id: result[work.task_id.id] = True
674 '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."),
675 'name': fields.char('Task Summary', size=128, required=True, select=True),
676 'description': fields.text('Description'),
677 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
678 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
679 'type_id': fields.many2one('project.task.type', 'Stage'),
680 'state': fields.related('type_id', 'state', type="selection", store=True,
681 selection=_TASK_STATE, string="State", readonly=True,
682 help='The state is set to \'Draft\', when a case is created.\
683 If the case is in progress the state is set to \'Open\'.\
684 When the case is over, the state is set to \'Done\'.\
685 If the case needs to be reviewed then the state is \
686 set to \'Pending\'.'),
687 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
688 help="A task's kanban state indicates special situations affecting it:\n"
689 " * Normal is the default situation\n"
690 " * Blocked indicates something is preventing the progress of this task\n"
691 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
692 readonly=True, required=False),
693 'create_date': fields.datetime('Create Date', readonly=True,select=True),
694 'date_start': fields.datetime('Starting Date',select=True),
695 'date_end': fields.datetime('Ending Date',select=True),
696 'date_deadline': fields.date('Deadline',select=True),
697 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
698 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
699 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
700 'notes': fields.text('Notes'),
701 '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.'),
702 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
704 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
705 'project.task.work': (_get_task, ['hours'], 10),
707 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
708 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
710 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
711 'project.task.work': (_get_task, ['hours'], 10),
713 '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",
715 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
716 'project.task.work': (_get_task, ['hours'], 10),
718 '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.",
720 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
721 'project.task.work': (_get_task, ['hours'], 10),
723 'user_id': fields.many2one('res.users', 'Assigned to'),
724 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
725 'partner_id': fields.many2one('res.partner', 'Partner'),
726 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
727 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
728 'company_id': fields.many2one('res.company', 'Company'),
729 'id': fields.integer('ID', readonly=True),
730 'color': fields.integer('Color Index'),
731 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
736 'kanban_state': 'normal',
741 'user_id': lambda obj, cr, uid, context: uid,
742 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
745 _order = "priority, sequence, date_start, name, id"
747 def set_priority(self, cr, uid, ids, priority):
750 return self.write(cr, uid, ids, {'priority' : priority})
752 def set_high_priority(self, cr, uid, ids, *args):
753 """Set task priority to high
755 return self.set_priority(cr, uid, ids, '1')
757 def set_normal_priority(self, cr, uid, ids, *args):
758 """Set task priority to normal
760 return self.set_priority(cr, uid, ids, '2')
762 def _check_recursion(self, cr, uid, ids, context=None):
764 visited_branch = set()
766 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
772 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
773 if id in visited_branch: #Cycle
776 if id in visited_node: #Already tested don't work one more time for nothing
779 visited_branch.add(id)
782 #visit child using DFS
783 task = self.browse(cr, uid, id, context=context)
784 for child in task.child_ids:
785 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
789 visited_branch.remove(id)
792 def _check_dates(self, cr, uid, ids, context=None):
795 obj_task = self.browse(cr, uid, ids[0], context=context)
796 start = obj_task.date_start or False
797 end = obj_task.date_end or False
804 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
805 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
808 # Override view according to the company definition
810 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
811 users_obj = self.pool.get('res.users')
813 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
814 # this should be safe (no context passed to avoid side-effects)
815 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
816 tm = obj_tm and obj_tm.name or 'Hours'
818 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
820 if tm in ['Hours','Hour']:
823 eview = etree.fromstring(res['arch'])
825 def _check_rec(eview):
826 if eview.attrib.get('widget','') == 'float_time':
827 eview.set('widget','float')
834 res['arch'] = etree.tostring(eview)
836 for f in res['fields']:
837 if 'Hours' in res['fields'][f]['string']:
838 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
841 def _check_child_task(self, cr, uid, ids, context=None):
844 tasks = self.browse(cr, uid, ids, context=context)
847 for child in task.child_ids:
848 if child.state in ['draft', 'open', 'pending']:
849 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
852 def action_close(self, cr, uid, ids, context=None):
853 # This action open wizard to send email to partner or project manager after close task.
856 task_id = len(ids) and ids[0] or False
857 self._check_child_task(cr, uid, ids, context=context)
858 if not task_id: return False
859 task = self.browse(cr, uid, task_id, context=context)
860 project = task.project_id
861 res = self.do_close(cr, uid, [task_id], context=context)
862 if project.warn_manager or project.warn_customer:
864 'name': _('Send Email after close task'),
867 'res_model': 'mail.compose.message',
868 'type': 'ir.actions.act_window',
871 'context': {'active_id': task.id,
872 'active_model': 'project.task'}
876 def do_close(self, cr, uid, ids, context={}):
880 request = self.pool.get('res.request')
881 if not isinstance(ids,list): ids = [ids]
882 for task in self.browse(cr, uid, ids, context=context):
884 project = task.project_id
886 # Send request to project manager
887 if project.warn_manager and project.user_id and (project.user_id.id != uid):
888 request.create(cr, uid, {
889 'name': _("Task '%s' closed") % task.name,
892 'act_to': project.user_id.id,
893 'ref_partner_id': task.partner_id.id,
894 'ref_doc1': 'project.task,%d'% (task.id,),
895 'ref_doc2': 'project.project,%d'% (project.id,),
898 for parent_id in task.parent_ids:
899 if parent_id.state in ('pending','draft'):
901 for child in parent_id.child_ids:
902 if child.id != task.id and child.state not in ('done','cancelled'):
905 self.do_reopen(cr, uid, [parent_id.id], context=context)
906 vals.update({'state': 'done'})
907 vals.update({'remaining_hours': 0.0})
908 if not task.date_end:
909 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
910 self.write(cr, uid, [task.id],vals, context=context)
911 self.do_close_send_note(cr, uid, [task.id], context)
914 def do_reopen(self, cr, uid, ids, context=None):
915 request = self.pool.get('res.request')
917 for task in self.browse(cr, uid, ids, context=context):
918 project = task.project_id
919 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
920 request.create(cr, uid, {
921 'name': _("Task '%s' set in progress") % task.name,
924 'act_to': project.user_id.id,
925 'ref_partner_id': task.partner_id.id,
926 'ref_doc1': 'project.task,%d' % task.id,
927 'ref_doc2': 'project.project,%d' % project.id,
930 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
931 self.do_open_send_note(cr, uid, [task.id], context)
934 def do_cancel(self, cr, uid, ids, context={}):
935 request = self.pool.get('res.request')
936 tasks = self.browse(cr, uid, ids, context=context)
937 self._check_child_task(cr, uid, ids, context=context)
939 project = task.project_id
940 if project.warn_manager and project.user_id and (project.user_id.id != uid):
941 request.create(cr, uid, {
942 'name': _("Task '%s' cancelled") % task.name,
945 'act_to': project.user_id.id,
946 'ref_partner_id': task.partner_id.id,
947 'ref_doc1': 'project.task,%d' % task.id,
948 'ref_doc2': 'project.project,%d' % project.id,
950 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
951 self.do_cancel_send_note(cr, uid, [task.id], context)
954 def do_open(self, cr, uid, ids, context={}):
955 if not isinstance(ids,list): ids = [ids]
956 tasks= self.browse(cr, uid, ids, context=context)
958 data = {'state': 'open'}
960 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
961 self.write(cr, uid, [t.id], data, context=context)
962 self.do_open_send_note(cr, uid, [t.id], context)
965 def do_draft(self, cr, uid, ids, context={}):
966 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
967 self.do_draft_send_note(cr, uid, ids, context)
971 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
972 attachment = self.pool.get('ir.attachment')
973 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
974 new_attachment_ids = []
975 for attachment_id in attachment_ids:
976 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
977 return new_attachment_ids
980 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
982 Delegate Task to another users.
984 assert delegate_data['user_id'], _("Delegated User should be specified")
986 for task in self.browse(cr, uid, ids, context=context):
987 delegated_task_id = self.copy(cr, uid, task.id, {
988 'name': delegate_data['name'],
989 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
990 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
991 'planned_hours': delegate_data['planned_hours'] or 0.0,
992 'parent_ids': [(6, 0, [task.id])],
994 'description': delegate_data['new_task_description'] or '',
998 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
999 newname = delegate_data['prefix'] or ''
1001 'remaining_hours': delegate_data['planned_hours_me'],
1002 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1005 if delegate_data['state'] == 'pending':
1006 self.do_pending(cr, uid, [task.id], context=context)
1007 elif delegate_data['state'] == 'done':
1008 self.do_close(cr, uid, [task.id], context=context)
1009 self.do_delegation_send_note(cr, uid, [task.id], context)
1010 delegated_tasks[task.id] = delegated_task_id
1011 return delegated_tasks
1013 def do_pending(self, cr, uid, ids, context={}):
1014 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1015 self.do_pending_send_note(cr, uid, ids, context)
1018 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1019 for task in self.browse(cr, uid, ids, context=context):
1020 if (task.state=='draft') or (task.planned_hours==0.0):
1021 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1022 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1025 def set_remaining_time_1(self, cr, uid, ids, context=None):
1026 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1028 def set_remaining_time_2(self, cr, uid, ids, context=None):
1029 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1031 def set_remaining_time_5(self, cr, uid, ids, context=None):
1032 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1034 def set_remaining_time_10(self, cr, uid, ids, context=None):
1035 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1037 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1038 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1040 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1041 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1043 def set_kanban_state_done(self, cr, uid, ids, context=None):
1044 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1046 def _change_type(self, cr, uid, ids, next, context=None):
1048 go to the next stage
1049 if next is False, go to previous stage
1051 for task in self.browse(cr, uid, ids):
1052 if task.project_id.type_ids:
1053 typeid = task.type_id.id
1055 for type in task.project_id.type_ids :
1056 types_seq[type.id] = type.sequence
1058 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1060 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1061 sorted_types = [x[0] for x in types]
1063 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1064 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1065 index = sorted_types.index(typeid)
1066 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1067 self.state_change_send_note(cr, uid, [task.id], context)
1070 def next_type(self, cr, uid, ids, context=None):
1071 return self._change_type(cr, uid, ids, True, context=context)
1073 def prev_type(self, cr, uid, ids, context=None):
1074 return self._change_type(cr, uid, ids, False, context=context)
1076 def _store_history(self, cr, uid, ids, context=None):
1077 for task in self.browse(cr, uid, ids, context=context):
1078 self.pool.get('project.task.history').create(cr, uid, {
1080 'remaining_hours': task.remaining_hours,
1081 'planned_hours': task.planned_hours,
1082 'kanban_state': task.kanban_state,
1083 'type_id': task.type_id.id,
1084 'state': task.state,
1085 'user_id': task.user_id.id
1090 def create(self, cr, uid, vals, context=None):
1091 task_id = super(task, self).create(cr, uid, vals, context=context)
1092 self._store_history(cr, uid, [task_id], context=context)
1093 self.create_send_note(cr, uid, [task_id], context=context)
1096 # Overridden to reset the kanban_state to normal whenever
1097 # the stage (type_id) of the task changes.
1098 def write(self, cr, uid, ids, vals, context=None):
1099 if isinstance(ids, (int, long)):
1101 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1102 new_stage = vals.get('type_id')
1103 vals_reset_kstate = dict(vals, kanban_state='normal')
1104 for t in self.browse(cr, uid, ids, context=context):
1105 #TO FIX:Kanban view doesn't raise warning
1106 # stages = [stage.id for stage in t.project_id.type_ids]
1107 # if new_stage not in stages:
1108 # raise osv.except_osv(_('Warning !'), _('Stage is not defined in the project.'))
1109 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1110 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1113 result = super(task,self).write(cr, uid, ids, vals, context=context)
1114 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1115 self._store_history(cr, uid, ids, context=context)
1116 self.state_change_send_note(cr, uid, ids, context)
1119 def unlink(self, cr, uid, ids, context=None):
1122 self._check_child_task(cr, uid, ids, context=context)
1123 res = super(task, self).unlink(cr, uid, ids, context)
1126 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1127 context = context or {}
1131 if task.state in ('done','cancelled'):
1136 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1138 for t2 in task.parent_ids:
1139 start.append("up.Task_%s.end" % (t2.id,))
1143 ''' % (ident,','.join(start))
1148 ''' % (ident, 'User_'+str(task.user_id.id))
1153 # ---------------------------------------------------
1154 # OpenChatter methods and notifications
1155 # ---------------------------------------------------
1157 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1158 result = dict.fromkeys(ids, [])
1159 for obj in self.browse(cr, uid, ids, context=context):
1160 if obj.state == 'draft' and obj.user_id:
1161 result[obj.id] = [obj.user_id.id]
1164 def message_get_subscribers(self, cr, uid, ids, context=None):
1165 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1166 for obj in self.browse(cr, uid, ids, context=context):
1168 sub_ids.append(obj.user_id.id)
1170 sub_ids.append(obj.manager_id.id)
1171 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1173 def create_send_note(self, cr, uid, ids, context=None):
1174 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1176 def do_pending_send_note(self, cr, uid, ids, context=None):
1177 if not isinstance(ids,list): ids = [ids]
1178 msg = _('Task is now <b>pending</b>.')
1179 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1181 def do_open_send_note(self, cr, uid, ids, context=None):
1182 msg = _('Task has been <b>opened</b>.')
1183 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1185 def do_cancel_send_note(self, cr, uid, ids, context=None):
1186 msg = _('Task has been <b>canceled</b>.')
1187 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1189 def do_close_send_note(self, cr, uid, ids, context=None):
1190 msg = _('Task has been <b>closed</b>.')
1191 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1193 def do_draft_send_note(self, cr, uid, ids, context=None):
1194 msg = _('Task has been <b>renewed</b>.')
1195 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1197 def do_delegation_send_note(self, cr, uid, ids, context=None):
1198 for task in self.browse(cr, uid, ids, context=context):
1199 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1200 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1203 def state_change_send_note(self, cr, uid, ids, context=None):
1204 for task in self.browse(cr, uid, ids, context=context):
1205 msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1206 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1211 class project_work(osv.osv):
1212 _name = "project.task.work"
1213 _description = "Project Task Work"
1215 'name': fields.char('Work summary', size=128),
1216 'date': fields.datetime('Date', select="1"),
1217 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1218 'hours': fields.float('Time Spent'),
1219 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1220 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1224 'user_id': lambda obj, cr, uid, context: uid,
1225 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1228 _order = "date desc"
1229 def create(self, cr, uid, vals, *args, **kwargs):
1230 if 'hours' in vals and (not vals['hours']):
1231 vals['hours'] = 0.00
1232 if 'task_id' in vals:
1233 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1234 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1236 def write(self, cr, uid, ids, vals, context=None):
1237 if 'hours' in vals and (not vals['hours']):
1238 vals['hours'] = 0.00
1240 for work in self.browse(cr, uid, ids, context=context):
1241 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))
1242 return super(project_work,self).write(cr, uid, ids, vals, context)
1244 def unlink(self, cr, uid, ids, *args, **kwargs):
1245 for work in self.browse(cr, uid, ids):
1246 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1247 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1250 class account_analytic_account(osv.osv):
1252 _inherit = 'account.analytic.account'
1253 _description = 'Analytic Account'
1255 def create(self, cr, uid, vals, context=None):
1258 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1259 vals['child_ids'] = []
1260 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1262 def unlink(self, cr, uid, ids, *args, **kwargs):
1263 project_obj = self.pool.get('project.project')
1264 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1266 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1267 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1269 account_analytic_account()
1272 # Tasks History, used for cumulative flow charts (Lean/Agile)
1275 class project_task_history(osv.osv):
1276 _name = 'project.task.history'
1277 _description = 'History of Tasks'
1278 _rec_name = 'task_id'
1280 def _get_date(self, cr, uid, ids, name, arg, context=None):
1282 for history in self.browse(cr, uid, ids, context=context):
1283 if history.state in ('done','cancelled'):
1284 result[history.id] = history.date
1286 cr.execute('''select
1289 project_task_history
1293 order by id limit 1''', (history.task_id.id, history.id))
1295 result[history.id] = res and res[0] or False
1298 def _get_related_date(self, cr, uid, ids, context=None):
1300 for history in self.browse(cr, uid, ids, context=context):
1301 cr.execute('''select
1304 project_task_history
1308 order by id desc limit 1''', (history.task_id.id, history.id))
1311 result.append(res[0])
1315 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1316 'type_id': fields.many2one('project.task.type', 'Stage'),
1317 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status'),
1318 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1319 'date': fields.date('Date', select=True),
1320 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1321 'project.task.history': (_get_related_date, None, 20)
1323 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1324 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1325 'user_id': fields.many2one('res.users', 'Responsible'),
1328 'date': fields.date.context_today,
1330 project_task_history()
1332 class project_task_history_cumulative(osv.osv):
1333 _name = 'project.task.history.cumulative'
1334 _table = 'project_task_history_cumulative'
1335 _inherit = 'project.task.history'
1338 'end_date': fields.date('End Date'),
1339 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1342 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1344 history.date::varchar||'-'||history.history_id::varchar as id,
1345 history.date as end_date,
1350 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1351 task_id, type_id, user_id, kanban_state, state,
1352 greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours
1354 project_task_history
1358 project_task_history_cumulative()