diff options
| author | uco (OpenERP) <uco@tinyerp.com> | 2010-11-19 11:28:40 +0530 |
|---|---|---|
| committer | uco (OpenERP) <uco@tinyerp.com> | 2010-11-19 11:28:40 +0530 |
| commit | 58e9ba97bbe188438cb67c1f59292f9b4d1de1c5 (patch) | |
| tree | 21469bc8258abbf4220aedd222b42c3efb56b081 /addons/project_long_term/project_long_term.py | |
| parent | 8a3839601d508dfe70dc5cbc56a0c75bf8ec1aad (diff) | |
| parent | 1422edad60eab6740b114b2fea5655192e37c6a2 (diff) | |
[MERGE]: Merged with trunk-addons.
bzr revid: uco@tinyerp.com-20101119055840-q038qakewjz72oq8
Diffstat (limited to 'addons/project_long_term/project_long_term.py')
| -rw-r--r-- | addons/project_long_term/project_long_term.py | 372 |
1 files changed, 348 insertions, 24 deletions
diff --git a/addons/project_long_term/project_long_term.py b/addons/project_long_term/project_long_term.py index 04fde75342e..faea0916906 100644 --- a/addons/project_long_term/project_long_term.py +++ b/addons/project_long_term/project_long_term.py @@ -19,11 +19,12 @@ # ############################################################################## -import time -from datetime import date, datetime, timedelta - +from datetime import datetime +from dateutil.relativedelta import relativedelta from tools.translate import _ from osv import fields, osv +from resource.faces import task as Task +import operator class project_phase(osv.osv): _name = "project.phase" @@ -91,12 +92,23 @@ class project_phase(osv.osv): model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour') return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id'] + def _compute(self, cr, uid, ids, field_name, arg, context=None): + res = {} + if not ids: + return res + for phase in self.browse(cr, uid, ids, context=context): + tot = 0.0 + for task in phase.task_ids: + tot += task.planned_hours + res[phase.id] = tot + return res + _columns = { 'name': fields.char("Name", size=64, required=True), 'date_start': fields.date('Start Date', help="Starting Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'date_end': fields.date('End Date', help="Ending Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'constraint_date_start': fields.date('Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'constraint_date_end': fields.date('End Date', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), + 'constraint_date_start': fields.date('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), + 'constraint_date_end': fields.date('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'project_id': fields.many2one('project.project', 'Project', required=True), 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}), 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}), @@ -108,7 +120,8 @@ class project_phase(osv.osv): 'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True, 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.\ - \n If the phase is over, the states is set to \'Done\'.') + \n If the phase is over, the states is set to \'Done\'.'), + 'total_hours': fields.function(_compute, method=True, string='Total Hours'), } _defaults = { 'responsible_id': lambda obj,cr,uid,context: uid, @@ -116,7 +129,7 @@ class project_phase(osv.osv): 'sequence': 10, 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0] } - _order = "name" + _order = "project_id, date_start, sequence, name" _constraints = [ (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']), (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']), @@ -131,6 +144,14 @@ class project_phase(osv.osv): result['date_start'] = project_id.date_start return {'value': result} + def onchange_days(self, cr, uid, ids, project, context=None): + result = {} + for id in ids: + project_id = self.browse(cr, uid, id, context=context) + newdate = datetime.strptime(project_id.date_start, '%Y-%m-%d') + relativedelta(days=project_id.duration or 0.0) + result['date_end'] = newdate.strftime('%Y-%m-%d') + return {'value': result} + def _check_date_start(self, cr, uid, phase, date_end, context=None): if context is None: context = {} @@ -181,38 +202,55 @@ class project_phase(osv.osv): uom_obj = self.pool.get('product.uom') if context is None: context = {} + res = super(project_phase, self).write(cr, uid, ids, vals, context=context) if context.get('scheduler',False): - return super(project_phase, self).write(cr, uid, ids, vals, context=context) + return res # Consider calendar and efficiency if the phase is performed by a resource # otherwise consider the project's working calendar + + #TOCHECK : why need this ? if isinstance(ids, (int, long)): ids = [ids] - phase = self.browse(cr, uid, ids[0], context=context) - calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False - resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context) - if resource_id: + default_uom_id = self._get_default_uom_id(cr, uid) + for phase in self.browse(cr, uid, ids, context=context): + calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False + resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context) + if resource_id: cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id if cal_id: calendar_id = cal_id - default_uom_id = self._get_default_uom_id(cr, uid) - avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id) - # Change the date_start and date_end - # for previous and next phases respectively based on valid condition - if vals.get('date_start', False) and vals['date_start'] < phase.date_start: + + avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id) + # Change the date_start and date_end + # for previous and next phases respectively based on valid condition + if vals.get('date_start', False) and vals['date_start'] < phase.date_start: dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d') 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) if work_times: vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d') for prv_phase in phase.previous_phase_ids: + if prv_phase.id == phase.id: + continue self._check_date_start(cr, uid, prv_phase, dt_start, context=context) - if vals.get('date_end', False) and vals['date_end'] > phase.date_end: + + if vals.get('date_end', False) and vals['date_end'] > phase.date_end: dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d') 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) if work_times: vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d') for next_phase in phase.next_phase_ids: + if next_phase.id == phase.id: + continue self._check_date_end(cr, uid, next_phase, dt_end, context=context) - return super(project_phase, self).write(cr, uid, ids, vals, context=context) + + return res + + def copy(self, cr, uid, id, default=None, context=None): + if default is None: + default = {} + if not default.get('name', False): + default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)') + return super(project_phase, self).copy(cr, uid, id, default, context) def set_draft(self, cr, uid, ids, *args): self.write(cr, uid, ids, {'state': 'draft'}) @@ -234,18 +272,146 @@ class project_phase(osv.osv): self.write(cr, uid, ids, {'state': 'done'}) return True + def generate_resources(self, cr, uid, ids, context=None): + """ + Return a list of Resource Class objects for the resources allocated to the phase. + """ + res = {} + if context is None: + context = {} + resource_pool = self.pool.get('resource.resource') + for phase in self.browse(cr, uid, ids, context=context): + user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids) + project = phase.project_id + calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False + resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context) + res[phase.id] = resource_objs + return res + + def generate_schedule(self, cr, uid, ids, start_date, calendar_id=False, context=None): + """ + Schedule phase with the start date till all the next phases are completed. + @param: start_dsate : start date for the phase + @param: calendar_id : working calendar of the project + """ + if context is None: + context = {} + resource_pool = self.pool.get('resource.resource') + resource_allocation_pool = self.pool.get('project.resource.allocation') + uom_pool = self.pool.get('product.uom') + if context is None: + context = {} + default_uom_id = self._get_default_uom_id(cr, uid) + for phase in self.browse(cr, uid, ids, context=context): + if not phase.responsible_id: + raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,)) + + phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context) + avg_hours = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id) + duration = str(avg_hours) + 'H' + # Create a new project for each phase + def Project(): + # If project has working calendar then that + # else the default one would be considered + start = start_date + minimum_time_unit = 1 + resource = phase_resource_obj + if calendar_id: + working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context) + vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id)) + + def phase(): + effort = duration + + project = Task.BalancedProject(Project) + s_date = project.phase.start.to_datetime() + e_date = project.phase.end.to_datetime() + # Recalculate date_start and date_end + # according to constraints on date start and date end on phase + if phase.constraint_date_start and str(s_date) < phase.constraint_date_start: + start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d') + else: + start_date = s_date + if phase.constraint_date_end and str(e_date) > phase.constraint_date_end: + end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d') + date_start = phase.constraint_date_end + else: + end_date = e_date + date_start = end_date + # Write the calculated dates back + ctx = context.copy() + ctx.update({'scheduler': True}) + self.write(cr, uid, [phase.id], { + 'date_start': start_date.strftime('%Y-%m-%d'), + 'date_end': end_date.strftime('%Y-%m-%d') + }, context=ctx) + # write dates into Resources Allocation + for resource in phase.resource_ids: + resource_allocation_pool.write(cr, uid, [resource.id], { + 'date_start': start_date.strftime('%Y-%m-%d'), + 'date_end': end_date.strftime('%Y-%m-%d') + }, context=ctx) + # Recursive call till all the next phases scheduled + for phase in phase.next_phase_ids: + if phase.state in ['draft', 'open', 'pending']: + id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False + self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context) + else: + continue + return True + + def schedule_tasks(self, cr, uid, ids, context=None): + """ + Schedule the tasks according to resource available and priority. + """ + task_pool = self.pool.get('project.task') + resource_pool = self.pool.get('resource.resource') + if context is None: + context = {} + resources_list = self.generate_resources(cr, uid, ids, context=context) + return_msg = {} + for phase in self.browse(cr, uid, ids, context=context): + start_date = phase.date_start + if not start_date and phase.project_id.date_start: + start_date = phase.project_id.date_start + if not start_date: + start_date = datetime.now().strftime("%Y-%m-%d") + resources = resources_list.get(phase.id, []) + calendar_id = phase.project_id.resource_calendar_id.id + task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids))) + if task_ids: + task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context) + + if not task_ids: + warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name) + if "warning" not in return_msg: + return_msg["warning"] = warning_msg + else: + return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg + return return_msg project_phase() class project_resource_allocation(osv.osv): _name = 'project.resource.allocation' _description = 'Project Resource Allocation' _rec_name = 'resource_id' + + def get_name(self, cr, uid, ids, field_name, arg, context=None): + res = {} + for allocation in self.browse(cr, uid, ids, context=context): + name = allocation.phase_id.name + name += ' (%s%%)' %(allocation.useability) + res[allocation.id] = name + return res _columns = { + 'name': fields.function(get_name, method=True, type='char', size=256), 'resource_id': fields.many2one('resource.resource', 'Resource', required=True), 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True), - 'phase_id_date_start': fields.related('phase_id', 'date_start', type='date', string='Starting Date of the phase'), - 'phase_id_date_end': fields.related('phase_id', 'date_end', type='date', string='Ending Date of the phase'), - 'useability': fields.float('Usability', help="Usability of this resource for this project phase in percentage (=50%)"), + 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True), + 'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'), + 'date_start': fields.date('Start Date', help="Starting Date"), + 'date_end': fields.date('End Date', help="Ending Date"), + 'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"), } _defaults = { 'useability': 100, @@ -259,14 +425,172 @@ class project(osv.osv): 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"), 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ), } + def generate_members(self, cr, uid, ids, context=None): + """ + Return a list of Resource Class objects for the resources allocated to the phase. + """ + res = {} + resource_pool = self.pool.get('resource.resource') + if context is None: + context = {} + for project in self.browse(cr, uid, ids, context=context): + user_ids = map(lambda x:x.id, project.members) + calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False + resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context) + res[project.id] = resource_objs + return res + + def schedule_phases(self, cr, uid, ids, context=None): + """ + Schedule the phases. + """ + if context is None: + context = {} + if type(ids) in (long, int,): + ids = [ids] + phase_pool = self.pool.get('project.phase') + for project in self.browse(cr, uid, ids, context=context): + phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id), + ('state', 'in', ['draft', 'open', 'pending']), + ('previous_phase_ids', '=', False) + ]) + start_date = project.date_start + if not start_date: + start_date = datetime.now().strftime("%Y-%m-%d") + start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M") + calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False + phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context) + return True + + def schedule_tasks(self, cr, uid, ids, context=None): + """ + Schedule the tasks according to resource available and priority. + """ + if type(ids) in (long, int,): + ids = [ids] + user_pool = self.pool.get('res.users') + task_pool = self.pool.get('project.task') + resource_pool = self.pool.get('resource.resource') + if context is None: + context = {} + + resources_list = self.generate_members(cr, uid, ids, context=context) + return_msg = {} + for project in self.browse(cr, uid, ids, context=context): + start_date = project.date_start + if not start_date: + start_date = datetime.now().strftime("%Y-%m-%d") + resources = resources_list.get(project.id, []) + calendar_id = project.resource_calendar_id.id + task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id), + ('state', 'in', ['draft', 'open', 'pending']) + ]) + + + if task_ids: + task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context) + else: + warning_msg = _("No tasks to compute for Project '%s'.") % (project.name) + if "warning" not in return_msg: + return_msg["warning"] = warning_msg + else: + return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg + + return return_msg project() -class task(osv.osv): +class resource_resource(osv.osv): + _inherit = "resource.resource" + def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): + if context is None: + context = {} + if context.get('project_id',False): + project_pool = self.pool.get('project.project') + project_rec = project_pool.browse(cr, uid, context['project_id']) + user_ids = [user_id.id for user_id in project_rec.members] + args.append(('user_id','in',user_ids)) + return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count) + +resource_resource() + +class project_task(osv.osv): _inherit = "project.task" _columns = { 'phase_id': fields.many2one('project.phase', 'Project Phase'), } -task() + def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None): + """ + Schedule the tasks according to resource available and priority. + """ + if not ids: + return False + if context is None: + context = {} + user_pool = self.pool.get('res.users') + project_pool = self.pool.get('project.project') + priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100} + # Create dynamic no of tasks with the resource specified + def create_tasks(task_number, eff, priorty=500, obj=False): + def task(): + """ + task is a dynamic method! + """ + effort = eff + if obj: + resource = obj + priority = priorty + task.__doc__ = "TaskNO%d" %task_number + task.__name__ = "task%d" %task_number + return task + + # Create a 'Faces' project with all the tasks and resources + def Project(): + title = "Project" + start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M") + try: + resource = reduce(operator.or_, resources) + except: + raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!')) + minimum_time_unit = 1 + if calendar_id: # If project has working calendar + working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context) + vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) + # Dynamic creation of tasks + task_number = 0 + for openobect_task in self.browse(cr, uid, ids, context=context): + hours = str(openobect_task.planned_hours )+ 'H' + if openobect_task.priority in priority_dict.keys(): + priorty = priority_dict[openobect_task.priority] + real_resource = False + if openobect_task.user_id: + for task_resource in resources: + if task_resource.__name__ == task_resource: + real_resource = task_resource + break + + task = create_tasks(task_number, hours, priorty, real_resource) + task_number += 1 + + + face_projects = Task.BalancedProject(Project) + loop_no = 0 + # Write back the computed dates + for face_project in face_projects: + s_date = face_project.start.to_datetime() + e_date = face_project.end.to_datetime() + if loop_no > 0: + ctx = context.copy() + ctx.update({'scheduler': True}) + user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)]) + self.write(cr, uid, [ids[loop_no-1]], { + 'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'), + 'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'), + 'user_id': user_id[0] + }, context=ctx) + + loop_no += 1 + return True +project_task() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
