[IMP] project_long_term: use xml_id to retrieve 'hour' uom instead of name search
[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     _columns = {
90         'name': fields.char("Name", size=64, required=True),
91         'date_start': fields.datetime('Start Date', help="Starting Date of the phase"),
92         'date_end': fields.datetime('End Date', help="Ending Date of the phase"),
93         'constraint_date_start': fields.datetime('Start Date', help='force the phase to start after this date'),
94         'constraint_date_end': fields.datetime('End Date', help='force the phase to finish before this date'),
95         'project_id': fields.many2one('project.project', 'Project', required=True),
96         'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases'),
97         'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases'),
98         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
99         'duration': fields.float('Duration', required=True, help="By default in days"),
100         'product_uom': fields.many2one('product.uom', 'Duration UoM', required=True, help="UoM (Unit of Measure) is the unit of measurement for Duration"),
101         'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks"),
102         'resource_ids': fields.one2many('project.resource.allocation', 'phase_id', "Project Resources"),
103         'responsible_id': fields.many2one('res.users', 'Responsible'),
104         'state': fields.selection([('draft', 'Draft'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
105                                   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.\
106                                   \n If the phase is over, the states is set to \'Done\'.')
107      }
108     _defaults = {
109         'responsible_id': lambda obj,cr,uid,context: uid,
110         'state': 'draft',
111         'sequence': 10,
112         'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', 'Day')], context=c)[0]
113     }
114     _order = "name"
115     _constraints = [
116         (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
117         (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
118         #(_check_constraint_start, 'Phase must start-after constraint start Date.', ['date_start', 'constraint_date_start']),
119         #(_check_constraint_end, 'Phase must end-before constraint end Date.', ['date_end', 'constraint_date_end']),
120     ]
121
122     def onchange_project(self, cr, uid, ids, project, context=None):
123         result = {}
124         project_obj = self.pool.get('project.project')
125         if project:
126             project_id = project_obj.browse(cr, uid, project, context=context)
127             if project_id.date_start:
128                 result['date_start'] = mx.DateTime.strptime(project_id.date_start, "%Y-%m-%d").strftime('%Y-%m-%d %H:%M:%S')
129                 return {'value': result}
130         return {'value': {'date_start': []}}
131
132     def _check_date_start(self, cr, uid, phase, date_end, context=None):
133        if context is None:
134             context = {}
135        """
136        Check And Compute date_end of phase if change in date_start < older time.
137        """
138        uom_obj = self.pool.get('product.uom')
139        resource_obj = self.pool.get('resource.resource')
140        model_data_obj = self.pool.get('ir.model.data')
141        cal_obj = self.pool.get('resource.calendar')
142        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
143        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)])
144        if resource_id:
145 #            cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
146             res = resource_obj.read(cr, uid, resource_id[0], ['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 = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
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 %H:%M:%S')
154        self.write(cr, uid, [phase.id], {'date_start': dt_start, 'date_end': date_end.strftime('%Y-%m-%d %H:%M:%S')}, 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        model_data_obj = self.pool.get('ir.model.data')
164        resource_obj = self.pool.get('resource.resource')
165        cal_obj = self.pool.get('resource.calendar')
166        calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
167        resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)], context=context)
168        if resource_id:
169 #            cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
170             res = resource_obj.read(cr, uid, resource_id[0], ['calendar_id'], context=context)[0]
171             cal_id = res.get('calendar_id', False) and res.get('calendar_id')[0] or False
172             if cal_id:
173                 calendar_id = cal_id
174        default_uom_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
175        avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
176        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)
177        dt_end = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
178        self.write(cr, uid, [phase.id], {'date_start': date_start.strftime('%Y-%m-%d %H:%M:%S'), 'date_end': dt_end}, context=context)
179
180     def write(self, cr, uid, ids, vals, context=None):
181         resource_calendar_obj = self.pool.get('resource.calendar')
182         resource_obj = self.pool.get('resource.resource')
183         uom_obj = self.pool.get('product.uom')
184         model_data_obj = self.pool.get('ir.model.data')
185         if context is None:
186             context = {}
187         if context.get('scheduler',False):
188             return super(project_phase, self).write(cr, uid, ids, vals, context=context)
189         # Consider calendar and efficiency if the phase is performed by a resource
190         # otherwise consider the project's working calendar
191         phase = self.browse(cr, uid, ids[0], context=context)
192         calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
193         resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
194         if resource_id:
195                 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
196                 if cal_id:
197                     calendar_id = cal_id
198         default_uom_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
199         avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
200
201         # Change the date_start and date_end
202         # for previous and next phases respectively based on valid condition
203         if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
204                 dt_start = mx.DateTime.strptime(vals['date_start'], '%Y-%m-%d %H:%M:%S')
205                 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)
206                 if work_times:
207                     vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
208                 for prv_phase in phase.previous_phase_ids:
209                     self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
210         if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
211                 dt_end = mx.DateTime.strptime(vals['date_end'],'%Y-%m-%d %H:%M:%S')
212                 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)
213                 if work_times:
214                     vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d %H:%M:%S')
215                 for next_phase in phase.next_phase_ids:
216                     self._check_date_end(cr, uid, next_phase, dt_end, context=context)
217         return super(project_phase, self).write(cr, uid, ids, vals, context=context)
218
219     def set_draft(self, cr, uid, ids, *args):
220         self.write(cr, uid, ids, {'state': 'draft'})
221         return True
222
223     def set_open(self, cr, uid, ids, *args):
224         self.write(cr, uid, ids, {'state': 'open'})
225         return True
226
227     def set_pending(self, cr, uid, ids, *args):
228         self.write(cr, uid, ids, {'state': 'pending'})
229         return True
230
231     def set_cancel(self, cr, uid, ids, *args):
232         self.write(cr, uid, ids, {'state': 'cancelled'})
233         return True
234
235     def set_done(self, cr, uid, ids, *args):
236         self.write(cr, uid, ids, {'state': 'done'})
237         return True
238
239 project_phase()
240
241 class project_resource_allocation(osv.osv):
242     _name = 'project.resource.allocation'
243     _description = 'Project Resource Allocation'
244     _rec_name = 'resource_id'
245     _columns = {
246         'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
247         'phase_id': fields.many2one('project.phase', 'Project Phase', required=True),
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"),
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: