1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 base_status.base_stage import base_stage
23 from datetime import datetime, date
24 from lxml import etree
25 from osv import fields, osv
26 from openerp.addons.resource.faces import task as Task
28 from tools.translate import _
30 _TASK_STATE = [('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')]
32 class project_task_type(osv.osv):
33 _name = 'project.task.type'
34 _description = 'Task Stage'
37 'name': fields.char('Stage Name', required=True, size=64, translate=True),
38 'description': fields.text('Description'),
39 'sequence': fields.integer('Sequence'),
40 'case_default': fields.boolean('Common to All Projects',
41 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."),
42 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
43 'state': fields.selection(_TASK_STATE, 'State', required=True,
44 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."),
45 'fold': fields.boolean('Hide in views if empty',
46 help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
55 class project(osv.osv):
56 _name = "project.project"
57 _description = "Project"
58 _inherits = {'account.analytic.account': "analytic_account_id"}
59 _inherit = ['ir.needaction_mixin', 'mail.thread']
61 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
63 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
64 if context and context.get('user_preference'):
65 cr.execute("""SELECT project.id FROM project_project project
66 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
67 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
68 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
69 return [(r[0]) for r in cr.fetchall()]
70 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
71 context=context, count=count)
73 def _complete_name(self, cr, uid, ids, name, args, context=None):
75 for m in self.browse(cr, uid, ids, context=context):
76 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
79 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
80 partner_obj = self.pool.get('res.partner')
84 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
85 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
86 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
87 val['pricelist_id'] = pricelist_id
90 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
91 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
92 project_ids = [task.project_id.id for task in tasks if task.project_id]
93 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
95 def _get_project_and_parents(self, cr, uid, ids, context=None):
96 """ return the project ids and all their parent projects """
100 SELECT DISTINCT parent.id
101 FROM project_project project, project_project parent, account_analytic_account account
102 WHERE project.analytic_account_id = account.id
103 AND parent.analytic_account_id = account.parent_id
106 ids = [t[0] for t in cr.fetchall()]
110 def _get_project_and_children(self, cr, uid, ids, context=None):
111 """ retrieve all children projects of project ids;
112 return a dictionary mapping each project to its parent project (or None)
114 res = dict.fromkeys(ids, None)
117 SELECT project.id, parent.id
118 FROM project_project project, project_project parent, account_analytic_account account
119 WHERE project.analytic_account_id = account.id
120 AND parent.analytic_account_id = account.parent_id
123 dic = dict(cr.fetchall())
128 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
129 child_parent = self._get_project_and_children(cr, uid, ids, context)
130 # compute planned_hours, total_hours, effective_hours specific to each project
132 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
133 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
134 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
136 """, (tuple(child_parent.keys()),))
137 # aggregate results into res
138 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
139 for id, planned, total, effective in cr.fetchall():
140 # add the values specific to id to all parent projects of id in the result
143 res[id]['planned_hours'] += planned
144 res[id]['total_hours'] += total
145 res[id]['effective_hours'] += effective
146 id = child_parent[id]
147 # compute progress rates
149 if res[id]['total_hours']:
150 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
152 res[id]['progress_rate'] = 0.0
155 def unlink(self, cr, uid, ids, *args, **kwargs):
156 for proj in self.browse(cr, uid, ids):
158 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
159 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
161 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
162 res = dict.fromkeys(ids, 0)
163 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
164 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
165 res[task.project_id.id] += 1
169 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
170 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
171 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
172 '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),
173 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
174 '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)]}),
176 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
177 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)]}),
178 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
179 '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.",
181 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
182 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
184 '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.",
186 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
187 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
189 '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.",
191 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
192 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
194 '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.",
196 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
197 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
199 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
200 '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)]}),
201 '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)]}),
202 '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)]}),
203 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
204 'use_tasks': fields.boolean('Use Tasks', help="Check this field if this project is aimed at managing tasks"),
205 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"),
206 'color': fields.integer('Color Index'),
207 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
210 def dummy(self, cr, uid, ids, context):
213 def _get_type_common(self, cr, uid, context):
214 ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context)
222 'type_ids': _get_type_common,
226 # TODO: Why not using a SQL contraints ?
227 def _check_dates(self, cr, uid, ids, context=None):
228 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
229 if leave['date_start'] and leave['date']:
230 if leave['date_start'] > leave['date']:
235 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
238 def set_template(self, cr, uid, ids, context=None):
239 res = self.setActive(cr, uid, ids, value=False, context=context)
242 def set_done(self, cr, uid, ids, context=None):
243 task_obj = self.pool.get('project.task')
244 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
245 task_obj.case_close(cr, uid, task_ids, context=context)
246 self.write(cr, uid, ids, {'state':'close'}, context=context)
247 self.set_close_send_note(cr, uid, ids, context=context)
250 def set_cancel(self, cr, uid, ids, context=None):
251 task_obj = self.pool.get('project.task')
252 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
253 task_obj.case_cancel(cr, uid, task_ids, context=context)
254 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
255 self.set_cancel_send_note(cr, uid, ids, context=context)
258 def set_pending(self, cr, uid, ids, context=None):
259 self.write(cr, uid, ids, {'state':'pending'}, context=context)
260 self.set_pending_send_note(cr, uid, ids, context=context)
263 def set_open(self, cr, uid, ids, context=None):
264 self.write(cr, uid, ids, {'state':'open'}, context=context)
265 self.set_open_send_note(cr, uid, ids, context=context)
268 def reset_project(self, cr, uid, ids, context=None):
269 res = self.setActive(cr, uid, ids, value=True, context=context)
270 self.set_open_send_note(cr, uid, ids, context=context)
273 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
274 """ copy and map tasks from old to new project """
278 task_obj = self.pool.get('project.task')
279 proj = self.browse(cr, uid, old_project_id, context=context)
280 for task in proj.tasks:
281 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
282 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
283 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
286 def copy(self, cr, uid, id, default={}, context=None):
290 default = default or {}
291 context['active_test'] = False
292 default['state'] = 'open'
293 default['tasks'] = []
294 proj = self.browse(cr, uid, id, context=context)
295 if not default.get('name', False):
296 default['name'] = proj.name + _(' (copy)')
298 res = super(project, self).copy(cr, uid, id, default, context)
299 self.map_tasks(cr,uid,id,res,context)
302 def duplicate_template(self, cr, uid, ids, context=None):
305 data_obj = self.pool.get('ir.model.data')
307 for proj in self.browse(cr, uid, ids, context=context):
308 parent_id = context.get('parent_id', False)
309 context.update({'analytic_project_copy': True})
310 new_date_start = time.strftime('%Y-%m-%d')
312 if proj.date_start and proj.date:
313 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
314 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
315 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
316 context.update({'copy':True})
317 new_id = self.copy(cr, uid, proj.id, default = {
318 'name': proj.name +_(' (copy)'),
320 'date_start':new_date_start,
322 'parent_id':parent_id}, context=context)
323 result.append(new_id)
325 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
326 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
328 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
330 if result and len(result):
332 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
333 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
334 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
335 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
336 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
337 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
339 'name': _('Projects'),
341 'view_mode': 'form,tree',
342 'res_model': 'project.project',
345 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
346 'type': 'ir.actions.act_window',
347 'search_view_id': search_view['res_id'],
351 # set active value for a project, its sub projects and its tasks
352 def setActive(self, cr, uid, ids, value=True, context=None):
353 task_obj = self.pool.get('project.task')
354 for proj in self.browse(cr, uid, ids, context=None):
355 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
356 cr.execute('select id from project_task where project_id=%s', (proj.id,))
357 tasks_id = [x[0] for x in cr.fetchall()]
359 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
360 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
362 self.setActive(cr, uid, child_ids, value, context=None)
365 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
366 context = context or {}
367 if type(ids) in (long, int,):
369 projects = self.browse(cr, uid, ids, context=context)
371 for project in projects:
372 if (not project.members) and force_members:
373 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
375 resource_pool = self.pool.get('resource.resource')
377 result = "from openerp.addons.resource.faces import *\n"
378 result += "import datetime\n"
379 for project in self.browse(cr, uid, ids, context=context):
380 u_ids = [i.id for i in project.members]
381 if project.user_id and (project.user_id.id not in u_ids):
382 u_ids.append(project.user_id.id)
383 for task in project.tasks:
384 if task.state in ('done','cancelled'):
386 if task.user_id and (task.user_id.id not in u_ids):
387 u_ids.append(task.user_id.id)
388 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
389 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
390 for key, vals in resource_objs.items():
392 class User_%s(Resource):
394 ''' % (key, vals.get('efficiency', False))
401 def _schedule_project(self, cr, uid, project, context=None):
402 resource_pool = self.pool.get('resource.resource')
403 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
404 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
405 # TODO: check if we need working_..., default values are ok.
406 puids = [x.id for x in project.members]
408 puids.append(project.user_id.id)
416 project.date_start, working_days,
417 '|'.join(['User_'+str(x) for x in puids])
419 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
426 #TODO: DO Resource allocation and compute availability
427 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
433 def schedule_tasks(self, cr, uid, ids, context=None):
434 context = context or {}
435 if type(ids) in (long, int,):
437 projects = self.browse(cr, uid, ids, context=context)
438 result = self._schedule_header(cr, uid, ids, False, context=context)
439 for project in projects:
440 result += self._schedule_project(cr, uid, project, context=context)
441 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
444 exec result in local_dict
445 projects_gantt = Task.BalancedProject(local_dict['Project'])
447 for project in projects:
448 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
449 for task in project.tasks:
450 if task.state in ('done','cancelled'):
453 p = getattr(project_gantt, 'Task_%d' % (task.id,))
455 self.pool.get('project.task').write(cr, uid, [task.id], {
456 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
457 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
459 if (not task.user_id) and (p.booked_resource):
460 self.pool.get('project.task').write(cr, uid, [task.id], {
461 'user_id': int(p.booked_resource[0].name[5:]),
465 # ------------------------------------------------
466 # OpenChatter methods and notifications
467 # ------------------------------------------------
469 def get_needaction_user_ids(self, cr, uid, ids, context=None):
470 result = dict.fromkeys(ids)
471 for obj in self.browse(cr, uid, ids, context=context):
473 if obj.state == 'draft' and obj.user_id:
474 result[obj.id] = [obj.user_id.id]
477 def message_get_subscribers(self, cr, uid, ids, context=None):
478 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
479 for obj in self.browse(cr, uid, ids, context=context):
481 sub_ids.append(obj.user_id.id)
482 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
484 def create(self, cr, uid, vals, context=None):
485 obj_id = super(project, self).create(cr, uid, vals, context=context)
486 self.create_send_note(cr, uid, [obj_id], context=context)
489 def create_send_note(self, cr, uid, ids, context=None):
490 return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
492 def set_open_send_note(self, cr, uid, ids, context=None):
493 message = _("Project has been <b>opened</b>.")
494 return self.message_append_note(cr, uid, ids, body=message, context=context)
496 def set_pending_send_note(self, cr, uid, ids, context=None):
497 message = _("Project is now <b>pending</b>.")
498 return self.message_append_note(cr, uid, ids, body=message, context=context)
500 def set_cancel_send_note(self, cr, uid, ids, context=None):
501 message = _("Project has been <b>cancelled</b>.")
502 return self.message_append_note(cr, uid, ids, body=message, context=context)
504 def set_close_send_note(self, cr, uid, ids, context=None):
505 message = _("Project has been <b>closed</b>.")
506 return self.message_append_note(cr, uid, ids, body=message, context=context)
509 class task(base_stage, osv.osv):
510 _name = "project.task"
511 _description = "Task"
512 _date_name = "date_start"
513 _inherit = ['ir.needaction_mixin', 'mail.thread']
515 def _get_default_project_id(self, cr, uid, context=None):
516 """ Gives default section by checking if present in the context """
517 return (self._resolve_project_id_from_context(cr, uid, context=context) or False)
519 def _get_default_stage_id(self, cr, uid, context=None):
520 """ Gives default stage_id """
521 project_id = self._get_default_project_id(cr, uid, context=context)
522 return self.stage_find(cr, uid, [], project_id, [('state', '=', 'draft')], context=context)
524 def _resolve_project_id_from_context(self, cr, uid, context=None):
525 """ Returns ID of project based on the value of 'default_project_id'
526 context key, or None if it cannot be resolved to a single
529 if context is None: context = {}
530 if type(context.get('default_project_id')) in (int, long):
531 return context['default_project_id']
532 if isinstance(context.get('default_project_id'), basestring):
533 project_name = context['default_project_id']
534 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
535 if len(project_ids) == 1:
536 return project_ids[0][0]
539 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
540 stage_obj = self.pool.get('project.task.type')
541 order = stage_obj._order
542 access_rights_uid = access_rights_uid or uid
543 # lame way to allow reverting search, should just work in the trivial case
544 if read_group_order == 'stage_id desc':
545 order = '%s desc' % order
546 # retrieve section_id from the context and write the domain
547 # - ('id', 'in', 'ids'): add columns that should be present
548 # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded
549 # - OR ('project_ids', 'in', project_id), ('fold', '=', False) if project_id: add project columns that are not folded
551 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
553 search_domain += ['|', '&', ('project_ids', '=', project_id), ('fold', '=', False)]
554 search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
555 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
556 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
557 # restore order of the search
558 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
561 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
562 res_users = self.pool.get('res.users')
563 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
564 access_rights_uid = access_rights_uid or uid
566 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
567 order = res_users._order
568 # lame way to allow reverting search, should just work in the trivial case
569 if read_group_order == 'user_id desc':
570 order = '%s desc' % order
571 # de-duplicate and apply search order
572 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
573 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
574 # restore order of the search
575 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
579 'stage_id': _read_group_stage_ids,
580 'user_id': _read_group_user_id,
583 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
584 obj_project = self.pool.get('project.project')
586 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
587 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
588 if id and isinstance(id, (long, int)):
589 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
590 args.append(('active', '=', False))
591 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
593 def _str_get(self, task, level=0, border='***', context=None):
594 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'+ \
595 border[0]+' '+(task.name or '')+'\n'+ \
596 (task.description or '')+'\n\n'
598 # Compute: effective_hours, total_hours, progress
599 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
601 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
602 hours = dict(cr.fetchall())
603 for task in self.browse(cr, uid, ids, context=context):
604 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)}
605 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
606 res[task.id]['progress'] = 0.0
607 if (task.remaining_hours + hours.get(task.id, 0.0)):
608 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
609 if task.state in ('done','cancelled'):
610 res[task.id]['progress'] = 100.0
613 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
614 if remaining and not planned:
615 return {'value':{'planned_hours': remaining}}
618 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
619 return {'value':{'remaining_hours': planned - effective}}
621 def onchange_project(self, cr, uid, id, project_id):
624 data = self.pool.get('project.project').browse(cr, uid, [project_id])
625 partner_id=data and data[0].partner_id
627 return {'value':{'partner_id':partner_id.id}}
630 def duplicate_task(self, cr, uid, map_ids, context=None):
631 for new in map_ids.values():
632 task = self.browse(cr, uid, new, context)
633 child_ids = [ ch.id for ch in task.child_ids]
635 for child in task.child_ids:
636 if child.id in map_ids.keys():
637 child_ids.remove(child.id)
638 child_ids.append(map_ids[child.id])
640 parent_ids = [ ch.id for ch in task.parent_ids]
642 for parent in task.parent_ids:
643 if parent.id in map_ids.keys():
644 parent_ids.remove(parent.id)
645 parent_ids.append(map_ids[parent.id])
646 #FIXME why there is already the copy and the old one
647 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
649 def copy_data(self, cr, uid, id, default={}, context=None):
650 default = default or {}
651 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
652 if not default.get('remaining_hours', False):
653 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
654 default['active'] = True
655 default['stage_id'] = False
656 if not default.get('name', False):
657 default['name'] = self.browse(cr, uid, id, context=context).name or ''
658 if not context.get('copy',False):
659 new_name = _("%s (copy)")%default.get('name','')
660 default.update({'name':new_name})
661 return super(task, self).copy_data(cr, uid, id, default, context)
664 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
666 for task in self.browse(cr, uid, ids, context=context):
669 if task.project_id.active == False or task.project_id.state == 'template':
673 def _get_task(self, cr, uid, ids, context=None):
675 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
676 if work.task_id: result[work.task_id.id] = True
680 '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."),
681 'name': fields.char('Task Summary', size=128, required=True, select=True),
682 'description': fields.text('Description'),
683 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
684 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
685 'stage_id': fields.many2one('project.task.type', 'Stage',
686 domain="['|', ('project_ids', '=', project_id), ('case_default', '=', True)]"),
687 'state': fields.related('stage_id', 'state', type="selection", store=True,
688 selection=_TASK_STATE, string="State", readonly=True,
689 help='The state is set to \'Draft\', when a case is created.\
690 If the case is in progress the state is set to \'Open\'.\
691 When the case is over, the state is set to \'Done\'.\
692 If the case needs to be reviewed then the state is \
693 set to \'Pending\'.'),
694 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
695 help="A task's kanban state indicates special situations affecting it:\n"
696 " * Normal is the default situation\n"
697 " * Blocked indicates something is preventing the progress of this task\n"
698 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
699 readonly=True, required=False),
700 'create_date': fields.datetime('Create Date', readonly=True,select=True),
701 'date_start': fields.datetime('Starting Date',select=True),
702 'date_end': fields.datetime('Ending Date',select=True),
703 'date_deadline': fields.date('Deadline',select=True),
704 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
705 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
706 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
707 'notes': fields.text('Notes'),
708 '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.'),
709 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
711 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
712 'project.task.work': (_get_task, ['hours'], 10),
714 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
715 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
717 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
718 'project.task.work': (_get_task, ['hours'], 10),
720 '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",
722 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
723 'project.task.work': (_get_task, ['hours'], 10),
725 '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.",
727 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
728 'project.task.work': (_get_task, ['hours'], 10),
730 'user_id': fields.many2one('res.users', 'Assigned to'),
731 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
732 'partner_id': fields.many2one('res.partner', 'Partner'),
733 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
734 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
735 'company_id': fields.many2one('res.company', 'Company'),
736 'id': fields.integer('ID', readonly=True),
737 'color': fields.integer('Color Index'),
738 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
742 'stage_id': _get_default_stage_id,
743 'project_id': _get_default_project_id,
745 'kanban_state': 'normal',
750 'user_id': lambda obj, cr, uid, context: uid,
751 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c),
754 _order = "priority, sequence, date_start, name, id"
756 def set_priority(self, cr, uid, ids, priority):
759 return self.write(cr, uid, ids, {'priority' : priority})
761 def set_high_priority(self, cr, uid, ids, *args):
762 """Set task priority to high
764 return self.set_priority(cr, uid, ids, '1')
766 def set_normal_priority(self, cr, uid, ids, *args):
767 """Set task priority to normal
769 return self.set_priority(cr, uid, ids, '2')
771 def _check_recursion(self, cr, uid, ids, context=None):
773 visited_branch = set()
775 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
781 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
782 if id in visited_branch: #Cycle
785 if id in visited_node: #Already tested don't work one more time for nothing
788 visited_branch.add(id)
791 #visit child using DFS
792 task = self.browse(cr, uid, id, context=context)
793 for child in task.child_ids:
794 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
798 visited_branch.remove(id)
801 def _check_dates(self, cr, uid, ids, context=None):
804 obj_task = self.browse(cr, uid, ids[0], context=context)
805 start = obj_task.date_start or False
806 end = obj_task.date_end or False
813 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
814 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
817 # Override view according to the company definition
819 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
820 users_obj = self.pool.get('res.users')
821 if context is None: context = {}
822 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
823 # this should be safe (no context passed to avoid side-effects)
824 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
825 tm = obj_tm and obj_tm.name or 'Hours'
827 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
829 if tm in ['Hours','Hour']:
832 eview = etree.fromstring(res['arch'])
834 def _check_rec(eview):
835 if eview.attrib.get('widget','') == 'float_time':
836 eview.set('widget','float')
843 res['arch'] = etree.tostring(eview)
845 for f in res['fields']:
846 if 'Hours' in res['fields'][f]['string']:
847 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
850 # ****************************************
852 # ****************************************
854 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
855 """ Override of the base.stage method
856 Parameter of the stage search taken from the lead:
857 - section_id: if set, stages must belong to this section or
858 be a default stage; if not set, stages must be default
861 if isinstance(cases, (int, long)):
862 cases = self.browse(cr, uid, cases, context=context)
863 # collect all section_ids
866 section_ids.append(section_id)
869 section_ids.append(task.project_id.id)
870 # OR all section_ids and OR with case_default
873 search_domain += [('|')] * len(section_ids)
874 for section_id in section_ids:
875 search_domain.append(('project_ids', '=', section_id))
876 search_domain.append(('case_default', '=', True))
877 # AND with the domain in parameter
878 search_domain += list(domain)
879 # perform search, return the first found
880 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
885 def _check_child_task(self, cr, uid, ids, context=None):
888 tasks = self.browse(cr, uid, ids, context=context)
891 for child in task.child_ids:
892 if child.state in ['draft', 'open', 'pending']:
893 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
896 def action_close(self, cr, uid, ids, context=None):
897 """ This action closes the task, then opens the wizard to send an
898 email to the partner or the project manager.
900 task_id = len(ids) and ids[0] or False
901 self._check_child_task(cr, uid, ids, context=context)
902 if not task_id: return False
903 task = self.browse(cr, uid, task_id, context=context)
904 project = task.project_id
905 res = self.do_close(cr, uid, [task_id], context=context)
906 if project.warn_manager or project.warn_customer:
908 'name': _('Send Email after close task'),
911 'res_model': 'mail.compose.message',
912 'type': 'ir.actions.act_window',
915 'context': {'active_id': task.id,
916 'active_model': 'project.task'}
920 def do_close(self, cr, uid, ids, context=None):
921 """ Compatibility when changing to case_close. """
922 return self.case_close(cr, uid, ids, context=context)
924 def case_close(self, cr, uid, ids, context=None):
926 if not isinstance(ids, list): ids = [ids]
927 for task in self.browse(cr, uid, ids, context=context):
929 project = task.project_id
930 for parent_id in task.parent_ids:
931 if parent_id.state in ('pending','draft'):
933 for child in parent_id.child_ids:
934 if child.id != task.id and child.state not in ('done','cancelled'):
937 self.do_reopen(cr, uid, [parent_id.id], context=context)
939 vals['remaining_hours'] = 0.0
940 if not task.date_end:
941 vals['date_end'] = fields.datetime.now()
942 self.case_set(cr, uid, [task.id], 'done', vals, context=context)
943 self.case_close_send_note(cr, uid, [task.id], context=context)
946 def do_reopen(self, cr, uid, ids, context=None):
947 for task in self.browse(cr, uid, ids, context=context):
948 project = task.project_id
949 self.case_set(cr, uid, [task.id], 'open', {}, context=context)
950 self.case_open_send_note(cr, uid, [task.id], context)
953 def do_cancel(self, cr, uid, ids, context=None):
954 """ Compatibility when changing to case_cancel. """
955 return self.case_cancel(cr, uid, ids, context=context)
957 def case_cancel(self, cr, uid, ids, context=None):
958 tasks = self.browse(cr, uid, ids, context=context)
959 self._check_child_task(cr, uid, ids, context=context)
961 self.case_set(cr, uid, [task.id], 'cancelled', {'remaining_hours': 0.0}, context=context)
962 self.case_cancel_send_note(cr, uid, [task.id], context=context)
965 def do_open(self, cr, uid, ids, context=None):
966 """ Compatibility when changing to case_open. """
967 return self.case_open(cr, uid, ids, context=context)
969 def case_open(self, cr, uid, ids, context=None):
970 if not isinstance(ids,list): ids = [ids]
971 self.case_set(cr, uid, ids, 'open', {'date_start': fields.datetime.now()}, context=context)
972 self.case_open_send_note(cr, uid, ids, context)
975 def do_draft(self, cr, uid, ids, context=None):
976 """ Compatibility when changing to case_draft. """
977 return self.case_draft(cr, uid, ids, context=context)
979 def case_draft(self, cr, uid, ids, context=None):
980 self.case_set(cr, uid, ids, 'draft', {}, context=context)
981 self.case_draft_send_note(cr, uid, ids, context=context)
984 def do_pending(self, cr, uid, ids, context=None):
985 """ Compatibility when changing to case_pending. """
986 return self.case_pending(cr, uid, ids, context=context)
988 def case_pending(self, cr, uid, ids, context=None):
989 self.case_set(cr, uid, ids, 'pending', {}, context=context)
990 return self.case_pending_send_note(cr, uid, ids, context=context)
992 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
993 attachment = self.pool.get('ir.attachment')
994 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
995 new_attachment_ids = []
996 for attachment_id in attachment_ids:
997 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
998 return new_attachment_ids
1000 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1002 Delegate Task to another users.
1004 assert delegate_data['user_id'], _("Delegated User should be specified")
1005 delegated_tasks = {}
1006 for task in self.browse(cr, uid, ids, context=context):
1007 delegated_task_id = self.copy(cr, uid, task.id, {
1008 'name': delegate_data['name'],
1009 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1010 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1011 'planned_hours': delegate_data['planned_hours'] or 0.0,
1012 'parent_ids': [(6, 0, [task.id])],
1014 'description': delegate_data['new_task_description'] or '',
1018 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1019 newname = delegate_data['prefix'] or ''
1021 'remaining_hours': delegate_data['planned_hours_me'],
1022 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1025 if delegate_data['state'] == 'pending':
1026 self.do_pending(cr, uid, [task.id], context=context)
1027 elif delegate_data['state'] == 'done':
1028 self.do_close(cr, uid, [task.id], context=context)
1029 self.do_delegation_send_note(cr, uid, [task.id], context)
1030 delegated_tasks[task.id] = delegated_task_id
1031 return delegated_tasks
1033 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1034 for task in self.browse(cr, uid, ids, context=context):
1035 if (task.state=='draft') or (task.planned_hours==0.0):
1036 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1037 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1040 def set_remaining_time_1(self, cr, uid, ids, context=None):
1041 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1043 def set_remaining_time_2(self, cr, uid, ids, context=None):
1044 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1046 def set_remaining_time_5(self, cr, uid, ids, context=None):
1047 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1049 def set_remaining_time_10(self, cr, uid, ids, context=None):
1050 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1052 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1053 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1055 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1056 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1058 def set_kanban_state_done(self, cr, uid, ids, context=None):
1059 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1061 def _store_history(self, cr, uid, ids, context=None):
1062 for task in self.browse(cr, uid, ids, context=context):
1063 self.pool.get('project.task.history').create(cr, uid, {
1065 'remaining_hours': task.remaining_hours,
1066 'planned_hours': task.planned_hours,
1067 'kanban_state': task.kanban_state,
1068 'type_id': task.stage_id.id,
1069 'state': task.state,
1070 'user_id': task.user_id.id
1075 def create(self, cr, uid, vals, context=None):
1076 task_id = super(task, self).create(cr, uid, vals, context=context)
1077 self._store_history(cr, uid, [task_id], context=context)
1078 self.create_send_note(cr, uid, [task_id], context=context)
1081 # Overridden to reset the kanban_state to normal whenever
1082 # the stage (stage_id) of the task changes.
1083 def write(self, cr, uid, ids, vals, context=None):
1084 if isinstance(ids, (int, long)):
1086 if vals and not 'kanban_state' in vals and 'stage_id' in vals:
1087 new_stage = vals.get('stage_id')
1088 vals_reset_kstate = dict(vals, kanban_state='normal')
1089 for t in self.browse(cr, uid, ids, context=context):
1090 #TO FIX:Kanban view doesn't raise warning
1091 #stages = [stage.id for stage in t.project_id.type_ids]
1092 #if new_stage not in stages:
1093 #raise osv.except_osv(_('Warning !'), _('Stage is not defined in the project.'))
1094 write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
1095 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1098 result = super(task,self).write(cr, uid, ids, vals, context=context)
1099 if ('stage_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1100 self._store_history(cr, uid, ids, context=context)
1103 def unlink(self, cr, uid, ids, context=None):
1106 self._check_child_task(cr, uid, ids, context=context)
1107 res = super(task, self).unlink(cr, uid, ids, context)
1110 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1111 context = context or {}
1115 if task.state in ('done','cancelled'):
1120 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1122 for t2 in task.parent_ids:
1123 start.append("up.Task_%s.end" % (t2.id,))
1127 ''' % (ident,','.join(start))
1132 ''' % (ident, 'User_'+str(task.user_id.id))
1137 # ---------------------------------------------------
1138 # OpenChatter methods and notifications
1139 # ---------------------------------------------------
1141 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
1142 """ Override of default prefix for notifications. """
1145 def get_needaction_user_ids(self, cr, uid, ids, context=None):
1146 result = dict.fromkeys(ids, [])
1147 for obj in self.browse(cr, uid, ids, context=context):
1148 if obj.state == 'draft' and obj.user_id:
1149 result[obj.id] = [obj.user_id.id]
1152 def message_get_subscribers(self, cr, uid, ids, context=None):
1153 sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1154 for obj in self.browse(cr, uid, ids, context=context):
1156 sub_ids.append(obj.user_id.id)
1158 sub_ids.append(obj.manager_id.id)
1159 return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1161 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
1162 """ Override of the (void) default notification method. """
1163 stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
1164 return self.message_append_note(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
1166 def create_send_note(self, cr, uid, ids, context=None):
1167 return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1169 def case_draft_send_note(self, cr, uid, ids, context=None):
1170 msg = _('Task has been set as <b>draft</b>.')
1171 return self.message_append_note(cr, uid, ids, body=msg, context=context)
1173 def do_delegation_send_note(self, cr, uid, ids, context=None):
1174 for task in self.browse(cr, uid, ids, context=context):
1175 msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1176 self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1180 class project_work(osv.osv):
1181 _name = "project.task.work"
1182 _description = "Project Task Work"
1184 'name': fields.char('Work summary', size=128),
1185 'date': fields.datetime('Date', select="1"),
1186 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1187 'hours': fields.float('Time Spent'),
1188 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1189 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1193 'user_id': lambda obj, cr, uid, context: uid,
1194 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1197 _order = "date desc"
1198 def create(self, cr, uid, vals, *args, **kwargs):
1199 if 'hours' in vals and (not vals['hours']):
1200 vals['hours'] = 0.00
1201 if 'task_id' in vals:
1202 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1203 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1205 def write(self, cr, uid, ids, vals, context=None):
1206 if 'hours' in vals and (not vals['hours']):
1207 vals['hours'] = 0.00
1209 for work in self.browse(cr, uid, ids, context=context):
1210 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))
1211 return super(project_work,self).write(cr, uid, ids, vals, context)
1213 def unlink(self, cr, uid, ids, *args, **kwargs):
1214 for work in self.browse(cr, uid, ids):
1215 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1216 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1219 class account_analytic_account(osv.osv):
1221 _inherit = 'account.analytic.account'
1222 _description = 'Analytic Account'
1224 def create(self, cr, uid, vals, context=None):
1227 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1228 vals['child_ids'] = []
1229 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1231 def unlink(self, cr, uid, ids, *args, **kwargs):
1232 project_obj = self.pool.get('project.project')
1233 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1235 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1236 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1240 # Tasks History, used for cumulative flow charts (Lean/Agile)
1243 class project_task_history(osv.osv):
1244 _name = 'project.task.history'
1245 _description = 'History of Tasks'
1246 _rec_name = 'task_id'
1248 def _get_date(self, cr, uid, ids, name, arg, context=None):
1250 for history in self.browse(cr, uid, ids, context=context):
1251 if history.state in ('done','cancelled'):
1252 result[history.id] = history.date
1254 cr.execute('''select
1257 project_task_history
1261 order by id limit 1''', (history.task_id.id, history.id))
1263 result[history.id] = res and res[0] or False
1266 def _get_related_date(self, cr, uid, ids, context=None):
1268 for history in self.browse(cr, uid, ids, context=context):
1269 cr.execute('''select
1272 project_task_history
1276 order by id desc limit 1''', (history.task_id.id, history.id))
1279 result.append(res[0])
1283 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1284 'type_id': fields.many2one('project.task.type', 'Stage'),
1285 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status'),
1286 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1287 'date': fields.date('Date', select=True),
1288 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1289 'project.task.history': (_get_related_date, None, 20)
1291 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1292 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1293 'user_id': fields.many2one('res.users', 'Responsible'),
1296 'date': fields.date.context_today,
1300 class project_task_history_cumulative(osv.osv):
1301 _name = 'project.task.history.cumulative'
1302 _table = 'project_task_history_cumulative'
1303 _inherit = 'project.task.history'
1306 'end_date': fields.date('End Date'),
1307 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1310 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1312 history.date::varchar||'-'||history.history_id::varchar as id,
1313 history.date as end_date,
1318 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1319 task_id, type_id, user_id, kanban_state, state,
1320 greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours
1322 project_task_history