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, timedelta
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="It's computed by the scheduler according the project date or the end date of the previous phase.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
109 'date_end': fields.date('End Date', help=" It's computed by the scheduler according to the start date and the duration.", 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}
148 def _check_date_start(self, cr, uid, phase, date_end, context=None):
150 Check And Compute date_end of phase if change in date_start < older time.
152 uom_obj = self.pool.get('product.uom')
153 resource_obj = self.pool.get('resource.resource')
154 cal_obj = self.pool.get('resource.calendar')
155 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
156 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
158 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
159 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
162 default_uom_id = self._get_default_uom_id(cr, uid)
163 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
164 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)
165 dt_start = work_times[0][0].strftime('%Y-%m-%d')
166 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
168 def _check_date_end(self, cr, uid, phase, date_start, context=None):
170 Check And Compute date_end of phase if change in date_end > older time.
172 uom_obj = self.pool.get('product.uom')
173 resource_obj = self.pool.get('resource.resource')
174 cal_obj = self.pool.get('resource.calendar')
175 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
176 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
178 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
179 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
182 default_uom_id = self._get_default_uom_id(cr, uid)
183 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
184 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)
185 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
186 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
188 def copy(self, cr, uid, id, default=None, context=None):
191 if not default.get('name', False):
192 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
193 return super(project_phase, self).copy(cr, uid, id, default, context)
195 def set_draft(self, cr, uid, ids, *args):
196 self.write(cr, uid, ids, {'state': 'draft'})
199 def set_open(self, cr, uid, ids, *args):
200 self.write(cr, uid, ids, {'state': 'open'})
203 def set_pending(self, cr, uid, ids, *args):
204 self.write(cr, uid, ids, {'state': 'pending'})
207 def set_cancel(self, cr, uid, ids, *args):
208 self.write(cr, uid, ids, {'state': 'cancelled'})
211 def set_done(self, cr, uid, ids, *args):
212 self.write(cr, uid, ids, {'state': 'done'})
215 def generate_resources(self, cr, uid, ids, context=None):
217 Return a list of Resource Class objects for the resources allocated to the phase.
220 resource_pool = self.pool.get('resource.resource')
221 for phase in self.browse(cr, uid, ids, context=context):
222 user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
224 project = phase.project_id
225 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
226 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
227 res[phase.id] = resource_objs
230 def generate_schedule(self, cr, uid, ids, start_date=False, calendar_id=False, context=None):
232 Schedule phase with the start date till all the next phases are completed.
233 @param: start_date (datetime.datetime) : start date for the phase. It would be either Start date of phase or start date of project or system current date
234 @param: calendar_id : working calendar of the project
238 resource_pool = self.pool.get('resource.resource')
239 data_pool = self.pool.get('ir.model.data')
240 resource_allocation_pool = self.pool.get('project.resource.allocation')
241 uom_pool = self.pool.get('product.uom')
242 data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
243 for phase in self.browse(cr, uid, ids, context=context):
244 if not phase.previous_phase_ids:
246 if not phase.responsible_id:
247 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
249 start_date = phase.project_id.date_start or phase.date_start or datetime.now().strftime("%Y-%m-%d")
250 start_date = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d")
251 phase_resource_obj = self.generate_resources(cr, uid, [phase.id], context=context)[phase.id]
252 avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
253 if not phase_resource_obj: #TOCHECK: why need this ?
254 avg_days = avg_days - 1
255 duration = str(avg_days) + 'd'
256 # Create a new project for each phase
258 # If project has working calendar then that
259 # else the default one would be considered
261 minimum_time_unit = 1
262 resource = phase_resource_obj
263 working_hours_per_day = 24
266 working_hours_per_day = 8 #TODO: it should be come from calendars
267 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
268 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
272 project = Task.BalancedProject(Project)
274 s_date = project.phase.start.to_datetime()
275 e_date = project.phase.end.to_datetime()
276 # Recalculate date_start and date_end
277 # according to constraints on date start and date end on phase
278 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
279 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
283 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
284 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
285 date_start = phase.constraint_date_end
288 date_start = end_date
289 # Write the calculated dates back
291 ctx.update({'scheduler': True})
292 self.write(cr, uid, [phase.id], {
293 'date_start': start_date.strftime('%Y-%m-%d'),
294 'date_end': end_date.strftime('%Y-%m-%d')
296 # write dates into Resources Allocation
297 for resource in phase.resource_ids:
298 resource_allocation_pool.write(cr, uid, [resource.id], {
299 'date_start': start_date.strftime('%Y-%m-%d'),
300 'date_end': end_date.strftime('%Y-%m-%d')
302 # Recursive call till all the next phases scheduled
303 for next_phase in phase.next_phase_ids:
304 if next_phase.state in ['draft', 'open', 'pending']:
305 id_cal = next_phase.project_id.resource_calendar_id and next_phase.project_id.resource_calendar_id.id or False
306 self.generate_schedule(cr, uid, [next_phase.id], date_start+timedelta(days=1), id_cal, context=context)
311 def schedule_tasks(self, cr, uid, ids, context=None):
313 Schedule the tasks according to resource available and priority.
315 task_pool = self.pool.get('project.task')
316 resource_pool = self.pool.get('resource.resource')
317 resources_list = self.generate_resources(cr, uid, ids, context=context)
319 for phase in self.browse(cr, uid, ids, context=context):
320 start_date = phase.date_start
321 if not start_date and phase.project_id.date_start:
322 start_date = phase.project_id.date_start
324 start_date = datetime.now().strftime("%Y-%m-%d")
325 resources = resources_list.get(phase.id, [])
326 calendar_id = phase.project_id.resource_calendar_id.id
327 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['draft'] , phase.task_ids))) #reassign only task not yet started
329 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
332 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
333 if "warning" not in return_msg:
334 return_msg["warning"] = warning_msg
336 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
340 class project_resource_allocation(osv.osv):
341 _name = 'project.resource.allocation'
342 _description = 'Project Resource Allocation'
343 _rec_name = 'resource_id'
345 def get_name(self, cr, uid, ids, field_name, arg, context=None):
347 for allocation in self.browse(cr, uid, ids, context=context):
348 name = allocation.phase_id.name
349 name += ' (%s%%)' %(allocation.useability)
350 res[allocation.id] = name
353 'name': fields.function(get_name, method=True, type='char', size=256),
354 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
355 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
356 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
357 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
358 'date_start': fields.date('Start Date', help="Starting Date"),
359 'date_end': fields.date('End Date', help="Ending Date"),
360 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
366 project_resource_allocation()
368 class project(osv.osv):
369 _inherit = "project.project"
371 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
372 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
374 def generate_members(self, cr, uid, ids, context=None):
376 Return a list of Resource Class objects for the resources allocated to the phase.
379 resource_pool = self.pool.get('resource.resource')
380 for project in self.browse(cr, uid, ids, context=context):
381 user_ids = map(lambda x:x.id, project.members)
382 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
383 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
384 res[project.id] = resource_objs
387 def schedule_phases(self, cr, uid, ids, context=None):
391 if type(ids) in (long, int,):
393 phase_pool = self.pool.get('project.phase')
394 for project in self.browse(cr, uid, ids, context=context):
395 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
396 ('state', 'in', ['draft', 'open', 'pending']),
397 ('previous_phase_ids', '=', False)
399 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
401 phase_pool.generate_schedule(cr, uid, phase_ids, start_date, calendar_id, context=context)
404 def schedule_tasks(self, cr, uid, ids, context=None):
406 Schedule the tasks according to resource available and priority.
408 if type(ids) in (long, int,):
410 user_pool = self.pool.get('res.users')
411 task_pool = self.pool.get('project.task')
412 resource_pool = self.pool.get('resource.resource')
413 resources_list = self.generate_members(cr, uid, ids, context=context)
415 for project in self.browse(cr, uid, ids, context=context):
416 start_date = project.date_start
418 start_date = datetime.now().strftime("%Y-%m-%d")
419 resources = resources_list.get(project.id, [])
420 calendar_id = project.resource_calendar_id.id
421 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
422 ('state', 'in', ['draft', 'open', 'pending'])
427 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
429 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
430 if "warning" not in return_msg:
431 return_msg["warning"] = warning_msg
433 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
439 class resource_resource(osv.osv):
440 _inherit = "resource.resource"
441 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
444 if context.get('project_id',False):
445 project_pool = self.pool.get('project.project')
446 project_rec = project_pool.browse(cr, uid, context['project_id'], context=context)
447 user_ids = [user_id.id for user_id in project_rec.members]
448 args.append(('user_id','in',user_ids))
449 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
453 class project_task(osv.osv):
454 _inherit = "project.task"
456 'phase_id': fields.many2one('project.phase', 'Project Phase'),
459 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
461 Schedule the tasks according to resource available and priority.
463 resource_pool = self.pool.get('resource.resource')
468 user_pool = self.pool.get('res.users')
469 project_pool = self.pool.get('project.project')
470 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
471 # Create dynamic no of tasks with the resource specified
472 def create_tasks(task_number, eff, priorty=500, obj=False):
475 task is a dynamic method!
481 task.__doc__ = "TaskNO%d" %task_number
482 task.__name__ = "task%d" %task_number
485 # Create a 'Faces' project with all the tasks and resources
488 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
490 resource = reduce(operator.or_, resources)
492 raise osv.except_osv(_('Error'), _('Resources should be allocated to your phases and Members should be assigned to your Project!'))
493 minimum_time_unit = 1
494 working_hours_per_day = 24
497 working_hours_per_day = 8 #TODO: it should be come from calendars
498 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
499 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
500 # Dynamic creation of tasks
502 for openobect_task in self.browse(cr, uid, ids, context=context):
503 hours = str(openobect_task.planned_hours )+ 'H'
504 if openobect_task.priority in priority_dict.keys():
505 priorty = priority_dict[openobect_task.priority]
506 real_resource = False
507 if openobect_task.user_id:
508 for task_resource in resources:
509 if task_resource.__name__ == task_resource:
510 real_resource = task_resource
513 task = create_tasks(task_number, hours, priorty, real_resource)
517 face_projects = Task.BalancedProject(Project)
519 # Write back the computed dates
520 for face_project in face_projects:
521 s_date = face_project.start.to_datetime()
522 e_date = face_project.end.to_datetime()
525 ctx.update({'scheduler': True})
526 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
527 self.write(cr, uid, [ids[loop_no-1]], {
528 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
529 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
530 'user_id': user_id[0]
536 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: