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, 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
109 'name': fields.char("Name", size=64, required=True),
110 'date_start': fields.date('Start Date', help="Starting Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
111 'date_end': fields.date('End Date', help="Ending Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
112 'constraint_date_start': fields.date('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
113 'constraint_date_end': fields.date('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
114 'project_id': fields.many2one('project.project', 'Project', required=True),
115 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
116 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
117 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
118 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
119 '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)]}),
120 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
121 'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
122 'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
123 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
124 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.\
125 \n If the phase is over, the states is set to \'Done\'.'),
126 'total_hours': fields.function(_compute, method=True, string='Total Hours'),
129 'responsible_id': lambda obj,cr,uid,context: uid,
132 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
134 _order = "project_id, date_start, sequence, name"
136 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
137 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
140 def onchange_project(self, cr, uid, ids, project, context=None):
142 result['date_start'] = False
143 project_obj = self.pool.get('project.project')
145 project_id = project_obj.browse(cr, uid, project, context=context)
146 result['date_start'] = project_id.date_start
147 return {'value': result}
149 def onchange_days(self, cr, uid, ids, project, context=None):
152 project_id = self.browse(cr, uid, id, context=context)
153 newdate = datetime.strptime(project_id.date_start, '%Y-%m-%d') + relativedelta(days=project_id.duration or 0.0)
154 result['date_end'] = newdate.strftime('%Y-%m-%d')
155 return {'value': result}
157 def _check_date_start(self, cr, uid, phase, date_end, context=None):
161 Check And Compute date_end of phase if change in date_start < older time.
163 uom_obj = self.pool.get('product.uom')
164 resource_obj = self.pool.get('resource.resource')
165 cal_obj = self.pool.get('resource.calendar')
166 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
167 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
169 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
170 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
173 default_uom_id = self._get_default_uom_id(cr, uid)
174 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
175 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)
176 dt_start = work_times[0][0].strftime('%Y-%m-%d')
177 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
179 def _check_date_end(self, cr, uid, phase, date_start, context=None):
183 Check And Compute date_end of phase if change in date_end > older time.
185 uom_obj = self.pool.get('product.uom')
186 resource_obj = self.pool.get('resource.resource')
187 cal_obj = self.pool.get('resource.calendar')
188 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
189 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
191 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
192 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
195 default_uom_id = self._get_default_uom_id(cr, uid)
196 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
197 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)
198 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
199 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
201 def write(self, cr, uid, ids, vals, context=None):
202 resource_calendar_obj = self.pool.get('resource.calendar')
203 resource_obj = self.pool.get('resource.resource')
204 uom_obj = self.pool.get('product.uom')
207 res = super(project_phase, self).write(cr, uid, ids, vals, context=context)
208 if context.get('scheduler',False):
210 # Consider calendar and efficiency if the phase is performed by a resource
211 # otherwise consider the project's working calendar
213 #TOCHECK : why need this ?
214 if isinstance(ids, (int, long)):
216 default_uom_id = self._get_default_uom_id(cr, uid)
217 for phase in self.browse(cr, uid, ids, context=context):
218 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
219 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
221 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
225 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
226 # Change the date_start and date_end
227 # for previous and next phases respectively based on valid condition
228 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
229 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
230 work_times = resource_calendar_obj.interval_get(cr, uid, calendar_id, dt_start, avg_hours or 0.0, resource_id and resource_id[0] or False)
232 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
233 for prv_phase in phase.previous_phase_ids:
234 if prv_phase.id == phase.id:
236 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
238 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
239 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
240 work_times = resource_calendar_obj.interval_min_get(cr, uid, calendar_id, dt_end, avg_hours or 0.0, resource_id and resource_id[0] or False)
242 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
243 for next_phase in phase.next_phase_ids:
244 if next_phase.id == phase.id:
246 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
250 def copy(self, cr, uid, id, default=None, context=None):
253 if not default.get('name', False):
254 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
255 return super(project_phase, self).copy(cr, uid, id, default, context)
257 def set_draft(self, cr, uid, ids, *args):
258 self.write(cr, uid, ids, {'state': 'draft'})
261 def set_open(self, cr, uid, ids, *args):
262 self.write(cr, uid, ids, {'state': 'open'})
265 def set_pending(self, cr, uid, ids, *args):
266 self.write(cr, uid, ids, {'state': 'pending'})
269 def set_cancel(self, cr, uid, ids, *args):
270 self.write(cr, uid, ids, {'state': 'cancelled'})
273 def set_done(self, cr, uid, ids, *args):
274 self.write(cr, uid, ids, {'state': 'done'})
277 def generate_resources(self, cr, uid, ids, context=None):
279 Return a list of Resource Class objects for the resources allocated to the phase.
284 resource_pool = self.pool.get('resource.resource')
285 for phase in self.browse(cr, uid, ids, context=context):
286 user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
287 project = phase.project_id
288 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
289 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
290 res[phase.id] = resource_objs
293 def generate_schedule(self, cr, uid, ids, start_date, calendar_id=False, context=None):
295 Schedule phase with the start date till all the next phases are completed.
296 @param: start_dsate : start date for the phase
297 @param: calendar_id : working calendar of the project
301 resource_pool = self.pool.get('resource.resource')
302 uom_pool = self.pool.get('product.uom')
305 default_uom_id = self._get_default_uom_id(cr, uid)
306 for phase in self.browse(cr, uid, ids, context=context):
307 if not phase.responsible_id:
308 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
310 phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
311 avg_hours = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
312 duration = str(avg_hours) + 'H'
313 # Create a new project for each phase
315 # If project has working calendar then that
316 # else the default one would be considered
318 minimum_time_unit = 1
319 resource = phase_resource_obj
321 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
322 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
327 project = Task.BalancedProject(Project)
328 s_date = project.phase.start.to_datetime()
329 e_date = project.phase.end.to_datetime()
330 # Recalculate date_start and date_end
331 # according to constraints on date start and date end on phase
332 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
333 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
336 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
337 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
338 date_start = phase.constraint_date_end
341 date_start = end_date
342 # Write the calculated dates back
344 ctx.update({'scheduler': True})
345 self.write(cr, uid, [phase.id], {
346 'date_start': start_date.strftime('%Y-%m-%d'),
347 'date_end': end_date.strftime('%Y-%m-%d')
350 # Recursive call till all the next phases scheduled
351 for phase in phase.next_phase_ids:
352 if phase.state in ['draft', 'open', 'pending']:
353 id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
354 self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
359 def schedule_tasks(self, cr, uid, ids, context=None):
361 Schedule the tasks according to resource available and priority.
363 task_pool = self.pool.get('project.task')
364 resource_pool = self.pool.get('resource.resource')
367 resources_list = self.generate_resources(cr, uid, ids, context=context)
369 for phase in self.browse(cr, uid, ids, context=context):
370 start_date = phase.date_start
371 if not start_date and phase.project_id.date_start:
372 start_date = phase.project_id.date_start
374 start_date = datetime.now().strftime("%Y-%m-%d")
375 resources = resources_list.get(phase.id, [])
376 calendar_id = phase.project_id.resource_calendar_id.id
377 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
379 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
382 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
383 if "warning" not in return_msg:
384 return_msg["warning"] = warning_msg
386 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
390 class project_resource_allocation(osv.osv):
391 _name = 'project.resource.allocation'
392 _description = 'Project Resource Allocation'
393 _rec_name = 'resource_id'
395 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
396 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
397 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
398 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
399 'date_start': fields.date('Start Date', help="Starting Date"),
400 'date_end': fields.date('End Date', help="Ending Date"),
401 'useability': fields.float('Availability', help="Usability of this resource for this project phase in percentage (=50%)"),
407 project_resource_allocation()
409 class project(osv.osv):
410 _inherit = "project.project"
412 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
413 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
415 def generate_members(self, cr, uid, ids, context=None):
417 Return a list of Resource Class objects for the resources allocated to the phase.
420 resource_pool = self.pool.get('resource.resource')
423 for project in self.browse(cr, uid, ids, context=context):
424 user_ids = map(lambda x:x.id, project.members)
425 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
426 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
427 res[project.id] = resource_objs
430 def schedule_phases(self, cr, uid, ids, context=None):
436 if type(ids) in (long, int,):
438 phase_pool = self.pool.get('project.phase')
439 for project in self.browse(cr, uid, ids, context=context):
440 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
441 ('state', 'in', ['draft', 'open', 'pending']),
442 ('previous_phase_ids', '=', False)
444 start_date = project.date_start
446 start_date = datetime.now().strftime("%Y-%m-%d")
447 start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M")
448 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
449 phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context)
452 def schedule_tasks(self, cr, uid, ids, context=None):
454 Schedule the tasks according to resource available and priority.
456 if type(ids) in (long, int,):
458 user_pool = self.pool.get('res.users')
459 task_pool = self.pool.get('project.task')
460 resource_pool = self.pool.get('resource.resource')
464 resources_list = self.generate_members(cr, uid, ids, context=context)
466 for project in self.browse(cr, uid, ids, context=context):
467 start_date = project.date_start
469 start_date = datetime.now().strftime("%Y-%m-%d")
470 resources = resources_list.get(project.id, [])
471 calendar_id = project.resource_calendar_id.id
472 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
473 ('state', 'in', ['draft', 'open', 'pending'])
478 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
480 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
481 if "warning" not in return_msg:
482 return_msg["warning"] = warning_msg
484 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
490 class resource_resource(osv.osv):
491 _inherit = "resource.resource"
492 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
495 if context.get('project_id',False):
496 project_pool = self.pool.get('project.project')
497 project_rec = project_pool.browse(cr, uid, context['project_id'])
498 user_ids = [user_id.id for user_id in project_rec.members]
499 args.append(('user_id','in',user_ids))
500 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
504 class project_task(osv.osv):
505 _inherit = "project.task"
507 'phase_id': fields.many2one('project.phase', 'Project Phase'),
510 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
512 Schedule the tasks according to resource available and priority.
518 user_pool = self.pool.get('res.users')
519 project_pool = self.pool.get('project.project')
520 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
521 # Create dynamic no of tasks with the resource specified
522 def create_tasks(task_number, eff, priorty=500, obj=False):
525 task is a dynamic method!
531 task.__doc__ = "TaskNO%d" %task_number
532 task.__name__ = "task%d" %task_number
535 # Create a 'Faces' project with all the tasks and resources
538 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
540 resource = reduce(operator.or_, resources)
542 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
543 minimum_time_unit = 1
544 if calendar_id: # If project has working calendar
545 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
546 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
547 # Dynamic creation of tasks
549 for openobect_task in self.browse(cr, uid, ids, context=context):
550 hours = str(openobect_task.planned_hours )+ 'H'
551 if openobect_task.priority in priority_dict.keys():
552 priorty = priority_dict[openobect_task.priority]
553 real_resource = False
554 if openobect_task.user_id:
555 for task_resource in resources:
556 if task_resource.__name__ == task_resource:
557 real_resource = task_resource
560 task = create_tasks(task_number, hours, priorty, real_resource)
564 face_projects = Task.BalancedProject(Project)
566 # Write back the computed dates
567 for face_project in face_projects:
568 s_date = face_project.start.to_datetime()
569 e_date = face_project.end.to_datetime()
572 ctx.update({'scheduler': True})
573 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
574 self.write(cr, uid, [ids[loop_no-1]], {
575 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
576 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
577 'user_id': user_id[0]
583 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: