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 tools.translate import _
24 from osv import fields, osv
25 from resource.faces import task as Task
28 class project_phase(osv.osv):
29 _name = "project.phase"
30 _description = "Project Phase"
32 def _check_recursion(self, cr, uid, ids, context=None):
36 data_phase = self.browse(cr, uid, ids[0], context=context)
37 prev_ids = data_phase.previous_phase_ids
38 next_ids = data_phase.next_phase_ids
39 # it should neither be in prev_ids nor in next_ids
40 if (data_phase in prev_ids) or (data_phase in next_ids):
42 ids = [id for id in prev_ids if id in next_ids]
43 # both prev_ids and next_ids must be unique
47 prev_ids = [rec.id for rec in prev_ids]
48 next_ids = [rec.id for rec in next_ids]
51 cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
52 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
53 if data_phase.id in prv_phase_ids:
55 ids = [id for id in prv_phase_ids if id in next_ids]
58 prev_ids = prv_phase_ids
61 cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
62 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
63 if data_phase.id in next_phase_ids:
65 ids = [id for id in next_phase_ids if id in prev_ids]
68 next_ids = next_phase_ids
71 def _check_dates(self, cr, uid, ids, context=None):
72 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
73 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
77 def _check_constraint_start(self, cr, uid, ids, context=None):
78 phase = self.read(cr, uid, ids[0], ['date_start', 'constraint_date_start'], context=context)
79 if phase['date_start'] and phase['constraint_date_start'] and phase['date_start'] < phase['constraint_date_start']:
83 def _check_constraint_end(self, cr, uid, ids, context=None):
84 phase = self.read(cr, uid, ids[0], ['date_end', 'constraint_date_end'], context=context)
85 if phase['date_end'] and phase['constraint_date_end'] and phase['date_end'] > phase['constraint_date_end']:
89 def _get_default_uom_id(self, cr, uid):
90 model_data_obj = self.pool.get('ir.model.data')
91 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
92 return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
95 'name': fields.char("Name", size=64, required=True),
96 'date_start': fields.date('Start Date', help="Starting Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
97 'date_end': fields.date('End Date', help="Ending Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
98 'constraint_date_start': fields.date('Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
99 'constraint_date_end': fields.date('End Date', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
100 'project_id': fields.many2one('project.project', 'Project', required=True),
101 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
102 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
103 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
104 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
105 '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)]}),
106 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
107 'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
108 'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
109 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
110 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.\
111 \n If the phase is over, the states is set to \'Done\'.')
114 'responsible_id': lambda obj,cr,uid,context: uid,
117 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
119 _order = "project_id, date_start, sequence, name"
121 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
122 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
125 def onchange_project(self, cr, uid, ids, project, context=None):
127 result['date_start'] = False
128 project_obj = self.pool.get('project.project')
130 project_id = project_obj.browse(cr, uid, project, context=context)
131 result['date_start'] = project_id.date_start
132 return {'value': result}
134 def _check_date_start(self, cr, uid, phase, date_end, context=None):
138 Check And Compute date_end of phase if change in date_start < older time.
140 uom_obj = self.pool.get('product.uom')
141 resource_obj = self.pool.get('resource.resource')
142 cal_obj = self.pool.get('resource.calendar')
143 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
144 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
146 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
147 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
150 default_uom_id = self._get_default_uom_id(cr, uid)
151 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
152 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)
153 dt_start = work_times[0][0].strftime('%Y-%m-%d')
154 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
156 def _check_date_end(self, cr, uid, phase, date_start, context=None):
160 Check And Compute date_end of phase if change in date_end > older time.
162 uom_obj = self.pool.get('product.uom')
163 resource_obj = self.pool.get('resource.resource')
164 cal_obj = self.pool.get('resource.calendar')
165 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
166 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
168 res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
169 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
172 default_uom_id = self._get_default_uom_id(cr, uid)
173 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
174 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)
175 dt_end = work_times[-1][1].strftime('%Y-%m-%d')
176 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
178 def write(self, cr, uid, ids, vals, context=None):
179 resource_calendar_obj = self.pool.get('resource.calendar')
180 resource_obj = self.pool.get('resource.resource')
181 uom_obj = self.pool.get('product.uom')
184 res = super(project_phase, self).write(cr, uid, ids, vals, context=context)
185 if context.get('scheduler',False):
187 # Consider calendar and efficiency if the phase is performed by a resource
188 # otherwise consider the project's working calendar
190 #TOCHECK : why need this ?
191 if isinstance(ids, (int, long)):
193 default_uom_id = self._get_default_uom_id(cr, uid)
194 for phase in self.browse(cr, uid, ids, context=context):
195 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
196 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
198 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
202 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
203 # Change the date_start and date_end
204 # for previous and next phases respectively based on valid condition
205 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
206 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
207 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)
209 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
210 for prv_phase in phase.previous_phase_ids:
211 if prv_phase.id == phase.id:
213 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
215 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
216 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
217 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)
219 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
220 for next_phase in phase.next_phase_ids:
221 if next_phase.id == phase.id:
223 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
227 def copy(self, cr, uid, id, default=None, context=None):
230 if not default.get('name', False):
231 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
232 return super(project_phase, self).copy(cr, uid, id, default, context)
234 def set_draft(self, cr, uid, ids, *args):
235 self.write(cr, uid, ids, {'state': 'draft'})
238 def set_open(self, cr, uid, ids, *args):
239 self.write(cr, uid, ids, {'state': 'open'})
242 def set_pending(self, cr, uid, ids, *args):
243 self.write(cr, uid, ids, {'state': 'pending'})
246 def set_cancel(self, cr, uid, ids, *args):
247 self.write(cr, uid, ids, {'state': 'cancelled'})
250 def set_done(self, cr, uid, ids, *args):
251 self.write(cr, uid, ids, {'state': 'done'})
254 def generate_resources(self, cr, uid, ids, context=None):
256 Return a list of Resource Class objects for the resources allocated to the phase.
261 resource_pool = self.pool.get('resource.resource')
262 for phase in self.browse(cr, uid, ids, context=context):
263 user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
264 project = phase.project_id
265 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
266 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
267 res[phase.id] = resource_objs
270 def generate_schedule(self, cr, uid, ids, start_date, calendar_id=False, context=None):
272 Schedule phase with the start date till all the next phases are completed.
273 @param: start_dsate : start date for the phase
274 @param: calendar_id : working calendar of the project
278 resource_pool = self.pool.get('resource.resource')
279 uom_pool = self.pool.get('product.uom')
280 phase_resource = False
283 default_uom_id = self._get_default_uom_id(cr, uid)
284 for phase in self.browse(cr, uid, ids, context=context):
285 if not phase.responsible_id:
286 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
288 phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
289 avg_hours = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
290 duration = str(avg_hours) + 'H'
291 # Create a new project for each phase
294 minimum_time_unit = 1
295 resource = phase_resource_obj
296 # If project has working calendar then that
297 # else the default one would be considered
299 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
300 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
305 project = Task.BalancedProject(Project)
306 s_date = project.phase.start.to_datetime()
307 e_date = project.phase.end.to_datetime()
308 # Recalculate date_start and date_end
309 # according to constraints on date start and date end on phase
310 if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
311 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
314 if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
315 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
316 date_start = phase.constraint_date_end
319 date_start = end_date
320 # Write the calculated dates back
322 ctx.update({'scheduler': True})
323 self.write(cr, uid, [phase.id], {
324 'date_start': start_date.strftime('%Y-%m-%d'),
325 'date_end': end_date.strftime('%Y-%m-%d')
328 # Recursive call till all the next phases scheduled
329 for phase in phase.next_phase_ids:
330 if phase.state in ['draft', 'open', 'pending']:
331 id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
332 self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
337 def schedule_tasks(self, cr, uid, ids, context=None):
339 Schedule the tasks according to resource available and priority.
341 task_pool = self.pool.get('project.task')
342 resource_pool = self.pool.get('resource.resource')
345 resources_list = self.generate_resources(cr, uid, ids, context=context)
347 for phase in self.browse(cr, uid, ids, context=context):
348 start_date = phase.date_start
349 if not start_date and phase.project_id.date_start:
350 start_date = phase.project_id.date_start
352 start_date = datetime.now().strftime("%Y-%m-%d")
353 resources = resources_list.get(phase.id, [])
354 calendar_id = phase.project_id.resource_calendar_id.id
355 task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
357 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
360 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
361 if "warning" not in return_msg:
362 return_msg["warning"] = warning_msg
364 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
368 class project_resource_allocation(osv.osv):
369 _name = 'project.resource.allocation'
370 _description = 'Project Resource Allocation'
371 _rec_name = 'resource_id'
373 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
374 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
375 'phase_id_date_start': fields.related('phase_id', 'date_start', type='date', string='Starting Date of the phase'),
376 'phase_id_date_end': fields.related('phase_id', 'date_end', type='date', string='Ending Date of the phase'),
377 'useability': fields.float('Usability', help="Usability of this resource for this project phase in percentage (=50%)"),
383 project_resource_allocation()
385 class project(osv.osv):
386 _inherit = "project.project"
388 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
389 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
391 def generate_members(self, cr, uid, ids, context=None):
393 Return a list of Resource Class objects for the resources allocated to the phase.
396 resource_pool = self.pool.get('resource.resource')
399 for project in self.browse(cr, uid, ids, context=context):
400 user_ids = map(lambda x:x.id, project.members)
401 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
402 resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
403 res[project.id] = resource_objs
406 def schedule_phases(self, cr, uid, ids, context=None):
412 if type(ids) in (long, int,):
414 phase_pool = self.pool.get('project.phase')
415 for project in self.browse(cr, uid, ids, context=context):
416 phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
417 ('state', 'in', ['draft', 'open', 'pending']),
418 ('previous_phase_ids', '=', False)
420 start_date = project.date_start
422 start_date = datetime.now().strftime("%Y-%m-%d")
423 start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M")
424 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
425 phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context)
428 def schedule_tasks(self, cr, uid, ids, context=None):
430 Schedule the tasks according to resource available and priority.
432 if type(ids) in (long, int,):
434 user_pool = self.pool.get('res.users')
435 task_pool = self.pool.get('project.task')
436 resource_pool = self.pool.get('resource.resource')
440 resources_list = self.generate_members(cr, uid, ids, context=context)
442 for project in self.browse(cr, uid, ids, context=context):
443 start_date = project.date_start
445 start_date = datetime.now().strftime("%Y-%m-%d")
446 resources = resources_list.get(project.id, [])
447 calendar_id = project.resource_calendar_id.id
448 task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
449 ('state', 'in', ['draft', 'open', 'pending'])
454 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
456 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
457 if "warning" not in return_msg:
458 return_msg["warning"] = warning_msg
460 return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
466 class resource_resource(osv.osv):
467 _inherit = "resource.resource"
468 def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
471 if context.get('project_id',False):
472 project_pool = self.pool.get('project.project')
473 project_rec = project_pool.browse(cr, uid, context['project_id'])
474 user_ids = [user_id.id for user_id in project_rec.members]
475 args.append(('user_id','in',user_ids))
476 return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
480 class project_task(osv.osv):
481 _inherit = "project.task"
483 'phase_id': fields.many2one('project.phase', 'Project Phase'),
486 def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
488 Schedule the tasks according to resource available and priority.
494 user_pool = self.pool.get('res.users')
495 project_pool = self.pool.get('project.project')
496 priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
497 # Create dynamic no of tasks with the resource specified
498 def create_tasks(task_number, eff, priorty=500, obj=False):
501 task is a dynamic method!
507 task.__doc__ = "TaskNO%d" %task_number
508 task.__name__ = "task%d" %task_number
511 # Create a 'Faces' project with all the tasks and resources
514 start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
516 resource = reduce(operator.or_, resources)
518 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
519 minimum_time_unit = 1
520 if calendar_id: # If project has working calendar
521 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
522 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
523 # Dynamic creation of tasks
525 for openobect_task in self.browse(cr, uid, ids, context=context):
526 hours = str(openobect_task.planned_hours )+ 'H'
527 if openobect_task.priority in priority_dict.keys():
528 priorty = priority_dict[openobect_task.priority]
529 real_resource = False
530 if openobect_task.user_id:
531 for task_resource in resources:
532 if task_resource.__name__ == task_resource:
533 real_resource = task_resource
536 task = create_tasks(task_number, hours, priorty, real_resource)
540 face_projects = Task.BalancedProject(Project)
542 # Write back the computed dates
543 for face_project in face_projects:
544 s_date = face_project.start.to_datetime()
545 e_date = face_project.end.to_datetime()
548 ctx.update({'scheduler': True})
549 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
550 self.write(cr, uid, [ids[loop_no-1]], {
551 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
552 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
553 'user_id': user_id[0]
559 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: