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="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)]}),
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)
223 project = phase.project_id
224 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
225 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
226 res[phase.id] = resource_objs
229 def generate_schedule(self, cr, uid, ids, start_date=False, calendar_id=False, context=None):
231 Schedule phase with the start date till all the next phases are completed.
232 @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
233 @param: calendar_id : working calendar of the project
237 resource_pool = self.pool.get('resource.resource')
238 data_pool = self.pool.get('ir.model.data')
239 resource_allocation_pool = self.pool.get('project.resource.allocation')
240 uom_pool = self.pool.get('product.uom')
241 data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
242 for phase in self.browse(cr, uid, ids, context=context):
243 if not phase.responsible_id:
244 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
247 start_date = phase.project_id.date_start or phase.date_start or datetime.now().strftime("%Y-%m-%d")
248 start_date = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d")
249 phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
250 avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
251 duration = str(avg_days) + 'd'
252 # Create a new project for each phase
254 # If project has working calendar then that
255 # else the default one would be considered
257 minimum_time_unit = 1
258 resource = phase_resource_obj
259 working_hours_per_day = 24
262 working_hours_per_day = 8 #TODO: it should be come from calendars
263 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
264 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
268 project = Task.BalancedProject(Project)
270 s_date = project.phase.start.to_datetime()
271 e_date = project.phase.end.to_datetime()
272 # Recalculate date_start and date_end
273 # according to constraints on date start and date end on phase
274 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
275 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
278 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
279 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
280 date_start = phase.constraint_date_end
283 date_start = end_date
284 # Write the calculated dates back
286 ctx.update({'scheduler': True})
287 self.write(cr, uid, [phase.id], {
288 'date_start': start_date.strftime('%Y-%m-%d'),
289 'date_end': end_date.strftime('%Y-%m-%d')
291 # write dates into Resources Allocation
292 for resource in phase.resource_ids:
293 resource_allocation_pool.write(cr, uid, [resource.id], {
294 'date_start': start_date.strftime('%Y-%m-%d'),
295 'date_end': end_date.strftime('%Y-%m-%d')
297 # Recursive call till all the next phases scheduled
298 for phase in phase.next_phase_ids:
299 if phase.state in ['draft', 'open', 'pending']:
300 id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
301 self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
306 def schedule_tasks(self, cr, uid, ids, context=None):
308 Schedule the tasks according to resource available and priority.
310 task_pool = self.pool.get('project.task')
311 resource_pool = self.pool.get('resource.resource')
312 resources_list = self.generate_resources(cr, uid, ids, context=context)
314 for phase in self.browse(cr, uid, ids, context=context):
315 start_date = phase.date_start
316 if not start_date and phase.project_id.date_start:
317 start_date = phase.project_id.date_start
319 start_date = datetime.now().strftime("%Y-%m-%d")
320 resources = resources_list.get(phase.id, [])
321 calendar_id = phase.project_id.resource_calendar_id.id
322 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
324 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
327 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
328 if "warning" not in return_msg:
329 return_msg["warning"] = warning_msg
331 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
335 class project_resource_allocation(osv.osv):
336 _name = 'project.resource.allocation'
337 _description = 'Project Resource Allocation'
338 _rec_name = 'resource_id'
340 def get_name(self, cr, uid, ids, field_name, arg, context=None):
342 for allocation in self.browse(cr, uid, ids, context=context):
343 name = allocation.phase_id.name
344 name += ' (%s%%)' %(allocation.useability)
345 res[allocation.id] = name
348 'name': fields.function(get_name, method=True, type='char', size=256),
349 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
350 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
351 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
352 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
353 'date_start': fields.date('Start Date', help="Starting Date"),
354 'date_end': fields.date('End Date', help="Ending Date"),
355 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
361 project_resource_allocation()
363 class project(osv.osv):
364 _inherit = "project.project"
366 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
367 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
369 def generate_members(self, cr, uid, ids, context=None):
371 Return a list of Resource Class objects for the resources allocated to the phase.
374 resource_pool = self.pool.get('resource.resource')
375 for project in self.browse(cr, uid, ids, context=context):
376 user_ids = map(lambda x:x.id, project.members)
377 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
378 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
379 res[project.id] = resource_objs
382 def schedule_phases(self, cr, uid, ids, context=None):
386 if type(ids) in (long, int,):
388 phase_pool = self.pool.get('project.phase')
389 for project in self.browse(cr, uid, ids, context=context):
390 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
391 ('state', 'in', ['draft', 'open', 'pending']),
392 ('previous_phase_ids', '=', False)
394 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
396 phase_pool.generate_schedule(cr, uid, phase_ids, start_date, calendar_id, context=context)
399 def schedule_tasks(self, cr, uid, ids, context=None):
401 Schedule the tasks according to resource available and priority.
403 if type(ids) in (long, int,):
405 user_pool = self.pool.get('res.users')
406 task_pool = self.pool.get('project.task')
407 resource_pool = self.pool.get('resource.resource')
408 resources_list = self.generate_members(cr, uid, ids, context=context)
410 for project in self.browse(cr, uid, ids, context=context):
411 start_date = project.date_start
413 start_date = datetime.now().strftime("%Y-%m-%d")
414 resources = resources_list.get(project.id, [])
415 calendar_id = project.resource_calendar_id.id
416 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
417 ('state', 'in', ['draft', 'open', 'pending'])
422 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
424 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
425 if "warning" not in return_msg:
426 return_msg["warning"] = warning_msg
428 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
434 class resource_resource(osv.osv):
435 _inherit = "resource.resource"
436 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
439 if context.get('project_id',False):
440 project_pool = self.pool.get('project.project')
441 project_rec = project_pool.browse(cr, uid, context['project_id'], context=context)
442 user_ids = [user_id.id for user_id in project_rec.members]
443 args.append(('user_id','in',user_ids))
444 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
448 class project_task(osv.osv):
449 _inherit = "project.task"
451 'phase_id': fields.many2one('project.phase', 'Project Phase'),
454 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
456 Schedule the tasks according to resource available and priority.
458 resource_pool = self.pool.get('resource.resource')
463 user_pool = self.pool.get('res.users')
464 project_pool = self.pool.get('project.project')
465 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
466 # Create dynamic no of tasks with the resource specified
467 def create_tasks(task_number, eff, priorty=500, obj=False):
470 task is a dynamic method!
476 task.__doc__ = "TaskNO%d" %task_number
477 task.__name__ = "task%d" %task_number
480 # Create a 'Faces' project with all the tasks and resources
483 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
485 resource = reduce(operator.or_, resources)
487 raise osv.except_osv(_('Error'), _('Resources should be allocated to your phases and Members should be assigned to your Project!'))
488 minimum_time_unit = 1
489 working_hours_per_day = 24
492 working_hours_per_day = 8 #TODO: it should be come from calendars
493 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
494 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
495 # Dynamic creation of tasks
497 for openobect_task in self.browse(cr, uid, ids, context=context):
498 hours = str(openobect_task.planned_hours )+ 'H'
499 if openobect_task.priority in priority_dict.keys():
500 priorty = priority_dict[openobect_task.priority]
501 real_resource = False
502 if openobect_task.user_id:
503 for task_resource in resources:
504 if task_resource.__name__ == task_resource:
505 real_resource = task_resource
508 task = create_tasks(task_number, hours, priorty, real_resource)
512 face_projects = Task.BalancedProject(Project)
514 # Write back the computed dates
515 for face_project in face_projects:
516 s_date = face_project.start.to_datetime()
517 e_date = face_project.end.to_datetime()
520 ctx.update({'scheduler': True})
521 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
522 self.write(cr, uid, [ids[loop_no-1]], {
523 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
524 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
525 'user_id': user_id[0]
531 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: