1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from lxml import etree
24 from datetime import datetime, date
26 from tools.translate import _
27 from osv import fields, osv
28 from openerp.addons.resource.faces import task as Task
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 # _name = 'project.project'
35 class project_task_type(osv.osv):
36 _name = 'project.task.type'
37 _description = 'Task Stage'
40 'name': fields.char('Stage Name', required=True, size=64, translate=True),
41 'description': fields.text('Description'),
42 'sequence': fields.integer('Sequence'),
43 'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
52 class project(osv.osv):
53 _name = "project.project"
54 _description = "Project"
55 _inherits = {'account.analytic.account': "analytic_account_id"}
57 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
59 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
60 if context and context.get('user_preference'):
61 cr.execute("""SELECT project.id FROM project_project project
62 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
63 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
64 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
65 return [(r[0]) for r in cr.fetchall()]
66 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
67 context=context, count=count)
69 def _complete_name(self, cr, uid, ids, name, args, context=None):
71 for m in self.browse(cr, uid, ids, context=context):
72 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
75 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
76 partner_obj = self.pool.get('res.partner')
80 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
81 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
82 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
83 val['pricelist_id'] = pricelist_id
86 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
87 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
88 project_ids = [task.project_id.id for task in tasks if task.project_id]
89 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
91 def _get_project_and_parents(self, cr, uid, ids, context=None):
92 """ return the project ids and all their parent projects """
96 SELECT DISTINCT parent.id
97 FROM project_project project, project_project parent, account_analytic_account account
98 WHERE project.analytic_account_id = account.id
99 AND parent.analytic_account_id = account.parent_id
102 ids = [t[0] for t in cr.fetchall()]
106 def _get_project_and_children(self, cr, uid, ids, context=None):
107 """ retrieve all children projects of project ids;
108 return a dictionary mapping each project to its parent project (or None)
110 res = dict.fromkeys(ids, None)
113 SELECT project.id, parent.id
114 FROM project_project project, project_project parent, account_analytic_account account
115 WHERE project.analytic_account_id = account.id
116 AND parent.analytic_account_id = account.parent_id
119 dic = dict(cr.fetchall())
124 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
125 child_parent = self._get_project_and_children(cr, uid, ids, context)
126 # compute planned_hours, total_hours, effective_hours specific to each project
128 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
129 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
130 FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
132 """, (tuple(child_parent.keys()),))
133 # aggregate results into res
134 res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
135 for id, planned, total, effective in cr.fetchall():
136 # add the values specific to id to all parent projects of id in the result
139 res[id]['planned_hours'] += planned
140 res[id]['total_hours'] += total
141 res[id]['effective_hours'] += effective
142 id = child_parent[id]
143 # compute progress rates
145 if res[id]['total_hours']:
146 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
148 res[id]['progress_rate'] = 0.0
151 def unlink(self, cr, uid, ids, *args, **kwargs):
152 for proj in self.browse(cr, uid, ids):
154 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
155 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
157 def _open_task(self, cr, uid, ids, field_name, arg, context=None):
159 task_pool=self.pool.get('project.task')
161 task_ids = task_pool.search(cr, uid, [('project_id', '=', id)])
162 open_task[id] = len(task_ids)
166 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
167 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
168 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
169 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True),
170 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
171 'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive an email each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
173 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
174 help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
175 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
176 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
178 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
179 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
181 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
183 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
184 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
186 'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
188 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
189 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
191 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
193 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
194 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
196 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
197 'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
198 'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
199 'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
200 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
201 'task': fields.boolean('Task',help = "If you check this field tasks appears in kanban view"),
202 'open_task': fields.function(_open_task , type='integer',string="Open Tasks"),
204 def dummy(self, cr, uid, ids, context=None):
207 def open_tasks(self, cr, uid, ids, context=None):
208 #Open the View for the Tasks for the project
210 This opens Tasks views
211 @return :Dictionary value for task view
216 data_obj = self.pool.get('ir.model.data')
217 for project in self.browse(cr, uid, ids, context=context):
219 tree_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_tree2')
220 form_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_form2')
221 calander_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_calendar')
222 search_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_search_form')
223 kanban_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_kanban')
225 #'search_default_user_id': uid,
226 'search_default_project_id':project.id,
227 #'search_default_open':1,
233 'view_mode': 'form,tree',
234 'res_model': 'project.task',
236 'domain':[('project_id','in',ids)],
238 'views': [(kanban_view and kanban_view[1] or False, 'kanban'),(tree_view and tree_view[1] or False, 'tree'),(calander_view and calander_view[1] or False, 'calendar'),(form_view and form_view[1] or False, 'form')],
239 'type': 'ir.actions.act_window',
240 'search_view_id': search_view and search_view[1] or False,
245 def open_users(self, cr, uid, ids, context=None):
246 #Open the View for the Tasks for the project
248 This opens Tasks views
249 @return :Dictionary value for task view
254 data_obj = self.pool.get('ir.model.data')
255 for project in self.browse(cr, uid, ids, context=context):
257 tree_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_tree')
258 form_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_form')
259 search_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_search')
265 'view_mode': 'form,tree',
266 'res_model': 'res.users',
269 'res_id': project.user_id.id,
270 'views': [(form_view and form_view[1] or False, 'form'),(tree_view and tree_view[1] or False, 'tree')],
271 'type': 'ir.actions.act_window',
272 'search_view_id': search_view and search_view[1] or False,
277 def _get_type_common(self, cr, uid, context):
278 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
286 'type_ids': _get_type_common,
290 # TODO: Why not using a SQL contraints ?
291 def _check_dates(self, cr, uid, ids, context=None):
292 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
293 if leave['date_start'] and leave['date']:
294 if leave['date_start'] > leave['date']:
299 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
302 def set_template(self, cr, uid, ids, context=None):
303 res = self.setActive(cr, uid, ids, value=False, context=context)
306 def set_done(self, cr, uid, ids, context=None):
307 task_obj = self.pool.get('project.task')
308 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
309 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
310 self.write(cr, uid, ids, {'state':'close'}, context=context)
311 for (id, name) in self.name_get(cr, uid, ids):
312 message = _("The project '%s' has been closed.") % name
313 self.log(cr, uid, id, message)
316 def set_cancel(self, cr, uid, ids, context=None):
317 task_obj = self.pool.get('project.task')
318 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
319 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
320 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
323 def set_pending(self, cr, uid, ids, context=None):
324 self.write(cr, uid, ids, {'state':'pending'}, context=context)
327 def set_open(self, cr, uid, ids, context=None):
328 self.write(cr, uid, ids, {'state':'open'}, context=context)
331 def reset_project(self, cr, uid, ids, context=None):
332 res = self.setActive(cr, uid, ids, value=True, context=context)
333 for (id, name) in self.name_get(cr, uid, ids):
334 message = _("The project '%s' has been opened.") % name
335 self.log(cr, uid, id, message)
338 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
339 """ copy and map tasks from old to new project """
343 task_obj = self.pool.get('project.task')
344 proj = self.browse(cr, uid, old_project_id, context=context)
345 for task in proj.tasks:
346 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
347 self.write(cr, uid, new_project_id, {'tasks':[(6,0, map_task_id.values())]})
348 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
351 def copy(self, cr, uid, id, default={}, context=None):
355 default = default or {}
356 context['active_test'] = False
357 default['state'] = 'open'
358 default['tasks'] = []
359 proj = self.browse(cr, uid, id, context=context)
360 if not default.get('name', False):
361 default['name'] = proj.name + _(' (copy)')
363 res = super(project, self).copy(cr, uid, id, default, context)
364 self.map_tasks(cr,uid,id,res,context)
367 def duplicate_template(self, cr, uid, ids, context=None):
370 data_obj = self.pool.get('ir.model.data')
372 for proj in self.browse(cr, uid, ids, context=context):
373 parent_id = context.get('parent_id', False)
374 context.update({'analytic_project_copy': True})
375 new_date_start = time.strftime('%Y-%m-%d')
377 if proj.date_start and proj.date:
378 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
379 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
380 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
381 context.update({'copy':True})
382 new_id = self.copy(cr, uid, proj.id, default = {
383 'name': proj.name +_(' (copy)'),
385 'date_start':new_date_start,
387 'parent_id':parent_id}, context=context)
388 result.append(new_id)
390 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
391 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
393 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
395 if result and len(result):
397 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
398 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
399 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
400 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
401 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
402 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
404 'name': _('Projects'),
406 'view_mode': 'form,tree',
407 'res_model': 'project.project',
410 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
411 'type': 'ir.actions.act_window',
412 'search_view_id': search_view['res_id'],
416 # set active value for a project, its sub projects and its tasks
417 def setActive(self, cr, uid, ids, value=True, context=None):
418 task_obj = self.pool.get('project.task')
419 for proj in self.browse(cr, uid, ids, context=None):
420 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
421 cr.execute('select id from project_task where project_id=%s', (proj.id,))
422 tasks_id = [x[0] for x in cr.fetchall()]
424 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
425 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
427 self.setActive(cr, uid, child_ids, value, context=None)
430 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
431 context = context or {}
432 if type(ids) in (long, int,):
434 projects = self.browse(cr, uid, ids, context=context)
436 for project in projects:
437 if (not project.members) and force_members:
438 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
440 resource_pool = self.pool.get('resource.resource')
442 result = "from openerp.addons.resource.faces import *\n"
443 result += "import datetime\n"
444 for project in self.browse(cr, uid, ids, context=context):
445 u_ids = [i.id for i in project.members]
446 if project.user_id and (project.user_id.id not in u_ids):
447 u_ids.append(project.user_id.id)
448 for task in project.tasks:
449 if task.state in ('done','cancelled'):
451 if task.user_id and (task.user_id.id not in u_ids):
452 u_ids.append(task.user_id.id)
453 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
454 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
455 for key, vals in resource_objs.items():
457 class User_%s(Resource):
459 ''' % (key, vals.get('efficiency', False))
466 def _schedule_project(self, cr, uid, project, context=None):
467 resource_pool = self.pool.get('resource.resource')
468 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
469 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
470 # TODO: check if we need working_..., default values are ok.
471 puids = [x.id for x in project.members]
473 puids.append(project.user_id.id)
481 project.date_start, working_days,
482 '|'.join(['User_'+str(x) for x in puids])
484 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
491 #TODO: DO Resource allocation and compute availability
492 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
498 def schedule_tasks(self, cr, uid, ids, context=None):
499 context = context or {}
500 if type(ids) in (long, int,):
502 projects = self.browse(cr, uid, ids, context=context)
503 result = self._schedule_header(cr, uid, ids, False, context=context)
504 for project in projects:
505 result += self._schedule_project(cr, uid, project, context=context)
506 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
509 exec result in local_dict
510 projects_gantt = Task.BalancedProject(local_dict['Project'])
512 for project in projects:
513 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
514 for task in project.tasks:
515 if task.state in ('done','cancelled'):
518 p = getattr(project_gantt, 'Task_%d' % (task.id,))
520 self.pool.get('project.task').write(cr, uid, [task.id], {
521 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
522 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
524 if (not task.user_id) and (p.booked_resource):
525 self.pool.get('project.task').write(cr, uid, [task.id], {
526 'user_id': int(p.booked_resource[0].name[5:]),
531 class users(osv.osv):
532 _inherit = 'res.users'
534 'context_project_id': fields.many2one('project.project', 'Project')
539 _name = "project.task"
540 _description = "Task"
542 _date_name = "date_start"
545 def _resolve_project_id_from_context(self, cr, uid, context=None):
546 """Return ID of project based on the value of 'project_id'
547 context key, or None if it cannot be resolved to a single project.
549 if context is None: context = {}
550 if type(context.get('project_id')) in (int, long):
551 project_id = context['project_id']
553 if isinstance(context.get('project_id'), basestring):
554 project_name = context['project_id']
555 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
556 if len(project_ids) == 1:
557 return project_ids[0][0]
559 def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
560 stage_obj = self.pool.get('project.task.type')
561 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
562 order = stage_obj._order
563 access_rights_uid = access_rights_uid or uid
564 if read_group_order == 'type_id desc':
565 # lame way to allow reverting search, should just work in the trivial case
566 order = '%s desc' % order
568 domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
570 domain = ['|', ('id','in',ids), ('project_default','=',1)]
571 stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
572 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
573 # restore order of the search
574 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
577 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
578 res_users = self.pool.get('res.users')
579 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
580 access_rights_uid = access_rights_uid or uid
582 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
583 order = res_users._order
584 # lame way to allow reverting search, should just work in the trivial case
585 if read_group_order == 'user_id desc':
586 order = '%s desc' % order
587 # de-duplicate and apply search order
588 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
589 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
590 # restore order of the search
591 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
595 'type_id': _read_group_type_id,
596 'user_id': _read_group_user_id
600 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
601 obj_project = self.pool.get('project.project')
603 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
604 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
605 if id and isinstance(id, (long, int)):
606 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
607 args.append(('active', '=', False))
608 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
610 def _str_get(self, task, level=0, border='***', context=None):
611 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'+ \
612 border[0]+' '+(task.name or '')+'\n'+ \
613 (task.description or '')+'\n\n'
615 # Compute: effective_hours, total_hours, progress
616 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
618 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
619 hours = dict(cr.fetchall())
620 for task in self.browse(cr, uid, ids, context=context):
621 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)}
622 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
623 res[task.id]['progress'] = 0.0
624 if (task.remaining_hours + hours.get(task.id, 0.0)):
625 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
626 if task.state in ('done','cancelled'):
627 res[task.id]['progress'] = 100.0
631 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
632 if remaining and not planned:
633 return {'value':{'planned_hours': remaining}}
636 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
637 return {'value':{'remaining_hours': planned - effective}}
639 def onchange_project(self, cr, uid, id, project_id):
642 data = self.pool.get('project.project').browse(cr, uid, [project_id])
643 partner_id=data and data[0].partner_id
645 return {'value':{'partner_id':partner_id.id}}
648 def duplicate_task(self, cr, uid, map_ids, context=None):
649 for new in map_ids.values():
650 task = self.browse(cr, uid, new, context)
651 child_ids = [ ch.id for ch in task.child_ids]
653 for child in task.child_ids:
654 if child.id in map_ids.keys():
655 child_ids.remove(child.id)
656 child_ids.append(map_ids[child.id])
658 parent_ids = [ ch.id for ch in task.parent_ids]
660 for parent in task.parent_ids:
661 if parent.id in map_ids.keys():
662 parent_ids.remove(parent.id)
663 parent_ids.append(map_ids[parent.id])
664 #FIXME why there is already the copy and the old one
665 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
667 def copy_data(self, cr, uid, id, default={}, context=None):
668 default = default or {}
669 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
670 if not default.get('remaining_hours', False):
671 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
672 default['active'] = True
673 default['type_id'] = False
674 if not default.get('name', False):
675 default['name'] = self.browse(cr, uid, id, context=context).name or ''
676 if not context.get('copy',False):
677 new_name = _("%s (copy)")%default.get('name','')
678 default.update({'name':new_name})
679 return super(task, self).copy_data(cr, uid, id, default, context)
682 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
684 for task in self.browse(cr, uid, ids, context=context):
687 if task.project_id.active == False or task.project_id.state == 'template':
691 def _get_task(self, cr, uid, ids, context=None):
693 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
694 if work.task_id: result[work.task_id.id] = True
698 '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."),
699 'name': fields.char('Task Summary', size=128, required=True, select=True),
700 'description': fields.text('Description'),
701 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
702 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
703 'type_id': fields.many2one('project.task.type', 'Stage'),
704 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
705 help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
706 \n If the task is over, the states is set to \'Done\'.'),
707 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
708 help="A task's kanban state indicates special situations affecting it:\n"
709 " * Normal is the default situation\n"
710 " * Blocked indicates something is preventing the progress of this task\n"
711 " * Ready To Pull indicates the task is ready to be pulled to the next stage",
712 readonly=True, required=False),
713 'create_date': fields.datetime('Create Date', readonly=True,select=True),
714 'date_start': fields.datetime('Starting Date',select=True),
715 'date_end': fields.datetime('Ending Date',select=True),
716 'date_deadline': fields.date('Deadline',select=True),
717 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
718 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
719 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
720 'notes': fields.text('Notes'),
721 '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.'),
722 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
724 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
725 'project.task.work': (_get_task, ['hours'], 10),
727 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
728 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
730 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
731 'project.task.work': (_get_task, ['hours'], 10),
733 '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",
735 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
736 'project.task.work': (_get_task, ['hours'], 10),
738 '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.",
740 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
741 'project.task.work': (_get_task, ['hours'], 10),
743 'user_id': fields.many2one('res.users', 'Assigned to'),
744 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
745 'partner_id': fields.many2one('res.partner', 'Partner'),
746 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
747 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
748 'company_id': fields.many2one('res.company', 'Company'),
749 'id': fields.integer('ID', readonly=True),
750 'color': fields.integer('Color Index'),
751 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
756 'kanban_state': 'normal',
761 'user_id': lambda obj, cr, uid, context: uid,
762 'project_id':lambda self, cr, uid, context: context.get('active_id',False),
763 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
766 _order = "priority, sequence, date_start, name, id"
768 def set_priority(self, cr, uid, ids, priority):
771 return self.write(cr, uid, ids, {'priority' : priority})
773 def set_high_priority(self, cr, uid, ids, *args):
774 """Set task priority to high
776 return self.set_priority(cr, uid, ids, '1')
778 def set_normal_priority(self, cr, uid, ids, *args):
779 """Set task priority to normal
781 return self.set_priority(cr, uid, ids, '2')
783 def _check_recursion(self, cr, uid, ids, context=None):
785 visited_branch = set()
787 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
793 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
794 if id in visited_branch: #Cycle
797 if id in visited_node: #Already tested don't work one more time for nothing
800 visited_branch.add(id)
803 #visit child using DFS
804 task = self.browse(cr, uid, id, context=context)
805 for child in task.child_ids:
806 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
810 visited_branch.remove(id)
813 def _check_dates(self, cr, uid, ids, context=None):
816 obj_task = self.browse(cr, uid, ids[0], context=context)
817 start = obj_task.date_start or False
818 end = obj_task.date_end or False
825 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
826 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
829 # Override view according to the company definition
831 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
832 users_obj = self.pool.get('res.users')
834 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
835 # this should be safe (no context passed to avoid side-effects)
836 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
837 tm = obj_tm and obj_tm.name or 'Hours'
839 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
841 if tm in ['Hours','Hour']:
844 eview = etree.fromstring(res['arch'])
846 def _check_rec(eview):
847 if eview.attrib.get('widget','') == 'float_time':
848 eview.set('widget','float')
855 res['arch'] = etree.tostring(eview)
857 for f in res['fields']:
858 if 'Hours' in res['fields'][f]['string']:
859 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
862 def _check_child_task(self, cr, uid, ids, context=None):
865 tasks = self.browse(cr, uid, ids, context=context)
868 for child in task.child_ids:
869 if child.state in ['draft', 'open', 'pending']:
870 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
873 def action_close(self, cr, uid, ids, context=None):
874 # This action open wizard to send email to partner or project manager after close task.
877 task_id = len(ids) and ids[0] or False
878 self._check_child_task(cr, uid, ids, context=context)
879 if not task_id: return False
880 task = self.browse(cr, uid, task_id, context=context)
881 project = task.project_id
882 res = self.do_close(cr, uid, [task_id], context=context)
883 if project.warn_manager or project.warn_customer:
885 'name': _('Send Email after close task'),
888 'res_model': 'mail.compose.message',
889 'type': 'ir.actions.act_window',
892 'context': {'active_id': task.id,
893 'active_model': 'project.task'}
897 def do_close(self, cr, uid, ids, context={}):
901 request = self.pool.get('res.request')
902 if not isinstance(ids,list): ids = [ids]
903 for task in self.browse(cr, uid, ids, context=context):
905 project = task.project_id
907 # Send request to project manager
908 if project.warn_manager and project.user_id and (project.user_id.id != uid):
909 request.create(cr, uid, {
910 'name': _("Task '%s' closed") % task.name,
913 'act_to': project.user_id.id,
914 'ref_partner_id': task.partner_id.id,
915 'ref_doc1': 'project.task,%d'% (task.id,),
916 'ref_doc2': 'project.project,%d'% (project.id,),
919 for parent_id in task.parent_ids:
920 if parent_id.state in ('pending','draft'):
922 for child in parent_id.child_ids:
923 if child.id != task.id and child.state not in ('done','cancelled'):
926 self.do_reopen(cr, uid, [parent_id.id], context=context)
927 vals.update({'state': 'done'})
928 vals.update({'remaining_hours': 0.0})
929 if not task.date_end:
930 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
931 self.write(cr, uid, [task.id],vals, context=context)
932 message = _("The task '%s' is done") % (task.name,)
933 self.log(cr, uid, task.id, message)
936 def do_reopen(self, cr, uid, ids, context=None):
937 request = self.pool.get('res.request')
939 for task in self.browse(cr, uid, ids, context=context):
940 project = task.project_id
941 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
942 request.create(cr, uid, {
943 'name': _("Task '%s' set in progress") % task.name,
946 'act_to': project.user_id.id,
947 'ref_partner_id': task.partner_id.id,
948 'ref_doc1': 'project.task,%d' % task.id,
949 'ref_doc2': 'project.project,%d' % project.id,
952 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
955 def do_cancel(self, cr, uid, ids, context={}):
956 request = self.pool.get('res.request')
957 tasks = self.browse(cr, uid, ids, context=context)
958 self._check_child_task(cr, uid, ids, context=context)
960 project = task.project_id
961 if project.warn_manager and project.user_id and (project.user_id.id != uid):
962 request.create(cr, uid, {
963 'name': _("Task '%s' cancelled") % task.name,
966 'act_to': project.user_id.id,
967 'ref_partner_id': task.partner_id.id,
968 'ref_doc1': 'project.task,%d' % task.id,
969 'ref_doc2': 'project.project,%d' % project.id,
971 message = _("The task '%s' is cancelled.") % (task.name,)
972 self.log(cr, uid, task.id, message)
973 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
976 def do_open(self, cr, uid, ids, context={}):
977 if not isinstance(ids,list): ids = [ids]
978 tasks= self.browse(cr, uid, ids, context=context)
980 data = {'state': 'open'}
982 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
983 self.write(cr, uid, [t.id], data, context=context)
984 message = _("The task '%s' is opened.") % (t.name,)
985 self.log(cr, uid, t.id, message)
988 def do_draft(self, cr, uid, ids, context={}):
989 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
993 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
994 attachment = self.pool.get('ir.attachment')
995 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
996 new_attachment_ids = []
997 for attachment_id in attachment_ids:
998 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
999 return new_attachment_ids
1002 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1004 Delegate Task to another users.
1006 assert delegate_data['user_id'], _("Delegated User should be specified")
1007 delegated_tasks = {}
1008 for task in self.browse(cr, uid, ids, context=context):
1009 delegated_task_id = self.copy(cr, uid, task.id, {
1010 'name': delegate_data['name'],
1011 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1012 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1013 'planned_hours': delegate_data['planned_hours'] or 0.0,
1014 'parent_ids': [(6, 0, [task.id])],
1016 'description': delegate_data['new_task_description'] or '',
1020 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1021 newname = delegate_data['prefix'] or ''
1023 'remaining_hours': delegate_data['planned_hours_me'],
1024 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1027 if delegate_data['state'] == 'pending':
1028 self.do_pending(cr, uid, task.id, context=context)
1029 elif delegate_data['state'] == 'done':
1030 self.do_close(cr, uid, task.id, context=context)
1032 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
1033 self.log(cr, uid, task.id, message)
1034 delegated_tasks[task.id] = delegated_task_id
1035 return delegated_tasks
1037 def do_pending(self, cr, uid, ids, context={}):
1038 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1039 for (id, name) in self.name_get(cr, uid, ids):
1040 message = _("The task '%s' is pending.") % name
1041 self.log(cr, uid, id, message)
1044 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1045 for task in self.browse(cr, uid, ids, context=context):
1046 if (task.state=='draft') or (task.planned_hours==0.0):
1047 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1048 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1051 def set_remaining_time_1(self, cr, uid, ids, context=None):
1052 return self.set_remaining_time(cr, uid, ids, 1.0, context)
1054 def set_remaining_time_2(self, cr, uid, ids, context=None):
1055 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1057 def set_remaining_time_5(self, cr, uid, ids, context=None):
1058 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1060 def set_remaining_time_10(self, cr, uid, ids, context=None):
1061 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1063 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1064 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1066 def set_kanban_state_normal(self, cr, uid, ids, context=None):
1067 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1069 def set_kanban_state_done(self, cr, uid, ids, context=None):
1070 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1072 def _change_type(self, cr, uid, ids, next, *args):
1074 go to the next stage
1075 if next is False, go to previous stage
1077 for task in self.browse(cr, uid, ids):
1078 if task.project_id.type_ids:
1079 typeid = task.type_id.id
1081 for type in task.project_id.type_ids :
1082 types_seq[type.id] = type.sequence
1084 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1086 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1087 sorted_types = [x[0] for x in types]
1089 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1090 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1091 index = sorted_types.index(typeid)
1092 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1095 def next_type(self, cr, uid, ids, *args):
1096 return self._change_type(cr, uid, ids, True, *args)
1098 def prev_type(self, cr, uid, ids, *args):
1099 return self._change_type(cr, uid, ids, False, *args)
1101 def _store_history(self, cr, uid, ids, context=None):
1102 for task in self.browse(cr, uid, ids, context=context):
1103 self.pool.get('project.task.history').create(cr, uid, {
1105 'remaining_hours': task.remaining_hours,
1106 'planned_hours': task.planned_hours,
1107 'kanban_state': task.kanban_state,
1108 'type_id': task.type_id.id,
1109 'state': task.state,
1110 'user_id': task.user_id.id
1115 def create(self, cr, uid, vals, context=None):
1116 result = super(task, self).create(cr, uid, vals, context=context)
1117 self._store_history(cr, uid, [result], context=context)
1120 # Overridden to reset the kanban_state to normal whenever
1121 # the stage (type_id) of the task changes.
1122 def write(self, cr, uid, ids, vals, context=None):
1123 if isinstance(ids, (int, long)):
1125 if vals and not 'kanban_state' in vals and 'type_id' in vals:
1126 new_stage = vals.get('type_id')
1127 vals_reset_kstate = dict(vals, kanban_state='normal')
1128 for t in self.browse(cr, uid, ids, context=context):
1129 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1130 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1133 result = super(task,self).write(cr, uid, ids, vals, context=context)
1134 if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1135 self._store_history(cr, uid, ids, context=context)
1138 def unlink(self, cr, uid, ids, context=None):
1141 self._check_child_task(cr, uid, ids, context=context)
1142 res = super(task, self).unlink(cr, uid, ids, context)
1145 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1146 context = context or {}
1150 if task.state in ('done','cancelled'):
1155 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1157 for t2 in task.parent_ids:
1158 start.append("up.Task_%s.end" % (t2.id,))
1162 ''' % (ident,','.join(start))
1167 ''' % (ident, 'User_'+str(task.user_id.id))
1174 class project_work(osv.osv):
1175 _name = "project.task.work"
1176 _description = "Project Task Work"
1178 'name': fields.char('Work summary', size=128),
1179 'date': fields.datetime('Date', select="1"),
1180 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1181 'hours': fields.float('Time Spent'),
1182 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1183 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1187 'user_id': lambda obj, cr, uid, context: uid,
1188 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1191 _order = "date desc"
1192 def create(self, cr, uid, vals, *args, **kwargs):
1193 if 'hours' in vals and (not vals['hours']):
1194 vals['hours'] = 0.00
1195 if 'task_id' in vals:
1196 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1197 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1199 def write(self, cr, uid, ids, vals, context=None):
1200 if 'hours' in vals and (not vals['hours']):
1201 vals['hours'] = 0.00
1203 for work in self.browse(cr, uid, ids, context=context):
1204 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))
1205 return super(project_work,self).write(cr, uid, ids, vals, context)
1207 def unlink(self, cr, uid, ids, *args, **kwargs):
1208 for work in self.browse(cr, uid, ids):
1209 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1210 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1213 class account_analytic_account(osv.osv):
1215 _inherit = 'account.analytic.account'
1216 _description = 'Analytic Account'
1218 def create(self, cr, uid, vals, context=None):
1221 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1222 vals['child_ids'] = []
1223 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1225 def unlink(self, cr, uid, ids, *args, **kwargs):
1226 project_obj = self.pool.get('project.project')
1227 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1229 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1230 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1232 account_analytic_account()
1235 # Tasks History, used for cumulative flow charts (Lean/Agile)
1238 class project_task_history(osv.osv):
1239 _name = 'project.task.history'
1240 _description = 'History of Tasks'
1241 _rec_name = 'task_id'
1243 def _get_date(self, cr, uid, ids, name, arg, context=None):
1245 for history in self.browse(cr, uid, ids, context=context):
1246 if history.state in ('done','cancelled'):
1247 result[history.id] = history.date
1249 cr.execute('''select
1252 project_task_history
1256 order by id limit 1''', (history.task_id.id, history.id))
1258 result[history.id] = res and res[0] or False
1261 def _get_related_date(self, cr, uid, ids, context=None):
1263 for history in self.browse(cr, uid, ids, context=context):
1264 cr.execute('''select
1267 project_task_history
1271 order by id desc limit 1''', (history.task_id.id, history.id))
1274 result.append(res[0])
1278 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1279 'type_id': fields.many2one('project.task.type', 'Stage'),
1280 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1281 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1282 'date': fields.date('Date', select=True),
1283 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1284 'project.task.history': (_get_related_date, None, 20)
1286 'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1287 'planned_hours': fields.float('Planned Time', digits=(16,2)),
1288 'user_id': fields.many2one('res.users', 'Responsible'),
1291 'date': fields.date.context_today,
1293 project_task_history()
1295 class project_task_history_cumulative(osv.osv):
1296 _name = 'project.task.history.cumulative'
1297 _table = 'project_task_history_cumulative'
1298 _inherit = 'project.task.history'
1301 'end_date': fields.date('End Date'),
1302 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1305 cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1307 history.date::varchar||'-'||history.history_id::varchar as id,
1308 history.date as end_date,
1313 date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1314 task_id, type_id, user_id, kanban_state, state,
1315 remaining_hours, planned_hours
1317 project_task_history
1321 project_task_history_cumulative()