[IMP] project_long_term: improve gantt view
[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     _columns = {
399         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
400         'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
401         'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
402         'user_id': fields.related('resource_id', 'user_id', type='many2one', relation="res.users", string='User'),
403         'date_start': fields.date('Start Date', help="Starting Date"),
404         'date_end': fields.date('End Date', help="Ending Date"),
405         'useability': fields.float('Availability', help="Availability of this resource for this project phase in percentage (=50%)"),
406     }
407     _defaults = {
408         'useability': 100,
409     }
410
411 project_resource_allocation()
412
413 class project(osv.osv):
414     _inherit = "project.project"
415     _columns = {
416         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
417         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
418     }
419     def generate_members(self, cr, uid, ids, context=None):
420         """
421         Return a list of  Resource Class objects for the resources allocated to the phase.
422         """
423         res = {}
424         resource_pool = self.pool.get('resource.resource')
425         if context is None:
426             context = {}
427         for project in self.browse(cr, uid, ids, context=context):
428             user_ids = map(lambda x:x.id, project.members)
429             calendar_id  = project.resource_calendar_id and project.resource_calendar_id.id or False
430             resource_objs = resource_pool.generate_resources(cr, uid, user_ids, calendar_id, context=context)
431             res[project.id] = resource_objs
432         return res
433
434     def schedule_phases(self, cr, uid, ids, context=None):
435         """
436         Schedule the phases.
437         """
438         if context is None:
439             context = {}
440         if type(ids) in (long, int,):
441             ids = [ids]
442         phase_pool = self.pool.get('project.phase')
443         for project in self.browse(cr, uid, ids, context=context):
444             phase_ids = phase_pool.search(cr, uid, [('project_id', '=', project.id),
445                                                   ('state', 'in', ['draft', 'open', 'pending']),
446                                                   ('previous_phase_ids', '=', False)
447                                                   ])
448             start_date = project.date_start
449             if not start_date:
450                 start_date = datetime.now().strftime("%Y-%m-%d")
451             start_dt = datetime.strftime((datetime.strptime(start_date, "%Y-%m-%d")), "%Y-%m-%d %H:%M")
452             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
453             phase_pool.generate_schedule(cr, uid, phase_ids, start_dt, calendar_id, context=context)
454         return True
455
456     def schedule_tasks(self, cr, uid, ids, context=None):
457         """
458         Schedule the tasks according to resource available and priority.
459         """
460         if type(ids) in (long, int,):
461             ids = [ids]
462         user_pool = self.pool.get('res.users')
463         task_pool = self.pool.get('project.task')
464         resource_pool = self.pool.get('resource.resource')
465         if context is None:
466             context = {}
467
468         resources_list = self.generate_members(cr, uid, ids, context=context)
469         return_msg = {}
470         for project in self.browse(cr, uid, ids, context=context):
471             start_date = project.date_start
472             if not start_date:
473                 start_date = datetime.now().strftime("%Y-%m-%d")
474             resources = resources_list.get(project.id, [])
475             calendar_id = project.resource_calendar_id.id
476             task_ids = task_pool.search(cr, uid, [('project_id', '=', project.id),
477                                               ('state', 'in', ['draft', 'open', 'pending'])
478                                               ])
479
480
481             if task_ids:
482                 task_pool.generate_schedule(cr, uid, task_ids, resources, calendar_id, start_date, context=context)
483             else:
484                 warning_msg = _("No tasks to compute for Project '%s'.") % (project.name)
485                 if "warning" not in return_msg:
486                     return_msg["warning"] =  warning_msg
487                 else:
488                     return_msg["warning"] = return_msg["warning"] + "\n" + warning_msg
489
490         return return_msg
491
492 project()
493
494 class resource_resource(osv.osv):
495     _inherit = "resource.resource"
496     def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
497         if context is None:
498             context = {}
499         if context.get('project_id',False):
500             project_pool = self.pool.get('project.project')
501             project_rec = project_pool.browse(cr, uid, context['project_id'])
502             user_ids = [user_id.id for user_id in project_rec.members]
503             args.append(('user_id','in',user_ids))
504         return super(resource_resource, self).search(cr, uid, args, offset, limit, order, context, count)
505
506 resource_resource()
507
508 class project_task(osv.osv):
509     _inherit = "project.task"
510     _columns = {
511         'phase_id': fields.many2one('project.phase', 'Project Phase'),
512     }
513
514     def generate_schedule(self, cr, uid, ids, resources, calendar_id, start_date, context=None):
515         """
516         Schedule the tasks according to resource available and priority.
517         """
518         if not ids:
519             return False
520         if context is None:
521             context = {}
522         user_pool = self.pool.get('res.users')
523         project_pool = self.pool.get('project.project')
524         priority_dict = {'0': 1000, '1': 800, '2': 500, '3': 300, '4': 100}
525         # Create dynamic no of tasks with the resource specified
526         def create_tasks(task_number, eff, priorty=500, obj=False):
527             def task():
528                 """
529                 task is a dynamic method!
530                 """
531                 effort = eff
532                 if obj:
533                     resource = obj
534                 priority = priorty
535             task.__doc__ = "TaskNO%d" %task_number
536             task.__name__ = "task%d" %task_number
537             return task
538
539         # Create a 'Faces' project with all the tasks and resources
540         def Project():
541             title = "Project"
542             start = datetime.strftime(datetime.strptime(start_date, "%Y-%m-%d"), "%Y-%m-%d %H:%M")
543             try:
544                 resource = reduce(operator.or_, resources)
545             except:
546                 raise osv.except_osv(_('Error'), _('Should have Resources Allocation or Project Members!'))
547             minimum_time_unit = 1
548             if calendar_id:            # If project has working calendar
549                 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
550                 vacation = tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context))
551             # Dynamic creation of tasks
552             task_number = 0
553             for openobect_task in self.browse(cr, uid, ids, context=context):
554                 hours = str(openobect_task.planned_hours )+ 'H'
555                 if openobect_task.priority in priority_dict.keys():
556                     priorty = priority_dict[openobect_task.priority]
557                 real_resource = False
558                 if openobect_task.user_id:
559                     for task_resource in resources:
560                         if task_resource.__name__ == task_resource:
561                             real_resource = task_resource
562                             break
563
564                 task = create_tasks(task_number, hours, priorty, real_resource)
565                 task_number += 1
566
567
568         face_projects = Task.BalancedProject(Project)
569         loop_no = 0
570         # Write back the computed dates
571         for face_project in face_projects:
572             s_date = face_project.start.to_datetime()
573             e_date = face_project.end.to_datetime()
574             if loop_no > 0:
575                 ctx = context.copy()
576                 ctx.update({'scheduler': True})
577                 user_id = user_pool.search(cr, uid, [('name', '=', face_project.booked_resource[0].__name__)])
578                 self.write(cr, uid, [ids[loop_no-1]], {
579                                                     'date_start': s_date.strftime('%Y-%m-%d %H:%M:%S'),
580                                                     'date_end': e_date.strftime('%Y-%m-%d %H:%M:%S'),
581                                                     'user_id': user_id[0]
582                                                 }, context=ctx)
583
584             loop_no += 1
585         return True
586 project_task()
587 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: