[IMP] project_long_term: *improve the form view of phase
[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("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\'.')
104      }
105     _defaults = {
106         'responsible_id': lambda obj,cr,uid,context: uid,
107         'state': 'draft',
108         'sequence': 10,
109         'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', 'Day')], context=c)[0]
110     }
111     _order = "name"
112     _constraints = [
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']),
117     ]
118
119     def onchange_project(self, cr, uid, ids, project, context={}):
120         result = {}
121         project_obj = self.pool.get('project.project')
122         if 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': []}}
128
129     def _check_date_start(self, cr, uid, phase, date_end, context={}):
130        """
131        Check And Compute date_end of phase if change in date_start < older time.
132        """
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)])
138        if resource_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
142             if cal_id:
143                 calendar_id = cal_id
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)
149
150     def _check_date_end(self, cr, uid, phase, date_start, context={}):
151        """
152        Check And Compute date_end of phase if change in date_end > older time.
153        """
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)
159        if resource_id:
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
163             if cal_id:
164                 calendar_id = cal_id
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)
170
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')
175         if not context:
176             context = {}
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)
184         if resource_id:
185                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
186                 if cal_id:
187                     calendar_id = cal_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)
190
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)
196                 if work_times:
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)
203                 if work_times:
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)
208
209     def set_draft(self, cr, uid, ids, *args):
210         self.write(cr, uid, ids, {'state': 'draft'})
211         return True
212
213     def set_open(self, cr, uid, ids, *args):
214         self.write(cr, uid, ids, {'state': 'open'})
215         return True
216
217     def set_pending(self, cr, uid, ids,*args):
218         self.write(cr, uid, ids, {'state': 'pending'})
219         return True
220
221     def set_cancel(self, cr, uid, ids, *args):
222         self.write(cr, uid, ids, {'state': 'cancelled'})
223         return True
224
225     def set_done(self, cr, uid, ids, *args):
226         self.write(cr, uid, ids, {'state': 'done'})
227         return True
228
229 project_phase()
230
231 class project_resource_allocation(osv.osv):
232     _name = 'project.resource.allocation'
233     _description = 'Project Resource Allocation'
234     _rec_name = 'resource_id'
235     _columns = {
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%)"),
239     }
240     _defaults = {
241         'useability': 100,
242     }
243
244 project_resource_allocation()
245
246 class project(osv.osv):
247     _inherit = "project.project"
248     _columns = {
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"),
251     }
252
253 project()
254
255 class task(osv.osv):
256     _inherit = "project.task"
257     _columns = {
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.'),
261     }
262     _defaults = {
263          'occupation_rate': '1'
264     }
265
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):
267         result = {}
268         resource = False
269         resource_obj = self.pool.get('resource.resource')
270         project_pool = self.pool.get('project.project')
271         resource_calendar = self.pool.get('resource.calendar')
272         if not project:
273             return {'value' : result}
274         if date_start:
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)])
279             if resource_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)
286             if work_times:
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}
290
291     def _check_date_start(self, cr, uid, task, date_end, context={}):
292        """
293        Check And Compute date_end of task if change in date_start < older time.
294        """
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)
300        if resource_id:
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')})
308
309     def _check_date_end(self, cr, uid, task, date_start, context={}):
310        """
311        Check And Compute date_end of task if change in date_end > older time.
312        """
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)
318        if resource_id:
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)
326
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')
330         if not context:
331             context = {}
332         if context.get('scheduler',False):
333             return super(task, self).write(cr, uid, ids, vals, context=context)
334
335         # Consider calendar and efficiency if the task is performed by a resource
336         # otherwise consider the project's working calendar
337         task_id = ids
338         if isinstance(ids, list):
339             task_id = ids[0]
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)
344         if resource_id:
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)
349
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)
355                 if work_times:
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)
362                 if work_times:
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)
366
367         return super(task, self).write(cr, uid, ids, vals, context=context)
368
369 task()
370 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: