[MERGE] a few project management-related minor fixes
[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=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 = "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         #(_check_constraint_start, 'Phase must start-after constraint start Date.', ['date_start', 'constraint_date_start']),
124         #(_check_constraint_end, 'Phase must end-before constraint end Date.', ['date_end', 'constraint_date_end']),
125     ]
126
127     def onchange_project(self, cr, uid, ids, project, context=None):
128         result = {}
129         project_obj = self.pool.get('project.project')
130         if project:
131             project_id = project_obj.browse(cr, uid, project, context=context)
132             if project_id.date_start:
133                 result['date_start'] = mx.DateTime.strptime(project_id.date_start, "%Y-%m-%d").strftime('%Y-%m-%d')
134                 return {'value': result}
135         return {'value': {'date_start': []}}
136
137     def _check_date_start(self, cr, uid, phase, date_end, context=None):
138        if context is None:
139             context = {}
140        """
141        Check And Compute date_end of phase if change in date_start < older time.
142        """
143        uom_obj = self.pool.get('product.uom')
144        resource_obj = self.pool.get('resource.resource')
145        cal_obj = self.pool.get('resource.calendar')
146        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
147        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
148        if resource_id:
149 #            cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
150             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
151             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
152             if cal_id:
153                 calendar_id = cal_id
154        default_uom_id = self._get_default_uom_id(cr, uid)
155        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
156        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)
157        dt_start = work_times[0][0].strftime('%Y-%m-%d')
158        self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d')}, context=context)
159
160     def _check_date_end(self, cr, uid, phase, date_start, context=None):
161        if context is None:
162             context = {}
163        """
164        Check And Compute date_end of phase if change in date_end > older time.
165        """
166        uom_obj = self.pool.get('product.uom')
167        resource_obj = self.pool.get('resource.resource')
168        cal_obj = self.pool.get('resource.calendar')
169        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
170        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
171        if resource_id:
172 #            cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
173             res = resource_obj.read(cr, uid, resource_id, ['calendar_id'], context=context)[0]
174             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
175             if cal_id:
176                 calendar_id = cal_id
177        default_uom_id = self._get_default_uom_id(cr, uid)
178        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
179        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)
180        dt_end = work_times[-1][1].strftime('%Y-%m-%d')
181        self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d'), 'date_end': dt_end}, context=context)
182
183     def write(self, cr, uid, ids, vals, context=None):
184         resource_calendar_obj = self.pool.get('resource.calendar')
185         resource_obj = self.pool.get('resource.resource')
186         uom_obj = self.pool.get('product.uom')
187         if context is None:
188             context = {}
189         if context.get('scheduler',False):
190             return super(project_phase, self).write(cr, uid, ids, vals, context=context)
191         # Consider calendar and efficiency if the phase is performed by a resource
192         # otherwise consider the project's working calendar
193         if type(ids) == int:
194             ids = [ids]
195         phase = self.browse(cr, uid, ids[0], context=context)
196         calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
197         resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
198         if resource_id:
199                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
200                 if cal_id:
201                     calendar_id = cal_id
202         default_uom_id = self._get_default_uom_id(cr, uid)
203         avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
204
205         # Change the date_start and date_end
206         # for previous and next phases respectively based on valid condition
207         if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
208                 dt_start = mx.DateTime.strptime(vals['date_start'], '%Y-%m-%d')
209                 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)
210                 if work_times:
211                     vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
212                 for prv_phase in phase.previous_phase_ids:
213                     self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
214         if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
215                 dt_end = mx.DateTime.strptime(vals['date_end'],'%Y-%m-%d')
216                 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)
217                 if work_times:
218                     vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
219                 for next_phase in phase.next_phase_ids:
220                     self._check_date_end(cr, uid, next_phase, dt_end, context=context)
221         return super(project_phase, self).write(cr, uid, ids, vals, context=context)
222
223     def set_draft(self, cr, uid, ids, *args):
224         self.write(cr, uid, ids, {'state': 'draft'})
225         return True
226
227     def set_open(self, cr, uid, ids, *args):
228         self.write(cr, uid, ids, {'state': 'open'})
229         return True
230
231     def set_pending(self, cr, uid, ids, *args):
232         self.write(cr, uid, ids, {'state': 'pending'})
233         return True
234
235     def set_cancel(self, cr, uid, ids, *args):
236         self.write(cr, uid, ids, {'state': 'cancelled'})
237         return True
238
239     def set_done(self, cr, uid, ids, *args):
240         self.write(cr, uid, ids, {'state': 'done'})
241         return True
242
243 project_phase()
244
245 class project_resource_allocation(osv.osv):
246     _name = 'project.resource.allocation'
247     _description = 'Project Resource Allocation'
248     _rec_name = 'resource_id'
249     _columns = {
250         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
251         'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
252         'phase_id_date_start': fields.related('phase_id', 'date_start', type='date', string='Starting Date of the phase'),
253         'phase_id_date_end': fields.related('phase_id', 'date_end', type='date', string='Ending Date of the phase'),
254         'useability': fields.float('Usability', help="Usability of this resource for this project phase in percentage (=50%)"),
255     }
256     _defaults = {
257         'useability': 100,
258     }
259
260 project_resource_allocation()
261
262 class project(osv.osv):
263     _inherit = "project.project"
264     _columns = {
265         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
266         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
267     }
268
269 project()
270
271 class task(osv.osv):
272     _inherit = "project.task"
273     _columns = {
274         'phase_id': fields.many2one('project.phase', 'Project Phase'),
275     }
276
277 task()
278 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: