[MOD] Project Management : Usability changes to project management modules
[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 from lxml import etree
22 import mx.DateTime
23 import time
24
25 from tools.translate import _
26 from osv import fields, osv
27
28 class project_phase(osv.osv):
29     _name = "project.phase"
30     _description = "Project Phase"
31
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):
38              return False
39          ids = [id for id in prev_ids if id in next_ids]
40          # both prev_ids and next_ids must be unique
41          if ids:
42              return False
43          # unrelated project
44          prev_ids = [rec.id for rec in prev_ids]
45          next_ids = [rec.id for rec in next_ids]
46          # iter prev_ids
47          while prev_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:
51                  return False
52              ids = [id for id in prv_phase_ids if id in next_ids]
53              if ids:
54                  return False
55              prev_ids = prv_phase_ids
56          # iter next_ids
57          while next_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:
61                  return False
62              ids = [id for id in next_phase_ids if id in prev_ids]
63              if ids:
64                  return False
65              next_ids = next_phase_ids
66          return True
67
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']:
71                  return False
72          return True
73
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']:
77              return False
78          return True
79
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']:
83              return False
84          return True
85
86     _columns = {
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\'.')
104      }
105     _defaults = {
106         'responsible_id': lambda obj,cr,uid,context: uid,
107         'state': 'draft',
108         'sequence': 10,
109     }
110     _order = "name"
111     _constraints = [
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']),
116     ]
117
118     def onchange_project(self, cr, uid, ids, project, context={}):
119         result = {}
120         project_obj = self.pool.get('project.project')
121         if 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': []}}
127
128     def _check_date_start(self, cr, uid, phase, date_end, context={}):
129        """
130        Check And Compute date_end of phase if change in date_start < older time.
131        """
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)])
137        if resource_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
141             if cal_id:
142                 calendar_id = cal_id
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)
148
149     def _check_date_end(self, cr, uid, phase, date_start, context={}):
150        """
151        Check And Compute date_end of phase if change in date_end > older time.
152        """
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)
158        if resource_id:
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
162             if cal_id:
163                 calendar_id = cal_id
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)
169
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')
174         if not context:
175             context = {}
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)
183         if resource_id:
184                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
185                 if cal_id:
186                     calendar_id = cal_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)
189
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)
195                 if work_times:
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)
202                 if work_times:
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)
207
208     def set_draft(self, cr, uid, ids, *args):
209         self.write(cr, uid, ids, {'state': 'draft'})
210         return True
211
212     def set_open(self, cr, uid, ids, *args):
213         self.write(cr, uid, ids, {'state': 'open'})
214         return True
215
216     def set_pending(self, cr, uid, ids,*args):
217         self.write(cr, uid, ids, {'state': 'pending'})
218         return True
219
220     def set_cancel(self, cr, uid, ids, *args):
221         self.write(cr, uid, ids, {'state': 'cancelled'})
222         return True
223
224     def set_done(self, cr, uid, ids, *args):
225         self.write(cr, uid, ids, {'state': 'done'})
226         return True
227
228 project_phase()
229
230 class project_resource_allocation(osv.osv):
231     _name = 'project.resource.allocation'
232     _description = 'Project Resource Allocation'
233     _rec_name = 'resource_id'
234     _columns = {
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%)"),
238     }
239     _defaults = {
240         'useability': 100,
241     }
242
243 project_resource_allocation()
244
245 class project(osv.osv):
246     _inherit = "project.project"
247     _columns = {
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"),
250     }
251
252 project()
253
254 class task(osv.osv):
255     _inherit = "project.task"
256     _columns = {
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.'),
260     }
261     _defaults = {
262          'occupation_rate': '1'
263     }
264
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):
266         result = {}
267         resource = False
268         resource_obj = self.pool.get('resource.resource')
269         project_pool = self.pool.get('project.project')
270         resource_calendar = self.pool.get('resource.calendar')
271         if not project:
272             return {'value' : result}
273         if date_start:
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)])
278             if resource_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)
285             if work_times:
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}
289
290     def _check_date_start(self, cr, uid, task, date_end, context={}):
291        """
292        Check And Compute date_end of task if change in date_start < older time.
293        """
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)
299        if resource_id:
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')})
307
308     def _check_date_end(self, cr, uid, task, date_start, context={}):
309        """
310        Check And Compute date_end of task if change in date_end > older time.
311        """
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)
317        if resource_id:
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)
325
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')
329         if not context:
330             context = {}
331         if context.get('scheduler',False):
332             return super(task, self).write(cr, uid, ids, vals, context=context)
333
334         # Consider calendar and efficiency if the task is performed by a resource
335         # otherwise consider the project's working calendar
336         task_id = ids
337         if isinstance(ids, list):
338             task_id = ids[0]
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)
343         if resource_id:
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)
348
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)
354                 if work_times:
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)
361                 if work_times:
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)
365
366         return super(task, self).write(cr, uid, ids, vals, context=context)
367
368 task()
369 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: