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("Name", size=64, required=True),
88 'date_start': fields.datetime('Start Date', help="Starting Date of the phase"),
89 'date_end': fields.datetime('End Date', help="Ending 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),
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,
109 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', 'Day')], context=c)[0]
113 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
114 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
115 #(_check_constraint_start, 'Phase must start-after constraint start Date.', ['date_start', 'constraint_date_start']),
116 #(_check_constraint_end, 'Phase must end-before constraint end Date.', ['date_end', 'constraint_date_end']),
119 def onchange_project(self, cr, uid, ids, project, context={}):
121 project_obj = self.pool.get('project.project')
123 project_id = project_obj.browse(cr, uid, project, context=context)
124 if project_id.date_start:
125 result['date_start'] = mx.DateTime.strptime(project_id.date_start, "%Y-%m-%d").strftime('%Y-%m-%d %H:%M:%S')
126 return {'value': result}
127 return {'value': {'date_start': []}}
129 def _check_date_start(self, cr, uid, phase, date_end, context={}):
131 Check And Compute date_end of phase if change in date_start < older time.
133 uom_obj = self.pool.get('product.uom')
134 resource_obj = self.pool.get('resource.resource')
135 cal_obj = self.pool.get('resource.calendar')
136 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
137 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
139 # cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
140 res = resource_obj.read(cr, uid, resource_id[0], ['calendar_id'], context=context)[0]
141 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
144 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')], context=context)[0]
145 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
146 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)
147 dt_start = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
148 self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
150 def _check_date_end(self, cr, uid, phase, date_start, context={}):
152 Check And Compute date_end of phase if change in date_end > older time.
154 uom_obj = self.pool.get('product.uom')
155 resource_obj = self.pool.get('resource.resource')
156 cal_obj = self.pool.get('resource.calendar')
157 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
158 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
160 # cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
161 res = resource_obj.read(cr, uid, resource_id[0], ['calendar_id'], context=context)[0]
162 cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
165 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')], context=context)[0]
166 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
167 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)
168 dt_end = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
169 self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d %H:%M:%S'), 'date_end': dt_end}, context=context)
171 def write(self, cr, uid, ids, vals, context={}):
172 resource_calendar_obj = self.pool.get('resource.calendar')
173 resource_obj = self.pool.get('resource.resource')
174 uom_obj = self.pool.get('product.uom')
177 if context.get('scheduler',False):
178 return super(project_phase, self).write(cr, uid, ids, vals, context=context)
179 # Consider calendar and efficiency if the phase is performed by a resource
180 # otherwise consider the project's working calendar
181 phase = self.browse(cr, uid, ids[0], context=context)
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)
185 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
188 default_uom_id = uom_obj.search(cr, uid, [('name', '=', 'Hour')])[0]
189 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
191 # Change the date_start and date_end
192 # for previous and next phases respectively based on valid condition
193 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
194 dt_start = mx.DateTime.strptime(vals['date_start'],'%Y-%m-%d %H:%M:%S')
195 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)
197 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
198 for prv_phase in phase.previous_phase_ids:
199 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
200 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
201 dt_end = mx.DateTime.strptime(vals['date_end'],'%Y-%m-%d %H:%M:%S')
202 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)
204 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
205 for next_phase in phase.next_phase_ids:
206 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
207 return super(project_phase, self).write(cr, uid, ids, vals, context=context)
209 def set_draft(self, cr, uid, ids, *args):
210 self.write(cr, uid, ids, {'state': 'draft'})
213 def set_open(self, cr, uid, ids, *args):
214 self.write(cr, uid, ids, {'state': 'open'})
217 def set_pending(self, cr, uid, ids,*args):
218 self.write(cr, uid, ids, {'state': 'pending'})
221 def set_cancel(self, cr, uid, ids, *args):
222 self.write(cr, uid, ids, {'state': 'cancelled'})
225 def set_done(self, cr, uid, ids, *args):
226 self.write(cr, uid, ids, {'state': 'done'})
231 class project_resource_allocation(osv.osv):
232 _name = 'project.resource.allocation'
233 _description = 'Project Resource Allocation'
234 _rec_name = 'resource_id'
236 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
237 'phase_id': fields.many2one('project.phase', 'Project Phase', required=True),
238 'useability': fields.float('Useability', help="Useability of this ressource for this project phase in percentage (=50%)"),
244 project_resource_allocation()
246 class project(osv.osv):
247 _inherit = "project.project"
249 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
250 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
256 _inherit = "project.task"
258 'phase_id': fields.many2one('project.phase', 'Project Phase'),
259 '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.'),
260 '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.'),
263 'occupation_rate': '1'
266 def onchange_planned(self, cr, uid, ids, project, user_id=False, planned=0.0, effective=0.0, date_start=None, occupation_rate=0.0):
269 resource_obj = self.pool.get('resource.resource')
270 project_pool = self.pool.get('project.project')
271 resource_calendar = self.pool.get('resource.calendar')
273 return {'value' : result}
275 hrs = float(planned / float(occupation_rate))
276 calendar_id = project_pool.browse(cr, uid, project).resource_calendar_id.id
277 dt_start = mx.DateTime.strptime(date_start, '%Y-%m-%d %H:%M:%S')
278 resource_id = resource_obj.search(cr, uid, [('user_id','=',user_id)])
280 resource_data = resource_obj.browse(cr, uid, resource_id)[0]
281 resource = resource_data.id
282 hrs = planned / (float(occupation_rate) * resource_data.time_efficiency)
283 if resource_data.calendar_id.id:
284 calendar_id = resource_data.calendar_id.id
285 work_times = resource_calendar.interval_get(cr, uid, calendar_id, dt_start, hrs or 0.0, resource or False)
287 result['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
288 result['remaining_hours'] = planned - effective
289 return {'value' : result}
291 def _check_date_start(self, cr, uid, task, date_end, context={}):
293 Check And Compute date_end of task if change in date_start < older time.
295 resource_calendar_obj = self.pool.get('resource.calendar')
296 resource_obj = self.pool.get('resource.resource')
297 calendar_id = task.project_id.resource_calendar_id and task.project_id.resource_calendar_id.id or False
298 hours = task.planned_hours / task.occupation_rate
299 resource_id = resource_obj.search(cr, uid, [('user_id', '=', task.user_id.id)], context=context)
301 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
302 if resource.calendar_id.id:
303 calendar_id = resource.calendar_id and resource.calendar_id.id or False
304 hours = task.planned_hours / (float(task.occupation_rate) * resource.time_efficiency)
305 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)
306 dt_start = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
307 self.write(cr, uid, [task.id], {'date_start' : dt_start,'date_end' : date_end.strftime('%Y-%m-%d %H:%M:%S')})
309 def _check_date_end(self, cr, uid, task, date_start, context={}):
311 Check And Compute date_end of task if change in date_end > older time.
313 resource_calendar_obj = self.pool.get('resource.calendar')
314 resource_obj = self.pool.get('resource.resource')
315 calendar_id = task.project_id.resource_calendar_id and task.project_id.resource_calendar_id.id or False
316 hours = task.planned_hours / task.occupation_rate
317 resource_id = resource_obj.search(cr,uid,[('user_id', '=', task.user_id.id)], context=context)
319 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
320 if resource.calendar_id.id:
321 calendar_id = resource.calendar_id and resource.calendar_id.id or False
322 hours = task.planned_hours / (float(task.occupation_rate) * resource.time_efficiency)
323 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)
324 dt_end = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
325 self.write(cr, uid, [task.id], {'date_start': date_start.strftime('%Y-%m-%d %H:%M:%S'),'date_end' : dt_end}, context=context)
327 def write(self, cr, uid, ids, vals, context={}):
328 resource_calendar_obj = self.pool.get('resource.calendar')
329 resource_obj = self.pool.get('resource.resource')
332 if context.get('scheduler',False):
333 return super(task, self).write(cr, uid, ids, vals, context=context)
335 # Consider calendar and efficiency if the task is performed by a resource
336 # otherwise consider the project's working calendar
338 if isinstance(ids, list):
340 task_rec = self.browse(cr, uid, task_id, context=context)
341 calendar_id = task_rec.project_id.resource_calendar_id and task_rec.project_id.resource_calendar_id.id or False
342 hrs = task_rec.planned_hours / task_rec.occupation_rate
343 resource_id = resource_obj.search(cr, uid, [('user_id', '=', task_rec.user_id.id)], context=context)
345 resource = resource_obj.browse(cr, uid, resource_id[0], context=context)
346 if resource.calendar_id.id:
347 calendar_id = resource.calendar_id and resource.calendar_id.id or False
348 hrs = task_rec.planned_hours / (float(task_rec.occupation_rate) * resource.time_efficiency)
350 # Change the date_start and date_end
351 # for previous and next tasks respectively based on valid condition
352 if vals.get('date_start', False) and vals['date_start'] < task_rec.date_start:
353 dt_start = mx.DateTime.strptime(vals['date_start'], '%Y-%m-%d %H:%M:%S')
354 work_times = resource_calendar_obj.interval_get(cr, uid, calendar_id, dt_start, hrs or 0.0, resource.id or False)
356 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
357 for prv_task in task_rec.parent_ids:
358 self._check_date_start(cr, uid, prv_task, dt_start)
359 if vals.get('date_end', False) and vals['date_end'] > task_rec.date_end:
360 dt_end = mx.DateTime.strptime(vals['date_end'], '%Y-%m-%d %H:%M:%S')
361 work_times = resource_calendar_obj.interval_min_get(cr, uid, calendar_id, dt_end, hrs or 0.0, resource.id or False)
363 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
364 for next_task in task_rec.child_ids:
365 self._check_date_end(cr, uid, next_task, dt_end)
367 return super(task, self).write(cr, uid, ids, vals, context=context)
370 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: