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
28 from new import classobj
32 class project_phase(osv.osv):
33 _name = "project.phase"
34 _description = "Project Phase"
36 def _check_recursion(self, cr, uid, ids, context=None):
40 data_phase = self.browse(cr, uid, ids[0], context=context)
41 prev_ids = data_phase.previous_phase_ids
42 next_ids = data_phase.next_phase_ids
43 # it should neither be in prev_ids nor in next_ids
44 if (data_phase in prev_ids) or (data_phase in next_ids):
46 ids = [id for id in prev_ids if id in next_ids]
47 # both prev_ids and next_ids must be unique
51 prev_ids = [rec.id for rec in prev_ids]
52 next_ids = [rec.id for rec in next_ids]
55 cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
56 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
57 if data_phase.id in prv_phase_ids:
59 ids = [id for id in prv_phase_ids if id in next_ids]
62 prev_ids = prv_phase_ids
65 cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
66 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
67 if data_phase.id in next_phase_ids:
69 ids = [id for id in next_phase_ids if id in prev_ids]
72 next_ids = next_phase_ids
75 def _check_dates(self, cr, uid, ids, context=None):
76 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
77 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
81 def _check_constraint_start(self, cr, uid, ids, context=None):
82 phase = self.read(cr, uid, ids[0], ['date_start', 'constraint_date_start'], context=context)
83 if phase['date_start'] and phase['constraint_date_start'] and phase['date_start'] < phase['constraint_date_start']:
87 def _check_constraint_end(self, cr, uid, ids, context=None):
88 phase = self.read(cr, uid, ids[0], ['date_end', 'constraint_date_end'], context=context)
89 if phase['date_end'] and phase['constraint_date_end'] and phase['date_end'] > phase['constraint_date_end']:
93 def _get_default_uom_id(self, cr, uid):
94 model_data_obj = self.pool.get('ir.model.data')
95 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
96 return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
98 def _compute(self, cr, uid, ids, field_name, arg, context=None):
102 for phase in self.browse(cr, uid, ids, context=context):
104 for task in phase.task_ids:
105 tot += task.planned_hours
110 'name': fields.char("Name", size=64, required=True),
111 'date_start': fields.date('Start Date', help="It's computed according to the phases order : the start date of the 1st phase is set by you while the other start dates depend on the end date of their previous phases", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
112 '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)]}),
113 'constraint_date_start': fields.date('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
114 'constraint_date_end': fields.date('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
115 'project_id': fields.many2one('project.project', 'Project', required=True),
116 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
117 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
118 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
119 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
120 '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)]}),
121 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
122 'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
123 'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
124 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
125 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.\
126 \n If the phase is over, the states is set to \'Done\'.'),
127 'total_hours': fields.function(_compute, method=True, string='Total Hours'),
130 'responsible_id': lambda obj,cr,uid,context: uid,
133 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
135 _order = "project_id, date_start, sequence, name"
137 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
138 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
141 def onchange_project(self, cr, uid, ids, project, context=None):
143 result['date_start'] = False
144 project_obj = self.pool.get('project.project')
146 project_id = project_obj.browse(cr, uid, project, context=context)
147 result['date_start'] = project_id.date_start
148 return {'value': result}
151 def _check_date_start(self, cr, uid, phase, date_end, context=None):
153 Check And Compute date_end of phase if change in date_start < older time.
155 uom_obj = self.pool.get('product.uom')
156 resource_obj = self.pool.get('resource.resource')
157 cal_obj = self.pool.get('resource.calendar')
158 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
159 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
161 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
162 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
165 default_uom_id = self._get_default_uom_id(cr, uid)
166 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
167 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)
168 dt_start = work_times[0][0].strftime('%Y-%m-%d')
169 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
171 def _check_date_end(self, cr, uid, phase, date_start, context=None):
173 Check And Compute date_end of phase if change in date_end > older time.
175 uom_obj = self.pool.get('product.uom')
176 resource_obj = self.pool.get('resource.resource')
177 cal_obj = self.pool.get('resource.calendar')
178 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
179 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
181 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
182 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
185 default_uom_id = self._get_default_uom_id(cr, uid)
186 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
187 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)
188 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
189 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
191 def copy(self, cr, uid, id, default=None, context=None):
194 if not default.get('name', False):
195 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
196 return super(project_phase, self).copy(cr, uid, id, default, context)
198 def set_draft(self, cr, uid, ids, *args):
199 self.write(cr, uid, ids, {'state': 'draft'})
202 def set_open(self, cr, uid, ids, *args):
203 self.write(cr, uid, ids, {'state': 'open'})
206 def set_pending(self, cr, uid, ids, *args):
207 self.write(cr, uid, ids, {'state': 'pending'})
210 def set_cancel(self, cr, uid, ids, *args):
211 self.write(cr, uid, ids, {'state': 'cancelled'})
214 def set_done(self, cr, uid, ids, *args):
215 self.write(cr, uid, ids, {'state': 'done'})
218 def generate_resources(self, cr, uid, ids, context=None):
220 Return a list of Resource Class objects for the resources allocated to the phase.
224 resource_pool = self.pool.get('resource.resource')
225 for phase in self.browse(cr, uid, ids, context=context):
226 resource_objs = map(lambda x:x.resource_id.name, phase.resource_ids)
227 res[phase.id] = resource_objs
230 def generate_phase(self, cr, uid, ids, f, parent=False, context=None):
234 resource_pool = self.pool.get('resource.resource')
235 data_pool = self.pool.get('ir.model.data')
236 resource_allocation_pool = self.pool.get('project.resource.allocation')
237 uom_pool = self.pool.get('product.uom')
238 data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
239 for phase in self.browse(cr, uid, ids, context=context)[::-1]:
240 avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
241 duration = str(avg_days) + 'd'
242 # Create a new project for each phase
243 str_resource = ('%s,'*len(phase.resource_ids))[:-1]
244 str_vals = str_resource % tuple(map(lambda x: 'Resource_%s'%x.resource_id.id, phase.resource_ids))
245 # Phases Defination for the Project
250 '''%(phase.id, duration, str_vals or False)
252 start = 'up.Phase_%s.end' % (parent.id)
257 phase_ids.append(phase.id)
258 # Recursive call till all the next phases scheduled
259 for next_phase in phase.next_phase_ids:
260 if next_phase.state in ['draft', 'open', 'pending']:
261 rf, rphase_ids = self.generate_phase(cr, uid, [next_phase.id], f = '', parent=phase, context=context)
263 phase_ids += rphase_ids
268 def generate_schedule(self, cr, uid, root_phase, start_date=False, calendar_id=False, context=None):
270 Schedule phase with the start date till all the next phases are completed.
271 @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
272 @param: calendar_id : working calendar of the project
277 resource_pool = self.pool.get('resource.resource')
278 data_pool = self.pool.get('ir.model.data')
279 resource_allocation_pool = self.pool.get('project.resource.allocation')
280 uom_pool = self.pool.get('product.uom')
281 data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
285 start_date = root_phase.project_id.date_start or root_phase.date_start or datetime.now().strftime("%Y-%m-%d")
286 start_date = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d")
289 minimum_time_unit = 1
290 working_hours_per_day = 24
291 working_days_per_week = 7
292 working_days_per_month = 30
293 working_days_per_year = 365
297 working_hours_per_day = 8 #TODO: it should be come from calendars
298 working_days_per_week = 5
299 working_days_per_month = 20
300 working_days_per_year = 200
301 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
302 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
304 #Creating resources using the member of the Project
305 u_ids = [i.id for i in root_phase.project_id.members]
306 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
308 # Creating Resources for the Project
309 for key, vals in resource_objs.items():
311 class Resource_%s(Resource):
314 '''%(key, vals.get('vacation', False), vals.get('efficiency', False))
316 # Create a new project for each phase
320 # If project has working calendar then that
321 # else the default one would be considered
323 minimum_time_unit = %s
324 working_hours_per_day = %s
325 working_days_per_week = %s
326 working_days_per_month = %s
327 working_days_per_year = %s
330 from resource.faces import Resource
331 '''%(root_phase.project_id.id, start, minimum_time_unit, working_hours_per_day, working_days_per_week, working_days_per_month, working_days_per_year, vacation, working_days )
333 phases, phase_ids = self.generate_phase(cr, uid, [root_phase.id], func_str, context=context)
334 #Temp File to test the Code for the Allocation
335 # fn = '/home/tiny/Desktop/plt.py'
337 # fp.writelines(phases)
339 # Allocating Memory for the required Project and Pahses and Resources
341 Project = eval('Project_%d' % root_phase.project_id.id)
342 project = Task.BalancedProject(Project)
344 for phase_id in phase_ids:
345 phase = eval("project.Phase_%d" % phase_id)
346 start_date = phase.start.to_datetime()
347 end_date = phase.end.to_datetime()
348 # print phase_id,"\n\n****Phases *********", phase.resource
349 # Recalculate date_start and date_end
350 # according to constraints on date start and date end on phase
351 # if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
352 # start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
354 # start_date = s_date
355 # if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
356 # end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
357 # date_start = phase.constraint_date_end
360 # date_start = end_date
361 # # Write the calculated dates back
362 # ctx = context.copy()
363 # ctx.update({'scheduler': True})
364 self.write(cr, uid, [phase_id], {
365 'date_start': start_date.strftime('%Y-%m-%d'),
366 'date_end': end_date.strftime('%Y-%m-%d')
368 # write dates into Resources Allocation
369 # for resource in phase.resource_ids:
370 # resource_allocation_pool.write(cr, uid, [resource.id], {
371 # 'date_start': start_date.strftime('%Y-%m-%d'),
372 # 'date_end': end_date.strftime('%Y-%m-%d')
373 # }, context=context)
374 # # Recursive call till all the next phases scheduled
376 def schedule_tasks(self, cr, uid, ids, context=None):
378 Schedule the tasks according to resource available and priority.
380 task_pool = self.pool.get('project.task')
381 resource_pool = self.pool.get('resource.resource')
382 resources_list = self.generate_resources(cr, uid, ids, context=context)
384 for phase in self.browse(cr, uid, ids, context=context):
385 start_date = phase.date_start
386 if not start_date and phase.project_id.date_start:
387 start_date = phase.project_id.date_start
389 start_date = datetime.now().strftime("%Y-%m-%d")
390 resources = resources_list.get(phase.id, [])
391 calendar_id = phase.project_id.resource_calendar_id.id
392 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['draft'] , phase.task_ids))) #reassign only task not yet started
394 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
397 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
398 if "warning" not in return_msg:
399 return_msg["warning"] = warning_msg
401 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
405 class project_resource_allocation(osv.osv):
406 _name = 'project.resource.allocation'
407 _description = 'Project Resource Allocation'
408 _rec_name = 'resource_id'
410 def get_name(self, cr, uid, ids, field_name, arg, context=None):
412 for allocation in self.browse(cr, uid, ids, context=context):
413 name = allocation.phase_id.name
414 name += ' (%s%%)' %(allocation.useability)
415 res[allocation.id] = name
418 'name': fields.function(get_name, method=True, type='char', size=256),
419 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
420 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
421 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
422 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
423 'date_start': fields.date('Start Date', help="Starting Date"),
424 'date_end': fields.date('End Date', help="Ending Date"),
425 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
431 project_resource_allocation()
433 class project(osv.osv):
434 _inherit = "project.project"
436 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
437 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
439 def generate_members(self, cr, uid, ids, context=None):
441 Return a list of Resource Class objects for the resources allocated to the phase.
444 resource_pool = self.pool.get('resource.resource')
445 for project in self.browse(cr, uid, ids, context=context):
446 user_ids = map(lambda x:x.id, project.members)
447 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
448 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
449 res[project.id] = resource_objs
452 def schedule_phases(self, cr, uid, ids, context=None):
456 if type(ids) in (long, int,):
458 phase_pool = self.pool.get('project.phase')
459 for project in self.browse(cr, uid, ids, context=context):
460 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
461 ('state', 'in', ['draft', 'open', 'pending']),
462 ('previous_phase_ids', '=', False)
464 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
466 for phase in phase_pool.browse(cr, uid, phase_ids, context=context):
467 phase_pool.generate_schedule(cr, uid, phase, start_date, calendar_id, context=context)
470 def schedule_tasks(self, cr, uid, ids, context=None):
472 Schedule the tasks according to resource available and priority.
474 if type(ids) in (long, int,):
476 user_pool = self.pool.get('res.users')
477 task_pool = self.pool.get('project.task')
478 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'], context=context)
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.
529 resource_pool = self.pool.get('resource.resource')
534 user_pool = self.pool.get('res.users')
535 project_pool = self.pool.get('project.project')
536 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
537 # Create dynamic no of tasks with the resource specified
538 def create_tasks(task_number, eff, priorty=500, obj=False):
541 task is a dynamic method!
547 task.__doc__ = "TaskNO%d" %task_number
548 task.__name__ = "task%d" %task_number
551 # Create a 'Faces' project with all the tasks and resources
554 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
556 resource = reduce(operator.or_, resources)
558 raise osv.except_osv(_('Error'), _('Resources should be allocated to your phases and Members should be assigned to your Project!'))
559 minimum_time_unit = 1
560 working_hours_per_day = 24
561 working_days_per_week = 7
562 working_days_per_month = 30
563 working_days_per_year = 365
566 working_hours_per_day = 8 #TODO: it should be come from calendars
567 working_days_per_week = 5
568 working_days_per_month = 20
569 working_days_per_year = 200
570 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
571 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
572 # Dynamic creation of tasks
574 for openobect_task in self.browse(cr, uid, ids, context=context):
575 hours = str(openobect_task.planned_hours )+ 'H'
576 if openobect_task.priority in priority_dict.keys():
577 priorty = priority_dict[openobect_task.priority]
578 real_resource = False
579 if openobect_task.user_id:
580 for task_resource in resources:
581 if task_resource.__name__ == task_resource:
582 real_resource = task_resource
585 task = create_tasks(task_number, hours, priorty, real_resource)
589 face_projects = Task.BalancedProject(Project)
591 # Write back the computed dates
592 for face_project in face_projects:
593 s_date = face_project.start.to_datetime()
594 e_date = face_project.end.to_datetime()
597 ctx.update({'scheduler': True})
598 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
599 self.write(cr, uid, [ids[loop_no-1]], {
600 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
601 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
602 'user_id': user_id[0]
608 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: