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 ##############################################################################
21 from lxml import etree
25 from tools.translate import _
26 from osv import fields, osv
28 class project_phase(osv.osv):
29 _name = "project.phase"
30 _description = "Project Phase"
32 def _check_recursion(self, cr, uid, ids, context={}):
33 data_phase = self.browse(cr, uid, ids[0], context=context)
34 prev_ids = data_phase.previous_phase_ids
35 next_ids = data_phase.next_phase_ids
36 # it should nither be in prev_ids nor in next_ids
37 if (data_phase in prev_ids) or (data_phase in next_ids):
39 ids = [id for id in prev_ids if id in next_ids]
40 # both prev_ids and next_ids must be unique
44 prev_ids = [rec.id for rec in prev_ids]
45 next_ids = [rec.id for rec in next_ids]
48 cr.execute('select distinct prv_phase_id from project_phase_rel where next_phase_id in ('+','.join(map(str, prev_ids))+')')
49 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
50 if data_phase.id in prv_phase_ids:
52 ids = [id for id in prv_phase_ids if id in next_ids]
55 prev_ids = prv_phase_ids
58 cr.execute('select distinct next_phase_id from project_phase_rel where prv_phase_id in ('+','.join(map(str, next_ids))+')')
59 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
60 if data_phase.id in next_phase_ids:
62 ids = [id for id in next_phase_ids if id in prev_ids]
65 next_ids = next_phase_ids
68 def _check_dates(self, cr, uid, ids, context={}):
69 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
70 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
74 def _check_constraint_start(self, cr, uid, ids, context={}):
75 phase = self.read(cr, uid, ids[0], ['date_start', 'constraint_date_start'], context=context)
76 if phase['date_start'] and phase['constraint_date_start'] and phase['date_start'] < phase['constraint_date_start']:
80 def _check_constraint_end(self, cr, uid, ids, context={}):
81 phase = self.read(cr, uid, ids[0], ['date_end', 'constraint_date_end'], context=context)
82 if phase['date_end'] and phase['constraint_date_end'] and phase['date_end'] > phase['constraint_date_end']:
87 'name': fields.char("Phase Name", size=64, required=True),
88 'date_start': fields.datetime('Starting Date', help="Start date of the phase"),
89 'date_end': fields.datetime('End Date', help="End date of the phase"),
90 'constraint_date_start': fields.datetime('Start Date', help='force the phase to start after this date'),
91 'constraint_date_end': fields.datetime('End Date', help='force the phase to finish before this date'),
92 'project_id': fields.many2one('project.project', 'Project', required=True),
93 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases'),
94 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases'),
95 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
96 'duration': fields.float('Duration', required=True, help="By default in days"),
97 'product_uom': fields.many2one('product.uom', 'Duration UoM', required=True, help="UoM (Unit of Measure) is the unit of measurement for Duration"),
98 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks"),
99 'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources"),
100 'responsible_id':fields.many2one('res.users', 'Responsible'),
101 'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
102 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.\
103 \n If the phase is over, the states is set to \'Done\'.')
106 'responsible_id': lambda obj,cr,uid,context: uid,
112 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
113 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
114 #(_check_constraint_start, 'Phase must start-after constraint start Date.', ['date_start', 'constraint_date_start']),
115 #(_check_constraint_end, 'Phase must end-before constraint end Date.', ['date_end', 'constraint_date_end']),
118 def onchange_project(self, cr, uid, ids, project, context={}):
120 project_obj = self.pool.get('project.project')
122 project_id = project_obj.browse(cr, uid, project, context=context)
123 if project_id.date_start:
124 result['date_start'] = mx.DateTime.strptime(project_id.date_start, "%Y-%m-%d").strftime('%Y-%m-%d %H:%M:%S')
125 return {'value': result}
126 return {'value': {'date_start': []}}
128 def _check_date_start(self, cr, uid, phase, date_end, context={}):
130 Check And Compute date_end of phase if change in date_start < older time.
132 uom_obj = self.pool.get('product.uom')
133 resource_obj = self.pool.get('resource.resource')
134 cal_obj = self.pool.get('resource.calendar')
135 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
136 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
138 # cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
139 res = resource_obj.read(cr, uid, resource_id[0], ['calendar_id'], context=context)[0]
140 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
143 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')], context=context)[0]
144 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
145 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)
146 dt_start = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
147 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
149 def _check_date_end(self, cr, uid, phase, date_start, context={}):
151 Check And Compute date_end of phase if change in date_end > older time.
153 uom_obj = self.pool.get('product.uom')
154 resource_obj = self.pool.get('resource.resource')
155 cal_obj = self.pool.get('resource.calendar')
156 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
157 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
159 # cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
160 res = resource_obj.read(cr, uid, resource_id[0], ['calendar_id'], context=context)[0]
161 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
164 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')], context=context)[0]
165 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
166 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)
167 dt_end = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
168 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d %H:%M:%S'), 'date_end': dt_end}, context=context)
170 def write(self, cr, uid, ids, vals, context={}):
171 resource_calendar_obj = self.pool.get('resource.calendar')
172 resource_obj = self.pool.get('resource.resource')
173 uom_obj = self.pool.get('product.uom')
176 if context.get('scheduler',False):
177 return super(project_phase, self).write(cr, uid, ids, vals, context=context)
178 # Consider calendar and efficiency if the phase is performed by a resource
179 # otherwise consider the project's working calendar
180 phase = self.browse(cr, uid, ids[0], context=context)
181 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
182 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
184 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
187 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')])[0]
188 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
190 # Change the date_start and date_end
191 # for previous and next phases respectively based on valid condition
192 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
193 dt_start = mx.DateTime.strptime(vals['date_start'],'%Y-%m-%d %H:%M:%S')
194 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)
196 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
197 for prv_phase in phase.previous_phase_ids:
198 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
199 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
200 dt_end = mx.DateTime.strptime(vals['date_end'],'%Y-%m-%d %H:%M:%S')
201 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)
203 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
204 for next_phase in phase.next_phase_ids:
205 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
206 return super(project_phase, self).write(cr, uid, ids, vals, context=context)
208 def set_draft(self, cr, uid, ids, *args):
209 self.write(cr, uid, ids, {'state': 'draft'})
212 def set_open(self, cr, uid, ids, *args):
213 self.write(cr, uid, ids, {'state': 'open'})
216 def set_pending(self, cr, uid, ids,*args):
217 self.write(cr, uid, ids, {'state': 'pending'})
220 def set_cancel(self, cr, uid, ids, *args):
221 self.write(cr, uid, ids, {'state': 'cancelled'})
224 def set_done(self, cr, uid, ids, *args):
225 self.write(cr, uid, ids, {'state': 'done'})
230 class project_resource_allocation(osv.osv):
231 _name = 'project.resource.allocation'
232 _description = 'Project Resource Allocation'
233 _rec_name = 'resource_id'
235 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
236 'phase_id': fields.many2one('project.phase', 'Project Phase', required=True),
237 'useability': fields.float('Useability', help="Useability of this ressource for this project phase in percentage (=50%)"),
243 project_resource_allocation()
245 class project(osv.osv):
246 _inherit = "project.project"
248 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
249 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
255 _inherit = "project.task"
257 'phase_id': fields.many2one('project.phase', 'Project Phase'),
258 'occupation_rate': fields.float('Occupation Rate', help='The occupation rate fields indicates how much of his time a user is working on a task. A 100% occupation rate means the user works full time on the tasks. The ending date of a task is computed like this: Starting Date + Duration / Occupation Rate.'),
259 'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
262 'occupation_rate': '1'
265 def onchange_planned(self, cr, uid, ids, project, user_id=False, planned=0.0, effective=0.0, date_start=None, occupation_rate=0.0):
268 resource_obj = self.pool.get('resource.resource')
269 project_pool = self.pool.get('project.project')
270 resource_calendar = self.pool.get('resource.calendar')
272 return {'value' : result}
274 hrs = float(planned / float(occupation_rate))
275 calendar_id = project_pool.browse(cr, uid, project).resource_calendar_id.id
276 dt_start = mx.DateTime.strptime(date_start, '%Y-%m-%d %H:%M:%S')
277 resource_id = resource_obj.search(cr, uid, [('user_id','=',user_id)])
279 resource_data = resource_obj.browse(cr, uid, resource_id)[0]
280 resource = resource_data.id
281 hrs = planned / (float(occupation_rate) * resource_data.time_efficiency)
282 if resource_data.calendar_id.id:
283 calendar_id = resource_data.calendar_id.id
284 work_times = resource_calendar.interval_get(cr, uid, calendar_id, dt_start, hrs or 0.0, resource or False)
286 result['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
287 result['remaining_hours'] = planned - effective
288 return {'value' : result}
290 def _check_date_start(self, cr, uid, task, date_end, context={}):
292 Check And Compute date_end of task if change in date_start < older time.
294 resource_calendar_obj = self.pool.get('resource.calendar')
295 resource_obj = self.pool.get('resource.resource')
296 calendar_id = task.project_id.resource_calendar_id and task.project_id.resource_calendar_id.id or False
297 hours = task.planned_hours / task.occupation_rate
298 resource_id = resource_obj.search(cr, uid, [('user_id', '=', task.user_id.id)], context=context)
300 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
301 if resource.calendar_id.id:
302 calendar_id = resource.calendar_id and resource.calendar_id.id or False
303 hours = task.planned_hours / (float(task.occupation_rate) * resource.time_efficiency)
304 work_times = resource_calendar_obj.interval_min_get(cr, uid, calendar_id, date_end, hours or 0.0, resource_id and resource_id[0] or False)
305 dt_start = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
306 self.write(cr, uid, [task.id], {'date_start' : dt_start,'date_end' : date_end.strftime('%Y-%m-%d %H:%M:%S')})
308 def _check_date_end(self, cr, uid, task, date_start, context={}):
310 Check And Compute date_end of task if change in date_end > older time.
312 resource_calendar_obj = self.pool.get('resource.calendar')
313 resource_obj = self.pool.get('resource.resource')
314 calendar_id = task.project_id.resource_calendar_id and task.project_id.resource_calendar_id.id or False
315 hours = task.planned_hours / task.occupation_rate
316 resource_id = resource_obj.search(cr,uid,[('user_id', '=', task.user_id.id)], context=context)
318 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
319 if resource.calendar_id.id:
320 calendar_id = resource.calendar_id and resource.calendar_id.id or False
321 hours = task.planned_hours / (float(task.occupation_rate) * resource.time_efficiency)
322 work_times = resource_calendar_obj.interval_get(cr, uid, calendar_id, date_start, hours or 0.0, resource_id and resource_id[0] or False)
323 dt_end = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
324 self.write(cr, uid, [task.id], {'date_start': date_start.strftime('%Y-%m-%d %H:%M:%S'),'date_end' : dt_end}, context=context)
326 def write(self, cr, uid, ids, vals, context={}):
327 resource_calendar_obj = self.pool.get('resource.calendar')
328 resource_obj = self.pool.get('resource.resource')
331 if context.get('scheduler',False):
332 return super(task, self).write(cr, uid, ids, vals, context=context)
334 # Consider calendar and efficiency if the task is performed by a resource
335 # otherwise consider the project's working calendar
337 if isinstance(ids, list):
339 task_rec = self.browse(cr, uid, task_id, context=context)
340 calendar_id = task_rec.project_id.resource_calendar_id and task_rec.project_id.resource_calendar_id.id or False
341 hrs = task_rec.planned_hours / task_rec.occupation_rate
342 resource_id = resource_obj.search(cr, uid, [('user_id', '=', task_rec.user_id.id)], context=context)
344 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
345 if resource.calendar_id.id:
346 calendar_id = resource.calendar_id and resource.calendar_id.id or False
347 hrs = task_rec.planned_hours / (float(task_rec.occupation_rate) * resource.time_efficiency)
349 # Change the date_start and date_end
350 # for previous and next tasks respectively based on valid condition
351 if vals.get('date_start', False) and vals['date_start'] < task_rec.date_start:
352 dt_start = mx.DateTime.strptime(vals['date_start'], '%Y-%m-%d %H:%M:%S')
353 work_times = resource_calendar_obj.interval_get(cr, uid, calendar_id, dt_start, hrs or 0.0, resource.id or False)
355 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
356 for prv_task in task_rec.parent_ids:
357 self._check_date_start(cr, uid, prv_task, dt_start)
358 if vals.get('date_end', False) and vals['date_end'] > task_rec.date_end:
359 dt_end = mx.DateTime.strptime(vals['date_end'], '%Y-%m-%d %H:%M:%S')
360 work_times = resource_calendar_obj.interval_min_get(cr, uid, calendar_id, dt_end, hrs or 0.0, resource.id or False)
362 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
363 for next_task in task_rec.child_ids:
364 self._check_date_end(cr, uid, next_task, dt_end)
366 return super(task, self).write(cr, uid, ids, vals, context=context)
369 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: