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):
157 Check And Compute date_end of phase if change in date_start < older time.
159 uom_obj = self.pool.get('product.uom')
160 resource_obj = self.pool.get('resource.resource')
161 cal_obj = self.pool.get('resource.calendar')
162 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
163 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
165 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
166 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
169 default_uom_id = self._get_default_uom_id(cr, uid)
170 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
171 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)
172 dt_start = work_times[0][0].strftime('%Y-%m-%d')
173 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
175 def _check_date_end(self, cr, uid, phase, date_start, context=None):
177 Check And Compute date_end of phase if change in date_end > older time.
179 uom_obj = self.pool.get('product.uom')
180 resource_obj = self.pool.get('resource.resource')
181 cal_obj = self.pool.get('resource.calendar')
182 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
183 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
185 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
186 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
189 default_uom_id = self._get_default_uom_id(cr, uid)
190 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
191 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)
192 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
193 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
195 def copy(self, cr, uid, id, default=None, context=None):
198 if not default.get('name', False):
199 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
200 return super(project_phase, self).copy(cr, uid, id, default, context)
202 def set_draft(self, cr, uid, ids, *args):
203 self.write(cr, uid, ids, {'state': 'draft'})
206 def set_open(self, cr, uid, ids, *args):
207 self.write(cr, uid, ids, {'state': 'open'})
210 def set_pending(self, cr, uid, ids, *args):
211 self.write(cr, uid, ids, {'state': 'pending'})
214 def set_cancel(self, cr, uid, ids, *args):
215 self.write(cr, uid, ids, {'state': 'cancelled'})
218 def set_done(self, cr, uid, ids, *args):
219 self.write(cr, uid, ids, {'state': 'done'})
222 def generate_resources(self, cr, uid, ids, context=None):
224 Return a list of Resource Class objects for the resources allocated to the phase.
227 resource_pool = self.pool.get('resource.resource')
228 for phase in self.browse(cr, uid, ids, context=context):
229 user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
230 project = phase.project_id
231 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
232 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
233 res[phase.id] = resource_objs
236 def generate_schedule(self, cr, uid, ids, start_date=False, calendar_id=False, context=None):
238 Schedule phase with the start date till all the next phases are completed.
239 @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
240 @param: calendar_id : working calendar of the project
244 resource_pool = self.pool.get('resource.resource')
245 data_pool = self.pool.get('ir.model.data')
246 resource_allocation_pool = self.pool.get('project.resource.allocation')
247 uom_pool = self.pool.get('product.uom')
248 data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
249 for phase in self.browse(cr, uid, ids, context=context):
250 if not phase.responsible_id:
251 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
254 start_date = phase.project_id.date_start or phase.date_start or datetime.now().strftime("%Y-%m-%d")
255 start_date = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d")
256 phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
257 avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
258 duration = str(avg_days) + 'd'
259 # Create a new project for each phase
261 # If project has working calendar then that
262 # else the default one would be considered
264 minimum_time_unit = 1
265 resource = phase_resource_obj
266 working_hours_per_day = 24
269 working_hours_per_day = 8 #TODO: it should be come from calendars
270 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
271 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
275 project = Task.BalancedProject(Project)
277 s_date = project.phase.start.to_datetime()
278 e_date = project.phase.end.to_datetime()
279 # Recalculate date_start and date_end
280 # according to constraints on date start and date end on phase
281 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
282 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
285 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
286 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
287 date_start = phase.constraint_date_end
290 date_start = end_date
291 # Write the calculated dates back
293 ctx.update({'scheduler': True})
294 self.write(cr, uid, [phase.id], {
295 'date_start': start_date.strftime('%Y-%m-%d'),
296 'date_end': end_date.strftime('%Y-%m-%d')
298 # write dates into Resources Allocation
299 for resource in phase.resource_ids:
300 resource_allocation_pool.write(cr, uid, [resource.id], {
301 'date_start': start_date.strftime('%Y-%m-%d'),
302 'date_end': end_date.strftime('%Y-%m-%d')
304 # Recursive call till all the next phases scheduled
305 for phase in phase.next_phase_ids:
306 if phase.state in ['draft', 'open', 'pending']:
307 id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
308 self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
313 def schedule_tasks(self, cr, uid, ids, context=None):
315 Schedule the tasks according to resource available and priority.
317 task_pool = self.pool.get('project.task')
318 resource_pool = self.pool.get('resource.resource')
319 resources_list = self.generate_resources(cr, uid, ids, context=context)
321 for phase in self.browse(cr, uid, ids, context=context):
322 start_date = phase.date_start
323 if not start_date and phase.project_id.date_start:
324 start_date = phase.project_id.date_start
326 start_date = datetime.now().strftime("%Y-%m-%d")
327 resources = resources_list.get(phase.id, [])
328 calendar_id = phase.project_id.resource_calendar_id.id
329 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
331 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
334 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
335 if "warning" not in return_msg:
336 return_msg["warning"] = warning_msg
338 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
342 class project_resource_allocation(osv.osv):
343 _name = 'project.resource.allocation'
344 _description = 'Project Resource Allocation'
345 _rec_name = 'resource_id'
347 def get_name(self, cr, uid, ids, field_name, arg, context=None):
349 for allocation in self.browse(cr, uid, ids, context=context):
350 name = allocation.phase_id.name
351 name += ' (%s%%)' %(allocation.useability)
352 res[allocation.id] = name
355 'name': fields.function(get_name, method=True, type='char', size=256),
356 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
357 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
358 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
359 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
360 'date_start': fields.date('Start Date', help="Starting Date"),
361 'date_end': fields.date('End Date', help="Ending Date"),
362 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
368 project_resource_allocation()
370 class project(osv.osv):
371 _inherit = "project.project"
373 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
374 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
376 def generate_members(self, cr, uid, ids, context=None):
378 Return a list of Resource Class objects for the resources allocated to the phase.
381 resource_pool = self.pool.get('resource.resource')
382 for project in self.browse(cr, uid, ids, context=context):
383 user_ids = map(lambda x:x.id, project.members)
384 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
385 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
386 res[project.id] = resource_objs
389 def schedule_phases(self, cr, uid, ids, context=None):
393 if type(ids) in (long, int,):
395 phase_pool = self.pool.get('project.phase')
396 for project in self.browse(cr, uid, ids, context=context):
397 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
398 ('state', 'in', ['draft', 'open', 'pending']),
399 ('previous_phase_ids', '=', False)
401 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
403 phase_pool.generate_schedule(cr, uid, phase_ids, start_date, calendar_id, context=context)
406 def schedule_tasks(self, cr, uid, ids, context=None):
408 Schedule the tasks according to resource available and priority.
410 if type(ids) in (long, int,):
412 user_pool = self.pool.get('res.users')
413 task_pool = self.pool.get('project.task')
414 resource_pool = self.pool.get('resource.resource')
415 resources_list = self.generate_members(cr, uid, ids, context=context)
417 for project in self.browse(cr, uid, ids, context=context):
418 start_date = project.date_start
420 start_date = datetime.now().strftime("%Y-%m-%d")
421 resources = resources_list.get(project.id, [])
422 calendar_id = project.resource_calendar_id.id
423 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
424 ('state', 'in', ['draft', 'open', 'pending'])
429 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
431 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
432 if "warning" not in return_msg:
433 return_msg["warning"] = warning_msg
435 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
441 class resource_resource(osv.osv):
442 _inherit = "resource.resource"
443 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
446 if context.get('project_id',False):
447 project_pool = self.pool.get('project.project')
448 project_rec = project_pool.browse(cr, uid, context['project_id'], context=context)
449 user_ids = [user_id.id for user_id in project_rec.members]
450 args.append(('user_id','in',user_ids))
451 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
455 class project_task(osv.osv):
456 _inherit = "project.task"
458 'phase_id': fields.many2one('project.phase', 'Project Phase'),
461 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
463 Schedule the tasks according to resource available and priority.
465 resource_pool = self.pool.get('resource.resource')
470 user_pool = self.pool.get('res.users')
471 project_pool = self.pool.get('project.project')
472 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
473 # Create dynamic no of tasks with the resource specified
474 def create_tasks(task_number, eff, priorty=500, obj=False):
477 task is a dynamic method!
483 task.__doc__ = "TaskNO%d" %task_number
484 task.__name__ = "task%d" %task_number
487 # Create a 'Faces' project with all the tasks and resources
490 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
492 resource = reduce(operator.or_, resources)
494 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
495 minimum_time_unit = 1
496 working_hours_per_day = 24
499 working_hours_per_day = 8 #TODO: it should be come from calendars
500 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
501 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
502 # Dynamic creation of tasks
504 for openobect_task in self.browse(cr, uid, ids, context=context):
505 hours = str(openobect_task.planned_hours )+ 'H'
506 if openobect_task.priority in priority_dict.keys():
507 priorty = priority_dict[openobect_task.priority]
508 real_resource = False
509 if openobect_task.user_id:
510 for task_resource in resources:
511 if task_resource.__name__ == task_resource:
512 real_resource = task_resource
515 task = create_tasks(task_number, hours, priorty, real_resource)
519 face_projects = Task.BalancedProject(Project)
521 # Write back the computed dates
522 for face_project in face_projects:
523 s_date = face_project.start.to_datetime()
524 e_date = face_project.end.to_datetime()
527 ctx.update({'scheduler': True})
528 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
529 self.write(cr, uid, [ids[loop_no-1]], {
530 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
531 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
532 'user_id': user_id[0]
538 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: