[FIX] Project_long_term : Better ordering on 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
22 import time
23 from datetime import date, datetime, timedelta
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=None):
33          if context is None:
34             context = {}
35
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):
41              return False
42          ids = [id for id in prev_ids if id in next_ids]
43          # both prev_ids and next_ids must be unique
44          if ids:
45              return False
46          # unrelated project
47          prev_ids = [rec.id for rec in prev_ids]
48          next_ids = [rec.id for rec in next_ids]
49          # iter prev_ids
50          while prev_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:
54                  return False
55              ids = [id for id in prv_phase_ids if id in next_ids]
56              if ids:
57                  return False
58              prev_ids = prv_phase_ids
59          # iter next_ids
60          while next_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:
64                  return False
65              ids = [id for id in next_phase_ids if id in prev_ids]
66              if ids:
67                  return False
68              next_ids = next_phase_ids
69          return True
70
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']:
74                  return False
75          return True
76
77     def _check_constraint_start(self, cr, uid, ids, context=None):
78          phase = self.read(cr, uid, ids[0], ['date_start', 'constraint_date_start'], context=context)
79          if phase['date_start'] and phase['constraint_date_start'] and phase['date_start'] < phase['constraint_date_start']:
80              return False
81          return True
82
83     def _check_constraint_end(self, cr, uid, ids, context=None):
84          phase = self.read(cr, uid, ids[0], ['date_end', 'constraint_date_end'], context=context)
85          if phase['date_end'] and phase['constraint_date_end'] and phase['date_end'] > phase['constraint_date_end']:
86              return False
87          return True
88
89     def _get_default_uom_id(self, cr, uid):
90        model_data_obj = self.pool.get('ir.model.data')
91        model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
92        return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
93
94     _columns = {
95         'name': fields.char("Name", size=64, required=True),
96         'date_start': fields.date('Start Date', help="Starting Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
97         'date_end': fields.date('End Date', help="Ending Date of the phase", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
98         'constraint_date_start': fields.date('Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
99         'constraint_date_end': fields.date('End Date', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
100         'project_id': fields.many2one('project.project', 'Project', required=True),
101         'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
102         'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
103         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
104         'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
105         '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)]}),
106         'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
107         'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
108         'responsible_id': fields.many2one('res.users', 'Responsible', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
109         'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
110                                   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.\
111                                   \n If the phase is over, the states is set to \'Done\'.')
112      }
113     _defaults = {
114         'responsible_id': lambda obj,cr,uid,context: uid,
115         'state': 'draft',
116         'sequence': 10,
117         'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
118     }
119     _order = "project_id, date_start, sequence, name"
120     _constraints = [
121         (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
122         (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
123     ]
124
125     def onchange_project(self, cr, uid, ids, project, context=None):
126         result = {}
127         result['date_start'] = False
128         project_obj = self.pool.get('project.project')
129         if project:
130             project_id = project_obj.browse(cr, uid, project, context=context)
131             result['date_start'] = project_id.date_start
132         return {'value': result}
133
134     def _check_date_start(self, cr, uid, phase, date_end, context=None):
135        if context is None:
136             context = {}
137        """
138        Check And Compute date_end of phase if change in date_start < older time.
139        """
140        uom_obj = self.pool.get('product.uom')
141        resource_obj = self.pool.get('resource.resource')
142        cal_obj = self.pool.get('resource.calendar')
143        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
144        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
145        if resource_id:
146             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
147             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
148             if cal_id:
149                 calendar_id = cal_id
150        default_uom_id = self._get_default_uom_id(cr, uid)
151        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
152        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)
153        dt_start = work_times[0][0].strftime('%Y-%m-%d')
154        self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
155
156     def _check_date_end(self, cr, uid, phase, date_start, context=None):
157        if context is None:
158             context = {}
159        """
160        Check And Compute date_end of phase if change in date_end > older time.
161        """
162        uom_obj = self.pool.get('product.uom')
163        resource_obj = self.pool.get('resource.resource')
164        cal_obj = self.pool.get('resource.calendar')
165        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
166        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
167        if resource_id:
168             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
169             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
170             if cal_id:
171                 calendar_id = cal_id
172        default_uom_id = self._get_default_uom_id(cr, uid)
173        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
174        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)
175        dt_end = work_times[-1][1].strftime('%Y-%m-%d')
176        self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
177
178     def write(self, cr, uid, ids, vals, context=None):
179         resource_calendar_obj = self.pool.get('resource.calendar')
180         resource_obj = self.pool.get('resource.resource')
181         uom_obj = self.pool.get('product.uom')
182         if context is None:
183             context = {}
184         if context.get('scheduler',False):
185             return super(project_phase, self).write(cr, uid, ids, vals, context=context)
186         # Consider calendar and efficiency if the phase is performed by a resource
187         # otherwise consider the project's working calendar
188         if isinstance(ids, (int, long)):
189             ids = [ids]
190         phase = self.browse(cr, uid, ids[0], context=context)
191         calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
192         resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
193         if resource_id:
194                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
195                 if cal_id:
196                     calendar_id = cal_id
197         default_uom_id = self._get_default_uom_id(cr, uid)
198         avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
199         # Change the date_start and date_end
200         # for previous and next phases respectively based on valid condition
201         if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
202                 dt_start = datetime.strptime(vals['date_start'], '%Y-%m-%d')
203                 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)
204                 if work_times:
205                     vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
206                 for prv_phase in phase.previous_phase_ids:
207                     self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
208         if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
209                 dt_end = datetime.strptime(vals['date_end'], '%Y-%m-%d')
210                 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)
211                 if work_times:
212                     vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
213                 for next_phase in phase.next_phase_ids:
214                     self._check_date_end(cr, uid, next_phase, dt_end, context=context)
215         return super(project_phase, self).write(cr, uid, ids, vals, context=context)
216
217     def set_draft(self, cr, uid, ids, *args):
218         self.write(cr, uid, ids, {'state': 'draft'})
219         return True
220
221     def set_open(self, cr, uid, ids, *args):
222         self.write(cr, uid, ids, {'state': 'open'})
223         return True
224
225     def set_pending(self, cr, uid, ids, *args):
226         self.write(cr, uid, ids, {'state': 'pending'})
227         return True
228
229     def set_cancel(self, cr, uid, ids, *args):
230         self.write(cr, uid, ids, {'state': 'cancelled'})
231         return True
232
233     def set_done(self, cr, uid, ids, *args):
234         self.write(cr, uid, ids, {'state': 'done'})
235         return True
236
237 project_phase()
238
239 class project_resource_allocation(osv.osv):
240     _name = 'project.resource.allocation'
241     _description = 'Project Resource Allocation'
242     _rec_name = 'resource_id'
243     _columns = {
244         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
245         'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
246         'phase_id_date_start': fields.related('phase_id', 'date_start', type='date', string='Starting Date of the phase'),
247         'phase_id_date_end': fields.related('phase_id', 'date_end', type='date', string='Ending Date of the phase'),
248         'useability': fields.float('Usability', help="Usability of this resource for this project phase in percentage (=50%)"),
249     }
250     _defaults = {
251         'useability': 100,
252     }
253
254 project_resource_allocation()
255
256 class project(osv.osv):
257     _inherit = "project.project"
258     _columns = {
259         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
260         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
261     }
262
263 project()
264
265 class task(osv.osv):
266     _inherit = "project.task"
267     _columns = {
268         'phase_id': fields.many2one('project.phase', 'Project Phase'),
269     }
270
271 task()
272 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: