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 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 = self.generate_resources(cr, uid, [phase.id], context=context)[phase.id]
250 avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
251 if not phase_resource_obj: #TOCHECK: why need this ?
252 avg_days = avg_days - 1
253 duration = str(avg_days) + 'd'
254 # Create a new project for each phase
256 # If project has working calendar then that
257 # else the default one would be considered
259 minimum_time_unit = 1
260 resource = phase_resource_obj
261 working_hours_per_day = 24
264 working_hours_per_day = 8 #TODO: it should be come from calendars
265 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
266 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
270 project = Task.BalancedProject(Project)
272 s_date = project.phase.start.to_datetime()
273 e_date = project.phase.end.to_datetime()
274 # Recalculate date_start and date_end
275 # according to constraints on date start and date end on phase
276 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
277 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
280 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
281 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
282 date_start = phase.constraint_date_end
285 date_start = end_date
286 # Write the calculated dates back
288 ctx.update({'scheduler': True})
289 self.write(cr, uid, [phase.id], {
290 'date_start': start_date.strftime('%Y-%m-%d'),
291 'date_end': end_date.strftime('%Y-%m-%d')
293 # write dates into Resources Allocation
294 for resource in phase.resource_ids:
295 resource_allocation_pool.write(cr, uid, [resource.id], {
296 'date_start': start_date.strftime('%Y-%m-%d'),
297 'date_end': end_date.strftime('%Y-%m-%d')
299 # Recursive call till all the next phases scheduled
300 for next_phase in phase.next_phase_ids:
301 if next_phase.state in ['draft', 'open', 'pending']:
302 id_cal = next_phase.project_id.resource_calendar_id and next_phase.project_id.resource_calendar_id.id or False
303 self.generate_schedule(cr, uid, [next_phase.id], date_start+timedelta(days=1), id_cal, context=context)
308 def schedule_tasks(self, cr, uid, ids, context=None):
310 Schedule the tasks according to resource available and priority.
312 task_pool = self.pool.get('project.task')
313 resource_pool = self.pool.get('resource.resource')
314 resources_list = self.generate_resources(cr, uid, ids, context=context)
316 for phase in self.browse(cr, uid, ids, context=context):
317 start_date = phase.date_start
318 if not start_date and phase.project_id.date_start:
319 start_date = phase.project_id.date_start
321 start_date = datetime.now().strftime("%Y-%m-%d")
322 resources = resources_list.get(phase.id, [])
323 calendar_id = phase.project_id.resource_calendar_id.id
324 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['draft'] , phase.task_ids))) #reassign only task not yet started
326 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
329 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
330 if "warning" not in return_msg:
331 return_msg["warning"] = warning_msg
333 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
337 class project_resource_allocation(osv.osv):
338 _name = 'project.resource.allocation'
339 _description = 'Project Resource Allocation'
340 _rec_name = 'resource_id'
342 def get_name(self, cr, uid, ids, field_name, arg, context=None):
344 for allocation in self.browse(cr, uid, ids, context=context):
345 name = allocation.phase_id.name
346 name += ' (%s%%)' %(allocation.useability)
347 res[allocation.id] = name
350 'name': fields.function(get_name, method=True, type='char', size=256),
351 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
352 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
353 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
354 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
355 'date_start': fields.date('Start Date', help="Starting Date"),
356 'date_end': fields.date('End Date', help="Ending Date"),
357 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
363 project_resource_allocation()
365 class project(osv.osv):
366 _inherit = "project.project"
368 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
369 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
371 def generate_members(self, cr, uid, ids, context=None):
373 Return a list of Resource Class objects for the resources allocated to the phase.
376 resource_pool = self.pool.get('resource.resource')
377 for project in self.browse(cr, uid, ids, context=context):
378 user_ids = map(lambda x:x.id, project.members)
379 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
380 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
381 res[project.id] = resource_objs
384 def schedule_phases(self, cr, uid, ids, context=None):
388 if type(ids) in (long, int,):
390 phase_pool = self.pool.get('project.phase')
391 for project in self.browse(cr, uid, ids, context=context):
392 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
393 ('state', 'in', ['draft', 'open', 'pending']),
394 ('previous_phase_ids', '=', False)
396 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
398 phase_pool.generate_schedule(cr, uid, phase_ids, start_date, calendar_id, context=context)
401 def schedule_tasks(self, cr, uid, ids, context=None):
403 Schedule the tasks according to resource available and priority.
405 if type(ids) in (long, int,):
407 user_pool = self.pool.get('res.users')
408 task_pool = self.pool.get('project.task')
409 resource_pool = self.pool.get('resource.resource')
410 resources_list = self.generate_members(cr, uid, ids, context=context)
412 for project in self.browse(cr, uid, ids, context=context):
413 start_date = project.date_start
415 start_date = datetime.now().strftime("%Y-%m-%d")
416 resources = resources_list.get(project.id, [])
417 calendar_id = project.resource_calendar_id.id
418 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
419 ('state', 'in', ['draft', 'open', 'pending'])
424 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
426 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
427 if "warning" not in return_msg:
428 return_msg["warning"] = warning_msg
430 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
436 class resource_resource(osv.osv):
437 _inherit = "resource.resource"
438 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
441 if context.get('project_id',False):
442 project_pool = self.pool.get('project.project')
443 project_rec = project_pool.browse(cr, uid, context['project_id'], context=context)
444 user_ids = [user_id.id for user_id in project_rec.members]
445 args.append(('user_id','in',user_ids))
446 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
450 class project_task(osv.osv):
451 _inherit = "project.task"
453 'phase_id': fields.many2one('project.phase', 'Project Phase'),
456 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
458 Schedule the tasks according to resource available and priority.
460 resource_pool = self.pool.get('resource.resource')
465 user_pool = self.pool.get('res.users')
466 project_pool = self.pool.get('project.project')
467 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
468 # Create dynamic no of tasks with the resource specified
469 def create_tasks(task_number, eff, priorty=500, obj=False):
472 task is a dynamic method!
478 task.__doc__ = "TaskNO%d" %task_number
479 task.__name__ = "task%d" %task_number
482 # Create a 'Faces' project with all the tasks and resources
485 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
487 resource = reduce(operator.or_, resources)
489 raise osv.except_osv(_('Error'), _('Resources should be allocated to your phases and Members should be assigned to your Project!'))
490 minimum_time_unit = 1
491 working_hours_per_day = 24
494 working_hours_per_day = 8 #TODO: it should be come from calendars
495 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
496 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
497 # Dynamic creation of tasks
499 for openobect_task in self.browse(cr, uid, ids, context=context):
500 hours = str(openobect_task.planned_hours )+ 'H'
501 if openobect_task.priority in priority_dict.keys():
502 priorty = priority_dict[openobect_task.priority]
503 real_resource = False
504 if openobect_task.user_id:
505 for task_resource in resources:
506 if task_resource.__name__ == task_resource:
507 real_resource = task_resource
510 task = create_tasks(task_number, hours, priorty, real_resource)
514 face_projects = Task.BalancedProject(Project)
516 # Write back the computed dates
517 for face_project in face_projects:
518 s_date = face_project.start.to_datetime()
519 e_date = face_project.end.to_datetime()
522 ctx.update({'scheduler': True})
523 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
524 self.write(cr, uid, [ids[loop_no-1]], {
525 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
526 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
527 'user_id': user_id[0]
533 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: