1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import datetime
23 from tools.translate import _
24 from osv import fields, osv
25 from resource.faces import task as Task
26 from operator import itemgetter
28 class project_phase(osv.osv):
29 _name = "project.phase"
30 _description = "Project Phase"
32 def _check_recursion(self, cr, uid, ids, context=None):
36 data_phase = self.browse(cr, uid, ids[0], context=context)
37 prev_ids = data_phase.previous_phase_ids
38 next_ids = data_phase.next_phase_ids
39 # it should neither be in prev_ids nor in next_ids
40 if (data_phase in prev_ids) or (data_phase in next_ids):
42 ids = [id for id in prev_ids if id in next_ids]
43 # both prev_ids and next_ids must be unique
47 prev_ids = [rec.id for rec in prev_ids]
48 next_ids = [rec.id for rec in next_ids]
51 cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
52 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
53 if data_phase.id in prv_phase_ids:
55 ids = [id for id in prv_phase_ids if id in next_ids]
58 prev_ids = prv_phase_ids
61 cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
62 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
63 if data_phase.id in next_phase_ids:
65 ids = [id for id in next_phase_ids if id in prev_ids]
68 next_ids = next_phase_ids
71 def _check_dates(self, cr, uid, ids, context=None):
72 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
73 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
77 def _get_default_uom_id(self, cr, uid):
78 model_data_obj = self.pool.get('ir.model.data')
79 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
80 return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
82 def _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
86 for phase in self.browse(cr, uid, ids, context=context):
87 if phase.state=='done':
90 elif phase.state=="cancelled":
93 elif not phase.task_ids:
98 for task in phase.task_ids:
99 tot += task.total_hours
100 done += min(task.effective_hours, task.total_hours)
105 res[phase.id] = round(100.0 * done / tot, 2)
109 'name': fields.char("Name", size=64, required=True),
110 'date_start': fields.datetime('Start Date', help="It's computed by the scheduler according the project date or the end date of the previous phase.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
111 'date_end': fields.datetime('End Date', help=" It's computed by the scheduler according to the start date and the duration.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
112 'constraint_date_start': fields.datetime('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
113 'constraint_date_end': fields.datetime('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
114 'project_id': fields.many2one('project.project', 'Project', required=True),
115 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
116 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
117 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
118 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
119 '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)]}),
120 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
121 'user_force_ids': fields.many2many('res.users', string='Force Assigned Users'),
122 'user_ids': fields.one2many('project.user.allocation', 'phase_id', "Assigned Users",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]},
123 help="The ressources on the project can be computed automatically by the scheduler"),
124 'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
125 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.\
126 \n If the phase is over, the states is set to \'Done\'.'),
127 'progress': fields.function(_compute_progress, string='Progress', help="Computed based on related tasks"),
132 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
134 _order = "project_id, date_start, sequence"
136 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
137 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
140 def onchange_project(self, cr, uid, ids, project, context=None):
143 def copy(self, cr, uid, id, default=None, context=None):
146 if not default.get('name', False):
147 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
148 return super(project_phase, self).copy(cr, uid, id, default, context)
150 def set_draft(self, cr, uid, ids, *args):
151 self.write(cr, uid, ids, {'state': 'draft'})
154 def set_open(self, cr, uid, ids, *args):
155 self.write(cr, uid, ids, {'state': 'open'})
158 def set_pending(self, cr, uid, ids, *args):
159 self.write(cr, uid, ids, {'state': 'pending'})
162 def set_cancel(self, cr, uid, ids, *args):
163 self.write(cr, uid, ids, {'state': 'cancelled'})
166 def set_done(self, cr, uid, ids, *args):
167 self.write(cr, uid, ids, {'state': 'done'})
170 def generate_phase(self, cr, uid, phases, context=None):
171 context = context or {}
174 task_pool = self.pool.get('project.task')
176 if phase.state in ('done','cancelled'):
179 'days': 'd', 'day': 'd', 'd':'d',
180 'months': 'm', 'month':'month', 'm':'m',
181 'weeks': 'w', 'week': 'w', 'w':'w',
182 'hours': 'H', 'hour': 'H', 'h':'H',
183 }.get(phase.product_uom.name.lower(), "h")
184 duration = str(phase.duration) + duration_uom
188 effort = \"%s\"''' % (phase.id, phase.name, duration)
190 if phase.constraint_date_start:
191 start.append('datetime.datetime.strptime("'+str(phase.constraint_date_start)+'", "%Y-%m-%d %H:%M:%S")')
192 for previous_phase in phase.previous_phase_ids:
193 start.append("up.Phase_%s.end" % (previous_phase.id,))
197 ''' % (','.join(start))
199 if phase.user_force_ids:
202 ''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
204 result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
210 class project_user_allocation(osv.osv):
211 _name = 'project.user.allocation'
212 _description = 'Phase User Allocation'
213 _rec_name = 'user_id'
215 'user_id': fields.many2one('res.users', 'User', required=True),
216 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
217 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
218 'date_start': fields.datetime('Start Date', help="Starting Date"),
219 'date_end': fields.datetime('End Date', help="Ending Date"),
221 project_user_allocation()
223 class project(osv.osv):
224 _inherit = "project.project"
226 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
227 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
229 def _schedule_header(self, cr, uid, ids, context=None):
230 context = context or {}
231 if type(ids) in (long, int,):
233 projects = self.browse(cr, uid, ids, context=context)
235 for project in projects:
236 if not project.members:
237 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
239 resource_pool = self.pool.get('resource.resource')
241 result = "from resource.faces import *\n"
242 result += "import datetime\n"
243 for project in self.browse(cr, uid, ids, context=context):
244 u_ids = [i.id for i in project.members]
245 for task in project.tasks:
246 if task.state in ('done','cancelled'):
248 if task.user_id and (task.user_id.id not in u_ids):
249 u_ids.append(task.user_id.id)
250 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
251 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
252 for key, vals in resource_objs.items():
254 class User_%s(Resource):
257 ''' % (key, vals.get('name',False), vals.get('efficiency', False))
264 def _schedule_project(self, cr, uid, project, context=None):
265 resource_pool = self.pool.get('resource.resource')
266 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
267 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
268 # TODO: check if we need working_..., default values are ok.
276 project.id, project.name,
277 project.date_start, working_days,
278 '|'.join(['User_'+str(x.id) for x in project.members])
280 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
288 def schedule_phases(self, cr, uid, ids, context=None):
289 context = context or {}
290 if type(ids) in (long, int,):
292 projects = self.browse(cr, uid, ids, context=context)
293 result = self._schedule_header(cr, uid, ids, context=context)
294 for project in projects:
295 result += self._schedule_project(cr, uid, project, context=context)
296 result += self.pool.get('project.phase').generate_phase(cr, uid, project.phase_ids, context=context)
299 exec result in local_dict
300 projects_gantt = Task.BalancedProject(local_dict['Project'])
302 for project in projects:
303 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
304 for phase in project.phase_ids:
305 if phase.state in ('done','cancelled'):
307 # Maybe it's better to update than unlink/create if it already exists ?
308 p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
310 self.pool.get('project.user.allocation').unlink(cr, uid,
311 [x.id for x in phase.user_ids],
315 for r in p.booked_resource:
316 self.pool.get('project.user.allocation').create(cr, uid, {
317 'user_id': int(r.name[5:]),
318 'phase_id': phase.id,
319 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
320 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
322 self.pool.get('project.phase').write(cr, uid, [phase.id], {
323 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
324 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
328 #TODO: DO Resource allocation and compute availability
329 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
335 def schedule_tasks(self, cr, uid, ids, context=None):
336 context = context or {}
337 if type(ids) in (long, int,):
339 projects = self.browse(cr, uid, ids, context=context)
340 result = self._schedule_header(cr, uid, ids, context=context)
341 for project in projects:
342 result += self._schedule_project(cr, uid, project, context=context)
343 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
346 exec result in local_dict
347 projects_gantt = Task.BalancedProject(local_dict['Project'])
349 for project in projects:
350 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
351 for task in project.tasks:
352 if task.state in ('done','cancelled'):
355 p = getattr(project_gantt, 'Task_%d' % (task.id,))
357 self.pool.get('project.task').write(cr, uid, [task.id], {
358 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
359 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
361 if (not task.user_id) and (p.booked_resource):
362 self.pool.get('project.task').write(cr, uid, [task.id], {
363 'user_id': int(p.booked_resource[0].name[5:]),
368 class project_task(osv.osv):
369 _inherit = "project.task"
371 'phase_id': fields.many2one('project.phase', 'Project Phase'),
373 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
374 context = context or {}
378 if task.state in ('done','cancelled'):
384 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.name, ident,task.remaining_hours, ident,task.total_hours)
386 for t2 in task.parent_ids:
387 start.append("up.Task_%s.end" % (t2.id,))
391 ''' % (ident,','.join(start))
396 ''' % (ident, 'User_'+str(task.user_id.id))