1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 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 datetime import datetime
23 from dateutil.relativedelta import relativedelta
24 from tools.translate import _
25 from osv import fields, osv
26 from resource.faces import task as Task
29 class project_phase(osv.osv):
30 _name = "project.phase"
31 _description = "Project Phase"
33 def _check_recursion(self, cr, uid, ids, context=None):
37 data_phase = self.browse(cr, uid, ids[0], context=context)
38 prev_ids = data_phase.previous_phase_ids
39 next_ids = data_phase.next_phase_ids
40 # it should neither be in prev_ids nor in next_ids
41 if (data_phase in prev_ids) or (data_phase in next_ids):
43 ids = [id for id in prev_ids if id in next_ids]
44 # both prev_ids and next_ids must be unique
48 prev_ids = [rec.id for rec in prev_ids]
49 next_ids = [rec.id for rec in next_ids]
52 cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
53 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
54 if data_phase.id in prv_phase_ids:
56 ids = [id for id in prv_phase_ids if id in next_ids]
59 prev_ids = prv_phase_ids
62 cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
63 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
64 if data_phase.id in next_phase_ids:
66 ids = [id for id in next_phase_ids if id in prev_ids]
69 next_ids = next_phase_ids
72 def _check_dates(self, cr, uid, ids, context=None):
73 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
74 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
78 def _check_constraint_start(self, cr, uid, ids, context=None):
79 phase = self.read(cr, uid, ids[0], ['date_start', 'constraint_date_start'], context=context)
80 if phase['date_start'] and phase['constraint_date_start'] and phase['date_start'] < phase['constraint_date_start']:
84 def _check_constraint_end(self, cr, uid, ids, context=None):
85 phase = self.read(cr, uid, ids[0], ['date_end', 'constraint_date_end'], context=context)
86 if phase['date_end'] and phase['constraint_date_end'] and phase['date_end'] > phase['constraint_date_end']:
90 def _get_default_uom_id(self, cr, uid):
91 model_data_obj = self.pool.get('ir.model.data')
92 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
93 return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
95 def _compute(self, cr, uid, ids, field_name, arg, context=None):
99 for phase in self.browse(cr, uid, ids, context=context):
101 for task in phase.task_ids:
102 tot += task.planned_hours
107 'name': fields.char("Name", size=64, required=True),
108 'date_start': fields.date('Start Date', help="Starting Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
109 'date_end': fields.date('End Date', help="Ending Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
110 'constraint_date_start': fields.date('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
111 'constraint_date_end': fields.date('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
112 'project_id': fields.many2one('project.project', 'Project', required=True),
113 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
114 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
115 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
116 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
117 'product_uom': fields.many2one('product.uom', 'Duration UoM', required=True, help="UoM (Unit of Measure) is the unit of measurement for Duration", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
118 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
119 'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
120 'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
121 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
122 help='If the phase is created the state \'Draft\'.\n If the phase is started, the state becomes \'In Progress\'.\n If review is needed the phase is in \'Pending\' state.\
123 \n If the phase is over, the states is set to \'Done\'.'),
124 'total_hours': fields.function(_compute, method=True, string='Total Hours'),
127 'responsible_id': lambda obj,cr,uid,context: uid,
130 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
132 _order = "project_id, date_start, sequence, name"
134 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
135 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
138 def onchange_project(self, cr, uid, ids, project, context=None):
140 result['date_start'] = False
141 project_obj = self.pool.get('project.project')
143 project_id = project_obj.browse(cr, uid, project, context=context)
144 result['date_start'] = project_id.date_start
145 return {'value': result}
147 def onchange_days(self, cr, uid, ids, project, context=None):
150 project_id = self.browse(cr, uid, id, context=context)
151 newdate = datetime.strptime(project_id.date_start, '%Y-%m-%d') + relativedelta(days=project_id.duration or 0.0)
152 result['date_end'] = newdate.strftime('%Y-%m-%d')
153 return {'value': result}
155 def _check_date_start(self, cr, uid, phase, date_end, context=None):
159 Check And Compute date_end of phase if change in date_start < older time.
161 uom_obj = self.pool.get('product.uom')
162 resource_obj = self.pool.get('resource.resource')
163 cal_obj = self.pool.get('resource.calendar')
164 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
165 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
167 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
168 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
171 default_uom_id = self._get_default_uom_id(cr, uid)
172 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
173 work_times = cal_obj.interval_min_get(cr, uid, calendar_id, date_end, avg_hours or 0.0, resource_id and resource_id[0] or False)
174 dt_start = work_times[0][0].strftime('%Y-%m-%d')
175 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
177 def _check_date_end(self, cr, uid, phase, date_start, context=None):
181 Check And Compute date_end of phase if change in date_end > older time.
183 uom_obj = self.pool.get('product.uom')
184 resource_obj = self.pool.get('resource.resource')
185 cal_obj = self.pool.get('resource.calendar')
186 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
187 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
189 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
190 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
193 default_uom_id = self._get_default_uom_id(cr, uid)
194 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
195 work_times = cal_obj.interval_get(cr, uid, calendar_id, date_start, avg_hours or 0.0, resource_id and resource_id[0] or False)
196 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
197 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
199 def write(self, cr, uid, ids, vals, context=None):
200 resource_calendar_obj = self.pool.get('resource.calendar')
201 resource_obj = self.pool.get('resource.resource')
202 uom_obj = self.pool.get('product.uom')
205 res = super(project_phase, self).write(cr, uid, ids, vals, context=context)
206 if context.get('scheduler',False):
208 # Consider calendar and efficiency if the phase is performed by a resource
209 # otherwise consider the project's working calendar
211 #TOCHECK : why need this ?
212 if isinstance(ids, (int, long)):
214 default_uom_id = self._get_default_uom_id(cr, uid)
215 for phase in self.browse(cr, uid, ids, context=context):
216 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
217 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
219 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
223 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
224 # Change the date_start and date_end
225 # for previous and next phases respectively based on valid condition
226 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
227 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
228 work_times = resource_calendar_obj.interval_get(cr, uid, calendar_id, dt_start, avg_hours or 0.0, resource_id and resource_id[0] or False)
230 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
231 for prv_phase in phase.previous_phase_ids:
232 if prv_phase.id == phase.id:
234 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
236 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
237 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
238 work_times = resource_calendar_obj.interval_min_get(cr, uid, calendar_id, dt_end, avg_hours or 0.0, resource_id and resource_id[0] or False)
240 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
241 for next_phase in phase.next_phase_ids:
242 if next_phase.id == phase.id:
244 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
248 def copy(self, cr, uid, id, default=None, context=None):
251 if not default.get('name', False):
252 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
253 return super(project_phase, self).copy(cr, uid, id, default, context)
255 def set_draft(self, cr, uid, ids, *args):
256 self.write(cr, uid, ids, {'state': 'draft'})
259 def set_open(self, cr, uid, ids, *args):
260 self.write(cr, uid, ids, {'state': 'open'})
263 def set_pending(self, cr, uid, ids, *args):
264 self.write(cr, uid, ids, {'state': 'pending'})
267 def set_cancel(self, cr, uid, ids, *args):
268 self.write(cr, uid, ids, {'state': 'cancelled'})
271 def set_done(self, cr, uid, ids, *args):
272 self.write(cr, uid, ids, {'state': 'done'})
275 def generate_resources(self, cr, uid, ids, context=None):
277 Return a list of Resource Class objects for the resources allocated to the phase.
282 resource_pool = self.pool.get('resource.resource')
283 for phase in self.browse(cr, uid, ids, context=context):
284 user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
285 project = phase.project_id
286 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
287 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
288 res[phase.id] = resource_objs
291 def generate_schedule(self, cr, uid, ids, start_date, calendar_id=False, context=None):
293 Schedule phase with the start date till all the next phases are completed.
294 @param: start_dsate : start date for the phase
295 @param: calendar_id : working calendar of the project
299 resource_pool = self.pool.get('resource.resource')
300 resource_allocation_pool = self.pool.get('project.resource.allocation')
301 uom_pool = self.pool.get('product.uom')
304 default_uom_id = self._get_default_uom_id(cr, uid)
305 for phase in self.browse(cr, uid, ids, context=context):
306 if not phase.responsible_id:
307 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
309 phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
310 avg_hours = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
311 duration = str(avg_hours) + 'H'
312 # Create a new project for each phase
314 # If project has working calendar then that
315 # else the default one would be considered
317 minimum_time_unit = 1
318 resource = phase_resource_obj
320 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
321 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
326 project = Task.BalancedProject(Project)
327 s_date = project.phase.start.to_datetime()
328 e_date = project.phase.end.to_datetime()
329 # Recalculate date_start and date_end
330 # according to constraints on date start and date end on phase
331 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
332 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
335 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
336 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
337 date_start = phase.constraint_date_end
340 date_start = end_date
341 # Write the calculated dates back
343 ctx.update({'scheduler': True})
344 self.write(cr, uid, [phase.id], {
345 'date_start': start_date.strftime('%Y-%m-%d'),
346 'date_end': end_date.strftime('%Y-%m-%d')
348 # write dates into Resources Allocation
349 for resource in phase.resource_ids:
350 resource_allocation_pool.write(cr, uid, [resource.id], {
351 'date_start': start_date.strftime('%Y-%m-%d'),
352 'date_end': end_date.strftime('%Y-%m-%d')
354 # Recursive call till all the next phases scheduled
355 for phase in phase.next_phase_ids:
356 if phase.state in ['draft', 'open', 'pending']:
357 id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
358 self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
363 def schedule_tasks(self, cr, uid, ids, context=None):
365 Schedule the tasks according to resource available and priority.
367 task_pool = self.pool.get('project.task')
368 resource_pool = self.pool.get('resource.resource')
371 resources_list = self.generate_resources(cr, uid, ids, context=context)
373 for phase in self.browse(cr, uid, ids, context=context):
374 start_date = phase.date_start
375 if not start_date and phase.project_id.date_start:
376 start_date = phase.project_id.date_start
378 start_date = datetime.now().strftime("%Y-%m-%d")
379 resources = resources_list.get(phase.id, [])
380 calendar_id = phase.project_id.resource_calendar_id.id
381 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
383 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
386 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
387 if "warning" not in return_msg:
388 return_msg["warning"] = warning_msg
390 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
394 class project_resource_allocation(osv.osv):
395 _name = 'project.resource.allocation'
396 _description = 'Project Resource Allocation'
397 _rec_name = 'resource_id'
399 def get_name(self, cr, uid, ids, field_name, arg, context=None):
401 for allocation in self.browse(cr, uid, ids, context=context):
402 name = allocation.resource_id.name
403 if allocation.user_id:
404 name = '%s' %(allocation.user_id.name)
405 name += ' (%s%%)' %(allocation.useability)
406 res[allocation.id] = name
409 'name': fields.function(get_name, method=True, type='char', size=256),
410 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
411 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
412 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
413 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
414 'date_start': fields.date('Start Date', help="Starting Date"),
415 'date_end': fields.date('End Date', help="Ending Date"),
416 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
422 project_resource_allocation()
424 class project(osv.osv):
425 _inherit = "project.project"
427 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
428 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
430 def generate_members(self, cr, uid, ids, context=None):
432 Return a list of Resource Class objects for the resources allocated to the phase.
435 resource_pool = self.pool.get('resource.resource')
438 for project in self.browse(cr, uid, ids, context=context):
439 user_ids = map(lambda x:x.id, project.members)
440 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
441 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
442 res[project.id] = resource_objs
445 def schedule_phases(self, cr, uid, ids, context=None):
451 if type(ids) in (long, int,):
453 phase_pool = self.pool.get('project.phase')
454 for project in self.browse(cr, uid, ids, context=context):
455 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
456 ('state', 'in', ['draft', 'open', 'pending']),
457 ('previous_phase_ids', '=', False)
459 start_date = project.date_start
461 start_date = datetime.now().strftime("%Y-%m-%d")
462 start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M")
463 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
464 phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context)
467 def schedule_tasks(self, cr, uid, ids, context=None):
469 Schedule the tasks according to resource available and priority.
471 if type(ids) in (long, int,):
473 user_pool = self.pool.get('res.users')
474 task_pool = self.pool.get('project.task')
475 resource_pool = self.pool.get('resource.resource')
479 resources_list = self.generate_members(cr, uid, ids, context=context)
481 for project in self.browse(cr, uid, ids, context=context):
482 start_date = project.date_start
484 start_date = datetime.now().strftime("%Y-%m-%d")
485 resources = resources_list.get(project.id, [])
486 calendar_id = project.resource_calendar_id.id
487 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
488 ('state', 'in', ['draft', 'open', 'pending'])
493 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
495 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
496 if "warning" not in return_msg:
497 return_msg["warning"] = warning_msg
499 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
505 class resource_resource(osv.osv):
506 _inherit = "resource.resource"
507 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
510 if context.get('project_id',False):
511 project_pool = self.pool.get('project.project')
512 project_rec = project_pool.browse(cr, uid, context['project_id'])
513 user_ids = [user_id.id for user_id in project_rec.members]
514 args.append(('user_id','in',user_ids))
515 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
519 class project_task(osv.osv):
520 _inherit = "project.task"
522 'phase_id': fields.many2one('project.phase', 'Project Phase'),
525 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
527 Schedule the tasks according to resource available and priority.
533 user_pool = self.pool.get('res.users')
534 project_pool = self.pool.get('project.project')
535 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
536 # Create dynamic no of tasks with the resource specified
537 def create_tasks(task_number, eff, priorty=500, obj=False):
540 task is a dynamic method!
546 task.__doc__ = "TaskNO%d" %task_number
547 task.__name__ = "task%d" %task_number
550 # Create a 'Faces' project with all the tasks and resources
553 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
555 resource = reduce(operator.or_, resources)
557 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
558 minimum_time_unit = 1
559 if calendar_id: # If project has working calendar
560 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
561 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
562 # Dynamic creation of tasks
564 for openobect_task in self.browse(cr, uid, ids, context=context):
565 hours = str(openobect_task.planned_hours )+ 'H'
566 if openobect_task.priority in priority_dict.keys():
567 priorty = priority_dict[openobect_task.priority]
568 real_resource = False
569 if openobect_task.user_id:
570 for task_resource in resources:
571 if task_resource.__name__ == task_resource:
572 real_resource = task_resource
575 task = create_tasks(task_number, hours, priorty, real_resource)
579 face_projects = Task.BalancedProject(Project)
581 # Write back the computed dates
582 for face_project in face_projects:
583 s_date = face_project.start.to_datetime()
584 e_date = face_project.end.to_datetime()
587 ctx.update({'scheduler': True})
588 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
589 self.write(cr, uid, [ids[loop_no-1]], {
590 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
591 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
592 'user_id': user_id[0]
598 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: