[FIX] project_long_term: scheduling phase: if calendar is not specify, take 24/7...
[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        """
157        Check And Compute date_end of phase if change in date_start < older time.
158        """
159        uom_obj = self.pool.get('product.uom')
160        resource_obj = self.pool.get('resource.resource')
161        cal_obj = self.pool.get('resource.calendar')
162        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
163        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
164        if resource_id:
165             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
166             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
167             if cal_id:
168                 calendar_id = cal_id
169        default_uom_id = self._get_default_uom_id(cr, uid)
170        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
171        work_times = cal_obj.interval_min_get(cr, uid, calendar_id, date_end, avg_hours or 0.0, resource_id and resource_id[0] or False)
172        dt_start = work_times[0][0].strftime('%Y-%m-%d')
173        self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
174
175     def _check_date_end(self, cr, uid, phase, date_start, context=None):
176        """
177        Check And Compute date_end of phase if change in date_end > older time.
178        """
179        uom_obj = self.pool.get('product.uom')
180        resource_obj = self.pool.get('resource.resource')
181        cal_obj = self.pool.get('resource.calendar')
182        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
183        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
184        if resource_id:
185             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
186             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
187             if cal_id:
188                 calendar_id = cal_id
189        default_uom_id = self._get_default_uom_id(cr, uid)
190        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
191        work_times = cal_obj.interval_get(cr, uid, calendar_id, date_start, avg_hours or 0.0, resource_id and resource_id[0] or False)
192        dt_end = work_times[-1][1].strftime('%Y-%m-%d')
193        self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
194
195     def write(self, cr, uid, ids, vals, context=None):
196         resource_calendar_obj = self.pool.get('resource.calendar')
197         resource_obj = self.pool.get('resource.resource')
198         uom_obj = self.pool.get('product.uom')
199         if context is None:
200             context = {}
201         res = super(project_phase, self).write(cr, uid, ids, vals, context=context)
202         if context.get('scheduler',False):
203             return res
204         # Consider calendar and efficiency if the phase is performed by a resource
205         # otherwise consider the project's working calendar
206
207         #TOCHECK : why need this ?
208         if isinstance(ids, (int, long)):
209             ids = [ids]
210         default_uom_id = self._get_default_uom_id(cr, uid)
211         for phase in self.browse(cr, uid, ids, context=context):
212             calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
213             resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
214             if resource_id:
215                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
216                 if cal_id:
217                     calendar_id = cal_id
218
219             avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
220             # Change the date_start and date_end
221             # for previous and next phases respectively based on valid condition
222             if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
223                 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
224                 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)
225                 if work_times:
226                     vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
227                 for prv_phase in phase.previous_phase_ids:
228                     if prv_phase.id == phase.id:
229                         continue
230                     self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
231
232             if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
233                 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
234                 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)
235                 if work_times:
236                     vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
237                 for next_phase in phase.next_phase_ids:
238                     if next_phase.id == phase.id:
239                         continue
240                     self._check_date_end(cr, uid, next_phase, dt_end, context=context)
241
242         return res
243
244     def copy(self, cr, uid, id, default=None, context=None):
245         if default is None:
246             default = {}
247         if not default.get('name', False):
248             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
249         return super(project_phase, self).copy(cr, uid, id, default, context)
250
251     def set_draft(self, cr, uid, ids, *args):
252         self.write(cr, uid, ids, {'state': 'draft'})
253         return True
254
255     def set_open(self, cr, uid, ids, *args):
256         self.write(cr, uid, ids, {'state': 'open'})
257         return True
258
259     def set_pending(self, cr, uid, ids, *args):
260         self.write(cr, uid, ids, {'state': 'pending'})
261         return True
262
263     def set_cancel(self, cr, uid, ids, *args):
264         self.write(cr, uid, ids, {'state': 'cancelled'})
265         return True
266
267     def set_done(self, cr, uid, ids, *args):
268         self.write(cr, uid, ids, {'state': 'done'})
269         return True
270
271     def generate_resources(self, cr, uid, ids, context=None):
272         """
273         Return a list of  Resource Class objects for the resources allocated to the phase.
274         """
275         res = {}
276         resource_pool = self.pool.get('resource.resource')
277         for phase in self.browse(cr, uid, ids, context=context):
278             user_ids = map(lambda x:x.resource_id.user_id.id, phase.resource_ids)
279             project = phase.project_id
280             calendar_id  = project.resource_calendar_id and project.resource_calendar_id.id or False
281             resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
282             res[phase.id] = resource_objs
283         return res
284
285     def generate_schedule(self, cr, uid, ids, start_date=False, calendar_id=False, context=None):
286         """
287         Schedule phase with the start date till all the next phases are completed.
288         @param: start_date (datetime.datetime) : start date for the phase. It would be either Start date of phase or start date of project or system current date
289         @param: calendar_id : working calendar of the project
290         """
291         if context is None:
292             context = {}
293         resource_pool = self.pool.get('resource.resource')
294         data_pool = self.pool.get('ir.model.data')
295         resource_allocation_pool = self.pool.get('project.resource.allocation')
296         uom_pool = self.pool.get('product.uom')
297         data_model, day_uom_id = data_pool.get_object_reference(cr, uid, 'product', 'uom_day')
298         for phase in self.browse(cr, uid, ids, context=context):
299             if not phase.responsible_id:
300                 raise osv.except_osv(_('No responsible person assigned !'),_("You must assign a responsible person for phase '%s' !") % (phase.name,))
301
302             if not start_date:
303                 start_date = phase.project_id.date_start or phase.date_start or datetime.now().strftime("%Y-%m-%d")
304                 start_date = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d") 
305             phase_resource_obj = resource_pool.generate_resources(cr, uid, [phase.responsible_id.id], calendar_id, context=context)
306             avg_days = uom_pool._compute_qty(cr, uid, phase.product_uom.id, phase.duration, day_uom_id)
307             duration = str(avg_days) + 'd'
308             # Create a new project for each phase
309             def Project():
310                 # If project has working calendar then that
311                 # else the default one would be considered
312                 start = start_date
313                 minimum_time_unit = 1
314                 resource = phase_resource_obj
315                 working_hours_per_day = 24
316                 vacation = []
317                 if calendar_id:
318                     working_hours_per_day = 8 #TODO: it should be come from calendars
319                     vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id))
320                 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
321                 def phase():
322                     effort = duration
323
324             project = Task.BalancedProject(Project)
325
326             s_date = project.phase.start.to_datetime()
327             e_date = project.phase.end.to_datetime()
328             # Recalculate date_start and date_end
329             # according to constraints on date start and date end on phase
330             if phase.constraint_date_start and str(s_date) < phase.constraint_date_start:
331                 start_date = datetime.strptime(phase.constraint_date_start, '%Y-%m-%d')
332             else:
333                 start_date = s_date
334             if phase.constraint_date_end and str(e_date) > phase.constraint_date_end:
335                 end_date= datetime.strptime(phase.constraint_date_end, '%Y-%m-%d')
336                 date_start = phase.constraint_date_end
337             else:
338                 end_date = e_date
339                 date_start = end_date
340             # Write the calculated dates back
341             ctx = context.copy()
342             ctx.update({'scheduler': True})
343             self.write(cr, uid, [phase.id], {
344                                           'date_start': start_date.strftime('%Y-%m-%d'),
345                                           'date_end': end_date.strftime('%Y-%m-%d')
346                                         }, context=ctx)
347             # write dates into Resources Allocation
348             for resource in phase.resource_ids:
349                 resource_allocation_pool.write(cr, uid, [resource.id], {
350                                         'date_start': start_date.strftime('%Y-%m-%d'),
351                                         'date_end': end_date.strftime('%Y-%m-%d')
352                                     }, context=ctx)
353             # Recursive call till all the next phases scheduled
354             for phase in phase.next_phase_ids:
355                if phase.state in ['draft', 'open', 'pending']:
356                    id_cal = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
357                    self.generate_schedule(cr, uid, [phase.id], date_start, id_cal, context=context)
358                else:
359                    continue
360         return True
361
362     def schedule_tasks(self, cr, uid, ids, context=None):
363         """
364         Schedule the tasks according to resource available and priority.
365         """
366         task_pool = self.pool.get('project.task')
367         resource_pool = self.pool.get('resource.resource')
368         resources_list = self.generate_resources(cr, uid, ids, context=context)
369         return_msg = {}
370         for phase in self.browse(cr, uid, ids, context=context):
371             start_date = phase.date_start
372             if not start_date and phase.project_id.date_start:
373                 start_date = phase.project_id.date_start
374             if not start_date:
375                 start_date = datetime.now().strftime("%Y-%m-%d")
376             resources = resources_list.get(phase.id, [])
377             calendar_id = phase.project_id.resource_calendar_id.id
378             task_ids = map(lambda x : x.id, (filter(lambda x : x.state in ['open', 'draft', 'pending'] , phase.task_ids)))
379             if task_ids:
380                 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
381
382             if not task_ids:
383                 warning_msg = _("No tasks to compute for Phase '%s'.") % (phase.name)
384                 if "warning" not in return_msg:
385                     return_msg["warning"] =  warning_msg
386                 else:
387                     return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
388         return return_msg
389 project_phase()
390
391 class project_resource_allocation(osv.osv):
392     _name = 'project.resource.allocation'
393     _description = 'Project Resource Allocation'
394     _rec_name = 'resource_id'
395
396     def get_name(self, cr, uid, ids, field_name, arg, context=None):
397         res = {}
398         for allocation in self.browse(cr, uid, ids, context=context):
399             name = allocation.phase_id.name
400             name += ' (%s%%)' %(allocation.useability)
401             res[allocation.id] = name
402         return res
403     _columns = {
404         'name': fields.function(get_name, method=True, type='char', size=256),
405         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
406         'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
407         'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
408         'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
409         'date_start': fields.date('Start Date', help="Starting Date"),
410         'date_end': fields.date('End Date', help="Ending Date"),
411         'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
412     }
413     _defaults = {
414         'useability': 100,
415     }
416
417 project_resource_allocation()
418
419 class project(osv.osv):
420     _inherit = "project.project"
421     _columns = {
422         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
423         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
424     }
425     def generate_members(self, cr, uid, ids, context=None):
426         """
427         Return a list of  Resource Class objects for the resources allocated to the phase.
428         """
429         res = {}
430         resource_pool = self.pool.get('resource.resource')
431         for project in self.browse(cr, uid, ids, context=context):
432             user_ids = map(lambda x:x.id, project.members)
433             calendar_id  = project.resource_calendar_id and project.resource_calendar_id.id or False
434             resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
435             res[project.id] = resource_objs
436         return res
437
438     def schedule_phases(self, cr, uid, ids, context=None):
439         """
440         Schedule the phases.
441         """
442         if type(ids) in (long, int,):
443             ids = [ids]
444         phase_pool = self.pool.get('project.phase')
445         for project in self.browse(cr, uid, ids, context=context):
446             phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
447                                                   ('state', 'in', ['draft', 'open', 'pending']),
448                                                   ('previous_phase_ids', '=', False)
449                                                   ])
450             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
451             start_date = False
452             phase_pool.generate_schedule(cr, uid, phase_ids, start_date, calendar_id, context=context)
453         return True
454
455     def schedule_tasks(self, cr, uid, ids, context=None):
456         """
457         Schedule the tasks according to resource available and priority.
458         """
459         if type(ids) in (long, int,):
460             ids = [ids]
461         user_pool = self.pool.get('res.users')
462         task_pool = self.pool.get('project.task')
463         resource_pool = self.pool.get('resource.resource')
464         resources_list = self.generate_members(cr, uid, ids, context=context)
465         return_msg = {}
466         for project in self.browse(cr, uid, ids, context=context):
467             start_date = project.date_start
468             if not start_date:
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'])
474                                               ])
475
476
477             if task_ids:
478                 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
479             else:
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
483                 else:
484                     return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
485
486         return return_msg
487
488 project()
489
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):
493         if context is None:
494             context = {}
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'], context=context)
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)
501
502 resource_resource()
503
504 class project_task(osv.osv):
505     _inherit = "project.task"
506     _columns = {
507         'phase_id': fields.many2one('project.phase', 'Project Phase'),
508     }
509
510     def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
511         """
512         Schedule the tasks according to resource available and priority.
513         """
514         if not ids:
515             return False
516         if context is None:
517             context = {}
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):
523             def task():
524                 """
525                 task is a dynamic method!
526                 """
527                 effort = eff
528                 if obj:
529                     resource = obj
530                 priority = priorty
531             task.__doc__ = "TaskNO%d" %task_number
532             task.__name__ = "task%d" %task_number
533             return task
534
535         # Create a 'Faces' project with all the tasks and resources
536         def Project():
537             title = "Project"
538             start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
539             try:
540                 resource = reduce(operator.or_, resources)
541             except:
542                 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
543             minimum_time_unit = 1
544             working_hours_per_day = 24
545             vacation = []
546             if calendar_id:
547                 working_hours_per_day = 8 #TODO: it should be come from calendars
548                 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
549             working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
550             # Dynamic creation of tasks
551             task_number = 0
552             for openobect_task in self.browse(cr, uid, ids, context=context):
553                 hours = str(openobect_task.planned_hours )+ 'H'
554                 if openobect_task.priority in priority_dict.keys():
555                     priorty = priority_dict[openobect_task.priority]
556                 real_resource = False
557                 if openobect_task.user_id:
558                     for task_resource in resources:
559                         if task_resource.__name__ == task_resource:
560                             real_resource = task_resource
561                             break
562
563                 task = create_tasks(task_number, hours, priorty, real_resource)
564                 task_number += 1
565
566
567         face_projects = Task.BalancedProject(Project)
568         loop_no = 0
569         # Write back the computed dates
570         for face_project in face_projects:
571             s_date = face_project.start.to_datetime()
572             e_date = face_project.end.to_datetime()
573             if loop_no > 0:
574                 ctx = context.copy()
575                 ctx.update({'scheduler': True})
576                 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
577                 self.write(cr, uid, [ids[loop_no-1]], {
578                                                     'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
579                                                     'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
580                                                     'user_id': user_id[0]
581                                                 }, context=ctx)
582
583             loop_no += 1
584         return True
585 project_task()
586 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: