[IMP] project_long_term: improve gantt view of resource allocation
[odoo/odoo.git] / addons / project_long_term / project_long_term.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
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
27 import operator
28
29 class project_phase(osv.osv):
30     _name = "project.phase"
31     _description = "Project Phase"
32
33     def _check_recursion(self, cr, uid, ids, context=None):
34          if context is None:
35             context = {}
36
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):
42              return False
43          ids = [id for id in prev_ids if id in next_ids]
44          # both prev_ids and next_ids must be unique
45          if ids:
46              return False
47          # unrelated project
48          prev_ids = [rec.id for rec in prev_ids]
49          next_ids = [rec.id for rec in next_ids]
50          # iter prev_ids
51          while prev_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:
55                  return False
56              ids = [id for id in prv_phase_ids if id in next_ids]
57              if ids:
58                  return False
59              prev_ids = prv_phase_ids
60          # iter next_ids
61          while next_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:
65                  return False
66              ids = [id for id in next_phase_ids if id in prev_ids]
67              if ids:
68                  return False
69              next_ids = next_phase_ids
70          return True
71
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']:
75                  return False
76          return True
77
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']:
81              return False
82          return True
83
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']:
87              return False
88          return True
89
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']
94
95     def _compute(self, cr, uid, ids, field_name, arg, context=None):
96         res = {}
97         if not ids:
98             return res
99         for phase in self.browse(cr, uid, ids, context=context):
100             tot = 0.0
101             for task in phase.task_ids:
102                 tot += task.planned_hours
103             res[phase.id] = tot
104         return res
105
106     _columns = {
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'),
125      }
126     _defaults = {
127         'responsible_id': lambda obj,cr,uid,context: uid,
128         'state': 'draft',
129         'sequence': 10,
130         'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
131     }
132     _order = "project_id, date_start, sequence, name"
133     _constraints = [
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']),
136     ]
137
138     def onchange_project(self, cr, uid, ids, project, context=None):
139         result = {}
140         result['date_start'] = False
141         project_obj = self.pool.get('project.project')
142         if project:
143             project_id = project_obj.browse(cr, uid, project, context=context)
144             result['date_start'] = project_id.date_start
145         return {'value': result}
146
147     def onchange_days(self, cr, uid, ids, project, context=None):
148         result = {}
149         for id in ids:
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}
154
155     def _check_date_start(self, cr, uid, phase, date_end, context=None):
156        if context is None:
157             context = {}
158        """
159        Check And Compute date_end of phase if change in date_start < older time.
160        """
161        uom_obj = self.pool.get('product.uom')
162        resource_obj = self.pool.get('resource.resource')
163        cal_obj = self.pool.get('resource.calendar')
164        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
165        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
166        if resource_id:
167             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
168             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
169             if cal_id:
170                 calendar_id = cal_id
171        default_uom_id = self._get_default_uom_id(cr, uid)
172        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
173        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)
174        dt_start = work_times[0][0].strftime('%Y-%m-%d')
175        self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
176
177     def _check_date_end(self, cr, uid, phase, date_start, context=None):
178        if context is None:
179             context = {}
180        """
181        Check And Compute date_end of phase if change in date_end > older time.
182        """
183        uom_obj = self.pool.get('product.uom')
184        resource_obj = self.pool.get('resource.resource')
185        cal_obj = self.pool.get('resource.calendar')
186        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
187        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
188        if resource_id:
189             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
190             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
191             if cal_id:
192                 calendar_id = cal_id
193        default_uom_id = self._get_default_uom_id(cr, uid)
194        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
195        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)
196        dt_end = work_times[-1][1].strftime('%Y-%m-%d')
197        self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
198
199     def write(self, cr, uid, ids, vals, context=None):
200         resource_calendar_obj = self.pool.get('resource.calendar')
201         resource_obj = self.pool.get('resource.resource')
202         uom_obj = self.pool.get('product.uom')
203         if context is None:
204             context = {}
205         res = super(project_phase, self).write(cr, uid, ids, vals, context=context)
206         if context.get('scheduler',False):
207             return res
208         # Consider calendar and efficiency if the phase is performed by a resource
209         # otherwise consider the project's working calendar
210
211         #TOCHECK : why need this ?
212         if isinstance(ids, (int, long)):
213             ids = [ids]
214         default_uom_id = self._get_default_uom_id(cr, uid)
215         for phase in self.browse(cr, uid, ids, context=context):
216             calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
217             resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
218             if resource_id:
219                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
220                 if cal_id:
221                     calendar_id = cal_id
222
223             avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
224             # Change the date_start and date_end
225             # for previous and next phases respectively based on valid condition
226             if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
227                 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
228                 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)
229                 if work_times:
230                     vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
231                 for prv_phase in phase.previous_phase_ids:
232                     if prv_phase.id == phase.id:
233                         continue
234                     self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
235
236             if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
237                 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
238                 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)
239                 if work_times:
240                     vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
241                 for next_phase in phase.next_phase_ids:
242                     if next_phase.id == phase.id:
243                         continue
244                     self._check_date_end(cr, uid, next_phase, dt_end, context=context)
245
246         return res
247
248     def copy(self, cr, uid, id, default=None, context=None):
249         if default is None:
250             default = {}
251         if not default.get('name', False):
252             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
253         return super(project_phase, self).copy(cr, uid, id, default, context)
254
255     def set_draft(self, cr, uid, ids, *args):
256         self.write(cr, uid, ids, {'state': 'draft'})
257         return True
258
259     def set_open(self, cr, uid, ids, *args):
260         self.write(cr, uid, ids, {'state': 'open'})
261         return True
262
263     def set_pending(self, cr, uid, ids, *args):
264         self.write(cr, uid, ids, {'state': 'pending'})
265         return True
266
267     def set_cancel(self, cr, uid, ids, *args):
268         self.write(cr, uid, ids, {'state': 'cancelled'})
269         return True
270
271     def set_done(self, cr, uid, ids, *args):
272         self.write(cr, uid, ids, {'state': 'done'})
273         return True
274
275     def generate_resources(self, cr, uid, ids, context=None):
276         """
277         Return a list of  Resource Class objects for the resources allocated to the phase.
278         """
279         res = {}
280         if context is None:
281             context = {}
282         resource_pool = self.pool.get('resource.resource')
283         for phase in self.browse(cr, uid, ids, context=context):
284             user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
285             project = phase.project_id
286             calendar_id  = project.resource_calendar_id and project.resource_calendar_id.id or False
287             resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
288             res[phase.id] = resource_objs
289         return res
290
291     def generate_schedule(self, cr, uid, ids, start_date, calendar_id=False, context=None):
292         """
293         Schedule phase with the start date till all the next phases are completed.
294         @param: start_dsate : start date for the phase
295         @param: calendar_id : working calendar of the project
296         """
297         if context is None:
298             context = {}
299         resource_pool = self.pool.get('resource.resource')
300         resource_allocation_pool = self.pool.get('project.resource.allocation')
301         uom_pool = self.pool.get('product.uom')
302         if context is None:
303            context = {}
304         default_uom_id = self._get_default_uom_id(cr, uid)
305         for phase in self.browse(cr, uid, ids, context=context):
306             if not phase.responsible_id:
307                 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
308
309             phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
310             avg_hours = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
311             duration = str(avg_hours) + 'H'
312             # Create a new project for each phase
313             def Project():
314                 # If project has working calendar then that
315                 # else the default one would be considered
316                 start = start_date
317                 minimum_time_unit = 1
318                 resource = phase_resource_obj
319                 if calendar_id:
320                     working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
321                     vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
322
323                 def phase():
324                     effort = duration
325
326             project = Task.BalancedProject(Project)
327             s_date = project.phase.start.to_datetime()
328             e_date = project.phase.end.to_datetime()
329             # Recalculate date_start and date_end
330             # according to constraints on date start and date end on phase
331             if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
332                 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
333             else:
334                 start_date = s_date
335             if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
336                 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
337                 date_start = phase.constraint_date_end
338             else:
339                 end_date = e_date
340                 date_start = end_date
341             # Write the calculated dates back
342             ctx = context.copy()
343             ctx.update({'scheduler': True})
344             self.write(cr, uid, [phase.id], {
345                                           'date_start': start_date.strftime('%Y-%m-%d'),
346                                           'date_end': end_date.strftime('%Y-%m-%d')
347                                         }, context=ctx)
348             # write dates into Resources Allocation
349             for resource in phase.resource_ids:
350                 resource_allocation_pool.write(cr, uid, [resource.id], {
351                                         'date_start': start_date.strftime('%Y-%m-%d'),
352                                         'date_end': end_date.strftime('%Y-%m-%d')
353                                     }, context=ctx)
354             # Recursive call till all the next phases scheduled
355             for phase in phase.next_phase_ids:
356                if phase.state in ['draft', 'open', 'pending']:
357                    id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
358                    self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
359                else:
360                    continue
361         return True
362
363     def schedule_tasks(self, cr, uid, ids, context=None):
364         """
365         Schedule the tasks according to resource available and priority.
366         """
367         task_pool = self.pool.get('project.task')
368         resource_pool = self.pool.get('resource.resource')
369         if context is None:
370             context = {}
371         resources_list = self.generate_resources(cr, uid, ids, context=context)
372         return_msg = {}
373         for phase in self.browse(cr, uid, ids, context=context):
374             start_date = phase.date_start
375             if not start_date and phase.project_id.date_start:
376                 start_date = phase.project_id.date_start
377             if not start_date:
378                 start_date = datetime.now().strftime("%Y-%m-%d")
379             resources = resources_list.get(phase.id, [])
380             calendar_id = phase.project_id.resource_calendar_id.id
381             task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
382             if task_ids:
383                 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
384
385             if not task_ids:
386                 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
387                 if "warning" not in return_msg:
388                     return_msg["warning"] =  warning_msg
389                 else:
390                     return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
391         return return_msg
392 project_phase()
393
394 class project_resource_allocation(osv.osv):
395     _name = 'project.resource.allocation'
396     _description = 'Project Resource Allocation'
397     _rec_name = 'resource_id'
398
399     def get_name(self, cr, uid, ids, field_name, arg, context=None):
400         res = {}
401         for allocation in self.browse(cr, uid, ids, context=context):
402             name = allocation.resource_id.name
403             if allocation.user_id:
404                 name = '%s' %(allocation.user_id.name)
405             name += ' (%s%%)' %(allocation.useability)
406             res[allocation.id] = name
407         return res
408     _columns = {
409         'name': fields.function(get_name, method=True, type='char', size=256),
410         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
411         'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
412         'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
413         'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
414         'date_start': fields.date('Start Date', help="Starting Date"),
415         'date_end': fields.date('End Date', help="Ending Date"),
416         'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
417     }
418     _defaults = {
419         'useability': 100,
420     }
421
422 project_resource_allocation()
423
424 class project(osv.osv):
425     _inherit = "project.project"
426     _columns = {
427         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
428         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
429     }
430     def generate_members(self, cr, uid, ids, context=None):
431         """
432         Return a list of  Resource Class objects for the resources allocated to the phase.
433         """
434         res = {}
435         resource_pool = self.pool.get('resource.resource')
436         if context is None:
437             context = {}
438         for project in self.browse(cr, uid, ids, context=context):
439             user_ids = map(lambda x:x.id, project.members)
440             calendar_id  = project.resource_calendar_id and project.resource_calendar_id.id or False
441             resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
442             res[project.id] = resource_objs
443         return res
444
445     def schedule_phases(self, cr, uid, ids, context=None):
446         """
447         Schedule the phases.
448         """
449         if context is None:
450             context = {}
451         if type(ids) in (long, int,):
452             ids = [ids]
453         phase_pool = self.pool.get('project.phase')
454         for project in self.browse(cr, uid, ids, context=context):
455             phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
456                                                   ('state', 'in', ['draft', 'open', 'pending']),
457                                                   ('previous_phase_ids', '=', False)
458                                                   ])
459             start_date = project.date_start
460             if not start_date:
461                 start_date = datetime.now().strftime("%Y-%m-%d")
462             start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M")
463             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
464             phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context)
465         return True
466
467     def schedule_tasks(self, cr, uid, ids, context=None):
468         """
469         Schedule the tasks according to resource available and priority.
470         """
471         if type(ids) in (long, int,):
472             ids = [ids]
473         user_pool = self.pool.get('res.users')
474         task_pool = self.pool.get('project.task')
475         resource_pool = self.pool.get('resource.resource')
476         if context is None:
477             context = {}
478
479         resources_list = self.generate_members(cr, uid, ids, context=context)
480         return_msg = {}
481         for project in self.browse(cr, uid, ids, context=context):
482             start_date = project.date_start
483             if not start_date:
484                 start_date = datetime.now().strftime("%Y-%m-%d")
485             resources = resources_list.get(project.id, [])
486             calendar_id = project.resource_calendar_id.id
487             task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
488                                               ('state', 'in', ['draft', 'open', 'pending'])
489                                               ])
490
491
492             if task_ids:
493                 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
494             else:
495                 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
496                 if "warning" not in return_msg:
497                     return_msg["warning"] =  warning_msg
498                 else:
499                     return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
500
501         return return_msg
502
503 project()
504
505 class resource_resource(osv.osv):
506     _inherit = "resource.resource"
507     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
508         if context is None:
509             context = {}
510         if context.get('project_id',False):
511             project_pool = self.pool.get('project.project')
512             project_rec = project_pool.browse(cr, uid, context['project_id'])
513             user_ids = [user_id.id for user_id in project_rec.members]
514             args.append(('user_id','in',user_ids))
515         return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
516
517 resource_resource()
518
519 class project_task(osv.osv):
520     _inherit = "project.task"
521     _columns = {
522         'phase_id': fields.many2one('project.phase', 'Project Phase'),
523     }
524
525     def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
526         """
527         Schedule the tasks according to resource available and priority.
528         """
529         if not ids:
530             return False
531         if context is None:
532             context = {}
533         user_pool = self.pool.get('res.users')
534         project_pool = self.pool.get('project.project')
535         priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
536         # Create dynamic no of tasks with the resource specified
537         def create_tasks(task_number, eff, priorty=500, obj=False):
538             def task():
539                 """
540                 task is a dynamic method!
541                 """
542                 effort = eff
543                 if obj:
544                     resource = obj
545                 priority = priorty
546             task.__doc__ = "TaskNO%d" %task_number
547             task.__name__ = "task%d" %task_number
548             return task
549
550         # Create a 'Faces' project with all the tasks and resources
551         def Project():
552             title = "Project"
553             start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
554             try:
555                 resource = reduce(operator.or_, resources)
556             except:
557                 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
558             minimum_time_unit = 1
559             if calendar_id:            # If project has working calendar
560                 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
561                 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
562             # Dynamic creation of tasks
563             task_number = 0
564             for openobect_task in self.browse(cr, uid, ids, context=context):
565                 hours = str(openobect_task.planned_hours )+ 'H'
566                 if openobect_task.priority in priority_dict.keys():
567                     priorty = priority_dict[openobect_task.priority]
568                 real_resource = False
569                 if openobect_task.user_id:
570                     for task_resource in resources:
571                         if task_resource.__name__ == task_resource:
572                             real_resource = task_resource
573                             break
574
575                 task = create_tasks(task_number, hours, priorty, real_resource)
576                 task_number += 1
577
578
579         face_projects = Task.BalancedProject(Project)
580         loop_no = 0
581         # Write back the computed dates
582         for face_project in face_projects:
583             s_date = face_project.start.to_datetime()
584             e_date = face_project.end.to_datetime()
585             if loop_no > 0:
586                 ctx = context.copy()
587                 ctx.update({'scheduler': True})
588                 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
589                 self.write(cr, uid, [ids[loop_no-1]], {
590                                                     'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
591                                                     'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
592                                                     'user_id': user_id[0]
593                                                 }, context=ctx)
594
595             loop_no += 1
596         return True
597 project_task()
598 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: