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 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')
78 return {'value':{'contact_id': False}}
79 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
80 val = {'contact_id': addr['contact']}
81 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84 val['pricelist_id'] = pricelist_id
87 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
88 res = {}.fromkeys(ids, 0.0)
92 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
99 project_id''', (tuple(ids),))
100 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
101 for project in self.browse(cr, uid, ids, context=context):
102 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
104 'planned_hours': s[0],
105 'effective_hours': s[2],
107 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
111 def _get_project_task(self, cr, uid, ids, context=None):
113 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
114 if task.project_id: result[task.project_id.id] = True
117 def _get_project_work(self, cr, uid, ids, context=None):
119 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
120 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
123 def unlink(self, cr, uid, ids, *args, **kwargs):
124 for proj in self.browse(cr, uid, ids):
126 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
127 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
130 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
131 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
132 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
133 '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),
134 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
135 'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
137 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
138 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)]}),
139 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
140 '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.",
142 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
143 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
145 '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."),
146 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
147 '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.",
149 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
150 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
152 '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."),
153 '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)]}),
154 '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)]}),
155 '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)]}),
156 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
158 def _get_type_common(self, cr, uid, context):
159 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
167 'type_ids': _get_type_common
170 # TODO: Why not using a SQL contraints ?
171 def _check_dates(self, cr, uid, ids, context=None):
172 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
173 if leave['date_start'] and leave['date']:
174 if leave['date_start'] > leave['date']:
179 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
182 def set_template(self, cr, uid, ids, context=None):
183 res = self.setActive(cr, uid, ids, value=False, context=context)
186 def set_done(self, cr, uid, ids, context=None):
187 task_obj = self.pool.get('project.task')
188 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
189 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190 self.write(cr, uid, ids, {'state':'close'}, context=context)
191 for (id, name) in self.name_get(cr, uid, ids):
192 message = _("The project '%s' has been closed.") % name
193 self.log(cr, uid, id, message)
196 def set_cancel(self, cr, uid, ids, context=None):
197 task_obj = self.pool.get('project.task')
198 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
199 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
200 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
203 def set_pending(self, cr, uid, ids, context=None):
204 self.write(cr, uid, ids, {'state':'pending'}, context=context)
207 def set_open(self, cr, uid, ids, context=None):
208 self.write(cr, uid, ids, {'state':'open'}, context=context)
211 def reset_project(self, cr, uid, ids, context=None):
212 res = self.setActive(cr, uid, ids, value=True, context=context)
213 for (id, name) in self.name_get(cr, uid, ids):
214 message = _("The project '%s' has been opened.") % name
215 self.log(cr, uid, id, message)
218 def copy(self, cr, uid, id, default={}, context=None):
222 default = default or {}
223 context['active_test'] = False
224 default['state'] = 'open'
225 proj = self.browse(cr, uid, id, context=context)
226 if not default.get('name', False):
227 default['name'] = proj.name + _(' (copy)')
229 res = super(project, self).copy(cr, uid, id, default, context)
233 def template_copy(self, cr, uid, id, default={}, context=None):
234 task_obj = self.pool.get('project.task')
235 proj = self.browse(cr, uid, id, context=context)
237 default['tasks'] = [] #avoid to copy all the task automaticly
238 res = self.copy(cr, uid, id, default=default, context=context)
240 #copy all the task manually
242 for task in proj.tasks:
243 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
245 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
246 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
250 def duplicate_template(self, cr, uid, ids, context=None):
253 data_obj = self.pool.get('ir.model.data')
255 for proj in self.browse(cr, uid, ids, context=context):
256 parent_id = context.get('parent_id', False)
257 context.update({'analytic_project_copy': True})
258 new_date_start = time.strftime('%Y-%m-%d')
260 if proj.date_start and proj.date:
261 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
262 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
263 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
264 context.update({'copy':True})
265 new_id = self.template_copy(cr, uid, proj.id, default = {
266 'name': proj.name +_(' (copy)'),
268 'date_start':new_date_start,
270 'parent_id':parent_id}, context=context)
271 result.append(new_id)
273 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
274 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
276 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
278 if result and len(result):
280 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
281 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
282 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
283 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
284 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
285 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
287 'name': _('Projects'),
289 'view_mode': 'form,tree',
290 'res_model': 'project.project',
293 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
294 'type': 'ir.actions.act_window',
295 'search_view_id': search_view['res_id'],
299 # set active value for a project, its sub projects and its tasks
300 def setActive(self, cr, uid, ids, value=True, context=None):
301 task_obj = self.pool.get('project.task')
302 for proj in self.browse(cr, uid, ids, context=None):
303 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
304 cr.execute('select id from project_task where project_id=%s', (proj.id,))
305 tasks_id = [x[0] for x in cr.fetchall()]
307 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
308 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
310 self.setActive(cr, uid, child_ids, value, context=None)
313 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
314 context = context or {}
315 if type(ids) in (long, int,):
317 projects = self.browse(cr, uid, ids, context=context)
319 for project in projects:
320 if (not project.members) and force_members:
321 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
323 resource_pool = self.pool.get('resource.resource')
325 result = "from resource.faces import *\n"
326 result += "import datetime\n"
327 for project in self.browse(cr, uid, ids, context=context):
328 u_ids = [i.id for i in project.members]
329 if project.user_id and (project.user_id.id not in u_ids):
330 u_ids.append(project.user_id.id)
331 for task in project.tasks:
332 if task.state in ('done','cancelled'):
334 if task.user_id and (task.user_id.id not in u_ids):
335 u_ids.append(task.user_id.id)
336 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
337 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
338 for key, vals in resource_objs.items():
340 class User_%s(Resource):
342 ''' % (key, vals.get('efficiency', False))
349 def _schedule_project(self, cr, uid, project, context=None):
350 resource_pool = self.pool.get('resource.resource')
351 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
352 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
353 # TODO: check if we need working_..., default values are ok.
354 puids = [x.id for x in project.members]
356 puids.append(project.user_id.id)
364 project.date_start, working_days,
365 '|'.join(['User_'+str(x) for x in puids])
367 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
374 #TODO: DO Resource allocation and compute availability
375 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
381 def schedule_tasks(self, cr, uid, ids, context=None):
382 context = context or {}
383 if type(ids) in (long, int,):
385 projects = self.browse(cr, uid, ids, context=context)
386 result = self._schedule_header(cr, uid, ids, False, context=context)
387 for project in projects:
388 result += self._schedule_project(cr, uid, project, context=context)
389 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
392 exec result in local_dict
393 projects_gantt = Task.BalancedProject(local_dict['Project'])
395 for project in projects:
396 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
397 for task in project.tasks:
398 if task.state in ('done','cancelled'):
401 p = getattr(project_gantt, 'Task_%d' % (task.id,))
403 self.pool.get('project.task').write(cr, uid, [task.id], {
404 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
405 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
407 if (not task.user_id) and (p.booked_resource):
408 self.pool.get('project.task').write(cr, uid, [task.id], {
409 'user_id': int(p.booked_resource[0].name[5:]),
414 class users(osv.osv):
415 _inherit = 'res.users'
417 'context_project_id': fields.many2one('project.project', 'Project')
422 _name = "project.task"
423 _description = "Task"
425 _date_name = "date_start"
427 def _read_group_type_id(self, cr, uid, ids, domain, context=None):
428 context = context or {}
429 stage_obj = self.pool.get('project.task.type')
430 stage_ids = stage_obj.search(cr, uid, ['|',('id','in',ids)] + [('project_default','=',1)], context=context)
431 return stage_obj.name_get(cr, uid, stage_ids, context=context)
433 def _read_group_user_id(self, cr, uid, ids, domain, context={}):
434 context = context or {}
435 if type(context.get('project_id', None)) not in (int, long):
437 proj = self.pool.get('project.project').browse(cr, uid, context['project_id'], context=context)
438 ids += map(lambda x: x.id, proj.members)
439 stage_obj = self.pool.get('res.users')
440 stage_ids = stage_obj.search(cr, uid, [('id','in',ids)], context=context)
441 return stage_obj.name_get(cr, uid, ids, context=context)
444 'type_id': _read_group_type_id,
445 'user_id': _read_group_user_id
449 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
450 obj_project = self.pool.get('project.project')
452 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
453 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
454 if id and isinstance(id, (long, int)):
455 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
456 args.append(('active', '=', False))
457 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
459 def _str_get(self, task, level=0, border='***', context=None):
460 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'+ \
461 border[0]+' '+(task.name or '')+'\n'+ \
462 (task.description or '')+'\n\n'
464 # Compute: effective_hours, total_hours, progress
465 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
467 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
468 hours = dict(cr.fetchall())
469 for task in self.browse(cr, uid, ids, context=context):
470 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)}
471 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
472 res[task.id]['progress'] = 0.0
473 if (task.remaining_hours + hours.get(task.id, 0.0)):
474 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
475 if task.state in ('done','cancelled'):
476 res[task.id]['progress'] = 100.0
480 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
481 if remaining and not planned:
482 return {'value':{'planned_hours': remaining}}
485 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
486 return {'value':{'remaining_hours': planned - effective}}
488 def onchange_project(self, cr, uid, id, project_id):
491 data = self.pool.get('project.project').browse(cr, uid, [project_id])
492 partner_id=data and data[0].parent_id.partner_id
494 return {'value':{'partner_id':partner_id.id}}
497 def _default_project(self, cr, uid, context=None):
500 if 'project_id' in context and context['project_id']:
501 return int(context['project_id'])
504 def duplicate_task(self, cr, uid, map_ids, context=None):
505 for new in map_ids.values():
506 task = self.browse(cr, uid, new, context)
507 child_ids = [ ch.id for ch in task.child_ids]
509 for child in task.child_ids:
510 if child.id in map_ids.keys():
511 child_ids.remove(child.id)
512 child_ids.append(map_ids[child.id])
514 parent_ids = [ ch.id for ch in task.parent_ids]
516 for parent in task.parent_ids:
517 if parent.id in map_ids.keys():
518 parent_ids.remove(parent.id)
519 parent_ids.append(map_ids[parent.id])
520 #FIXME why there is already the copy and the old one
521 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
523 def copy_data(self, cr, uid, id, default={}, context=None):
524 default = default or {}
525 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
526 if not default.get('remaining_hours', False):
527 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
528 default['active'] = True
529 default['type_id'] = False
530 if not default.get('name', False):
531 default['name'] = self.browse(cr, uid, id, context=context).name or ''
532 if not context.get('copy',False):
533 new_name = _("%s (copy)")%default.get('name','')
534 default.update({'name':new_name})
535 return super(task, self).copy_data(cr, uid, id, default, context)
538 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
540 for task in self.browse(cr, uid, ids, context=context):
543 if task.project_id.active == False or task.project_id.state == 'template':
547 def _get_task(self, cr, uid, ids, context=None):
549 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
550 if work.task_id: result[work.task_id.id] = True
554 '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."),
555 'name': fields.char('Task Summary', size=128, required=True),
556 'description': fields.text('Description'),
557 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
558 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
559 'type_id': fields.many2one('project.task.type', 'Stage'),
560 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
561 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.\
562 \n If the task is over, the states is set to \'Done\'.'),
563 'kanban_state': fields.selection([('blocked', 'Blocked'),('normal', 'Normal'),('done', 'Done')], 'Kanban State', readonly=True, required=False),
564 'create_date': fields.datetime('Create Date', readonly=True,select=True),
565 'date_start': fields.datetime('Starting Date',select=True),
566 'date_end': fields.datetime('Ending Date',select=True),
567 'date_deadline': fields.date('Deadline',select=True),
568 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
569 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
570 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
571 'notes': fields.text('Notes'),
572 '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.'),
573 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
575 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
576 'project.task.work': (_get_task, ['hours'], 10),
578 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
579 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
581 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
582 'project.task.work': (_get_task, ['hours'], 10),
584 '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",
586 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
587 'project.task.work': (_get_task, ['hours'], 10),
589 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
591 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
592 'project.task.work': (_get_task, ['hours'], 10),
594 'user_id': fields.many2one('res.users', 'Assigned to'),
595 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
596 'partner_id': fields.many2one('res.partner', 'Partner'),
597 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
598 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
599 'company_id': fields.many2one('res.company', 'Company'),
600 'id': fields.integer('ID', readonly=True),
601 'color': fields.integer('Color Index'),
602 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
607 'kanban_state': 'normal',
612 'project_id': _default_project,
613 'user_id': lambda obj, cr, uid, context: uid,
614 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
617 _order = "priority, sequence, date_start, name, id"
619 def set_priority(self, cr, uid, ids, priority):
622 return self.write(cr, uid, ids, {'priority' : priority})
624 def set_high_priority(self, cr, uid, ids, *args):
625 """Set task priority to high
627 return self.set_priority(cr, uid, ids, '1')
629 def set_normal_priority(self, cr, uid, ids, *args):
630 """Set task priority to normal
632 return self.set_priority(cr, uid, ids, '3')
634 def _check_recursion(self, cr, uid, ids, context=None):
636 visited_branch = set()
638 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
644 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
645 if id in visited_branch: #Cycle
648 if id in visited_node: #Already tested don't work one more time for nothing
651 visited_branch.add(id)
654 #visit child using DFS
655 task = self.browse(cr, uid, id, context=context)
656 for child in task.child_ids:
657 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
661 visited_branch.remove(id)
664 def _check_dates(self, cr, uid, ids, context=None):
667 obj_task = self.browse(cr, uid, ids[0], context=context)
668 start = obj_task.date_start or False
669 end = obj_task.date_end or False
676 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
677 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
680 # Override view according to the company definition
682 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
683 users_obj = self.pool.get('res.users')
685 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
686 # this should be safe (no context passed to avoid side-effects)
687 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
688 tm = obj_tm and obj_tm.name or 'Hours'
690 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
692 if tm in ['Hours','Hour']:
695 eview = etree.fromstring(res['arch'])
697 def _check_rec(eview):
698 if eview.attrib.get('widget','') == 'float_time':
699 eview.set('widget','float')
706 res['arch'] = etree.tostring(eview)
708 for f in res['fields']:
709 if 'Hours' in res['fields'][f]['string']:
710 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
713 def _check_child_task(self, cr, uid, ids, context=None):
716 tasks = self.browse(cr, uid, ids, context=context)
719 for child in task.child_ids:
720 if child.state in ['draft', 'open', 'pending']:
721 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
724 def action_close(self, cr, uid, ids, context=None):
725 # This action open wizard to send email to partner or project manager after close task.
728 task_id = len(ids) and ids[0] or False
729 self._check_child_task(cr, uid, ids, context=context)
730 if not task_id: return False
731 task = self.browse(cr, uid, task_id, context=context)
732 project = task.project_id
733 res = self.do_close(cr, uid, [task_id], context=context)
734 if project.warn_manager or project.warn_customer:
736 'name': _('Send Email after close task'),
739 'res_model': 'mail.compose.message',
740 'type': 'ir.actions.act_window',
743 'context': {'active_id': task.id,
744 'active_model': 'project.task'}
748 def do_close(self, cr, uid, ids, context={}):
752 request = self.pool.get('res.request')
753 if not isinstance(ids,list): ids = [ids]
754 for task in self.browse(cr, uid, ids, context=context):
756 project = task.project_id
758 # Send request to project manager
759 if project.warn_manager and project.user_id and (project.user_id.id != uid):
760 request.create(cr, uid, {
761 'name': _("Task '%s' closed") % task.name,
764 'act_to': project.user_id.id,
765 'ref_partner_id': task.partner_id.id,
766 'ref_doc1': 'project.task,%d'% (task.id,),
767 'ref_doc2': 'project.project,%d'% (project.id,),
770 for parent_id in task.parent_ids:
771 if parent_id.state in ('pending','draft'):
773 for child in parent_id.child_ids:
774 if child.id != task.id and child.state not in ('done','cancelled'):
777 self.do_reopen(cr, uid, [parent_id.id], context=context)
778 vals.update({'state': 'done'})
779 vals.update({'remaining_hours': 0.0})
780 if not task.date_end:
781 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
782 self.write(cr, uid, [task.id],vals, context=context)
783 message = _("The task '%s' is done") % (task.name,)
784 self.log(cr, uid, task.id, message)
787 def do_reopen(self, cr, uid, ids, context=None):
788 request = self.pool.get('res.request')
790 for task in self.browse(cr, uid, ids, context=context):
791 project = task.project_id
792 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
793 request.create(cr, uid, {
794 'name': _("Task '%s' set in progress") % task.name,
797 'act_to': project.user_id.id,
798 'ref_partner_id': task.partner_id.id,
799 'ref_doc1': 'project.task,%d' % task.id,
800 'ref_doc2': 'project.project,%d' % project.id,
803 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
806 def do_cancel(self, cr, uid, ids, context={}):
807 request = self.pool.get('res.request')
808 tasks = self.browse(cr, uid, ids, context=context)
809 self._check_child_task(cr, uid, ids, context=context)
811 project = task.project_id
812 if project.warn_manager and project.user_id and (project.user_id.id != uid):
813 request.create(cr, uid, {
814 'name': _("Task '%s' cancelled") % task.name,
817 'act_to': project.user_id.id,
818 'ref_partner_id': task.partner_id.id,
819 'ref_doc1': 'project.task,%d' % task.id,
820 'ref_doc2': 'project.project,%d' % project.id,
822 message = _("The task '%s' is cancelled.") % (task.name,)
823 self.log(cr, uid, task.id, message)
824 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
827 def do_open(self, cr, uid, ids, context={}):
828 if not isinstance(ids,list): ids = [ids]
829 tasks= self.browse(cr, uid, ids, context=context)
831 data = {'state': 'open'}
833 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
834 self.write(cr, uid, [t.id], data, context=context)
835 message = _("The task '%s' is opened.") % (t.name,)
836 self.log(cr, uid, t.id, message)
839 def do_draft(self, cr, uid, ids, context={}):
840 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
843 def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
845 Delegate Task to another users.
847 assert delegate_data['user_id'], _("Delegated User should be specified")
848 delegrated_tasks = {}
849 for task in self.browse(cr, uid, ids, context=context):
850 delegrated_task_id = self.copy(cr, uid, task.id, {
851 'name': delegate_data['name'],
852 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
853 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
854 'planned_hours': delegate_data['planned_hours'] or 0.0,
855 'parent_ids': [(6, 0, [task.id])],
857 'description': delegate_data['new_task_description'] or '',
861 newname = delegate_data['prefix'] or ''
863 'remaining_hours': delegate_data['planned_hours_me'],
864 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
867 if delegate_data['state'] == 'pending':
868 self.do_pending(cr, uid, task.id, context=context)
869 elif delegate_data['state'] == 'done':
870 self.do_close(cr, uid, task.id, context=context)
872 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
873 self.log(cr, uid, task.id, message)
874 delegrated_tasks[task.id] = delegrated_task_id
875 return delegrated_tasks
877 def do_pending(self, cr, uid, ids, context={}):
878 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
879 for (id, name) in self.name_get(cr, uid, ids):
880 message = _("The task '%s' is pending.") % name
881 self.log(cr, uid, id, message)
884 def set_remaining_time_1(self, cr, uid, ids, context=None):
885 self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
888 def set_remaining_time_2(self, cr, uid, ids, context=None):
889 self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
892 def set_remaining_time_5(self, cr, uid, ids, context=None):
893 self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
896 def set_remaining_time_10(self, cr, uid, ids, context=None):
897 self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
900 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
901 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
903 def set_kanban_state_normal(self, cr, uid, ids, context=None):
904 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
906 def set_kanban_state_done(self, cr, uid, ids, context=None):
907 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
909 def _change_type(self, cr, uid, ids, next, *args):
912 if next is False, go to previous stage
914 for task in self.browse(cr, uid, ids):
915 if task.project_id.type_ids:
916 typeid = task.type_id.id
918 for type in task.project_id.type_ids :
919 types_seq[type.id] = type.sequence
921 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
923 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
924 sorted_types = [x[0] for x in types]
926 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
927 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
928 index = sorted_types.index(typeid)
929 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
932 def next_type(self, cr, uid, ids, *args):
933 return self._change_type(cr, uid, ids, True, *args)
935 def prev_type(self, cr, uid, ids, *args):
936 return self._change_type(cr, uid, ids, False, *args)
938 def unlink(self, cr, uid, ids, context=None):
941 self._check_child_task(cr, uid, ids, context=context)
942 res = super(task, self).unlink(cr, uid, ids, context)
945 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
946 context = context or {}
950 if task.state in ('done','cancelled'):
955 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
957 for t2 in task.parent_ids:
958 start.append("up.Task_%s.end" % (t2.id,))
962 ''' % (ident,','.join(start))
967 ''' % (ident, 'User_'+str(task.user_id.id))
974 class project_work(osv.osv):
975 _name = "project.task.work"
976 _description = "Project Task Work"
978 'name': fields.char('Work summary', size=128),
979 'date': fields.datetime('Date', select="1"),
980 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
981 'hours': fields.float('Time Spent'),
982 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
983 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
987 'user_id': lambda obj, cr, uid, context: uid,
988 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
992 def create(self, cr, uid, vals, *args, **kwargs):
993 if 'hours' in vals and (not vals['hours']):
995 if 'task_id' in vals:
996 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
997 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
999 def write(self, cr, uid, ids, vals, context=None):
1000 if 'hours' in vals and (not vals['hours']):
1001 vals['hours'] = 0.00
1003 for work in self.browse(cr, uid, ids, context=context):
1004 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))
1005 return super(project_work,self).write(cr, uid, ids, vals, context)
1007 def unlink(self, cr, uid, ids, *args, **kwargs):
1008 for work in self.browse(cr, uid, ids):
1009 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1010 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1013 class account_analytic_account(osv.osv):
1015 _inherit = 'account.analytic.account'
1016 _description = 'Analytic Account'
1018 def create(self, cr, uid, vals, context=None):
1021 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1022 vals['child_ids'] = []
1023 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1025 def unlink(self, cr, uid, ids, *args, **kwargs):
1026 project_obj = self.pool.get('project.project')
1027 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1029 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1030 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1032 account_analytic_account()
1034 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: