1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
21 from lxml import etree
25 from tools.translate import _
26 from osv import fields, osv
28 class project_phase(osv.osv):
29 _name = "project.phase"
30 _description = "Project Phase"
32 def _check_recursion(self, cr, uid, ids, context=None):
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):
42 ids = [id for id in prev_ids if id in next_ids]
43 # both prev_ids and next_ids must be unique
47 prev_ids = [rec.id for rec in prev_ids]
48 next_ids = [rec.id for rec in next_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:
55 ids = [id for id in prv_phase_ids if id in next_ids]
58 prev_ids = prv_phase_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:
65 ids = [id for id in next_phase_ids if id in prev_ids]
68 next_ids = next_phase_ids
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']:
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']:
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']:
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']
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\'.')
114 'responsible_id': lambda obj,cr,uid,context: uid,
117 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
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']),
127 def onchange_project(self, cr, uid, ids, project, context=None):
129 project_obj = self.pool.get('project.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': []}}
137 def _check_date_start(self, cr, uid, phase, date_end, context=None):
141 Check And Compute date_end of phase if change in date_start < older time.
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)])
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
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)
160 def _check_date_end(self, cr, uid, phase, date_start, context=None):
164 Check And Compute date_end of phase if change in date_end > older time.
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)
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
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)
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')
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 phase = self.browse(cr, uid, ids[0], context=context)
194 calendar_id = phase.project_id.resource_calendar_id and phase.project_id.resource_calendar_id.id or False
195 resource_id = resource_obj.search(cr, uid, [('user_id', '=', phase.responsible_id.id)],context=context)
197 cal_id = resource_obj.browse(cr, uid, resource_id[0], context=context).calendar_id.id
200 default_uom_id = self._get_default_uom_id(cr, uid)
201 avg_hours = uom_obj._compute_qty(cr, uid, phase.product_uom.id, phase.duration, default_uom_id)
203 # Change the date_start and date_end
204 # for previous and next phases respectively based on valid condition
205 if vals.get('date_start', False) and vals['date_start'] < phase.date_start:
206 dt_start = mx.DateTime.strptime(vals['date_start'], '%Y-%m-%d')
207 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)
209 vals['date_end'] = work_times[-1][1].strftime('%Y-%m-%d')
210 for prv_phase in phase.previous_phase_ids:
211 self._check_date_start(cr, uid, prv_phase, dt_start, context=context)
212 if vals.get('date_end', False) and vals['date_end'] > phase.date_end:
213 dt_end = mx.DateTime.strptime(vals['date_end'],'%Y-%m-%d')
214 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)
216 vals['date_start'] = work_times[0][0].strftime('%Y-%m-%d')
217 for next_phase in phase.next_phase_ids:
218 self._check_date_end(cr, uid, next_phase, dt_end, context=context)
219 return super(project_phase, self).write(cr, uid, ids, vals, context=context)
221 def set_draft(self, cr, uid, ids, *args):
222 self.write(cr, uid, ids, {'state': 'draft'})
225 def set_open(self, cr, uid, ids, *args):
226 self.write(cr, uid, ids, {'state': 'open'})
229 def set_pending(self, cr, uid, ids, *args):
230 self.write(cr, uid, ids, {'state': 'pending'})
233 def set_cancel(self, cr, uid, ids, *args):
234 self.write(cr, uid, ids, {'state': 'cancelled'})
237 def set_done(self, cr, uid, ids, *args):
238 self.write(cr, uid, ids, {'state': 'done'})
243 class project_resource_allocation(osv.osv):
244 _name = 'project.resource.allocation'
245 _description = 'Project Resource Allocation'
246 _rec_name = 'resource_id'
248 'resource_id': fields.many2one('resource.resource', 'Resource', required=True),
249 'phase_id': fields.many2one('project.phase', 'Project Phase', required=True),
250 'phase_id_date_start': fields.related('phase_id', 'date_start', type='date', string='Starting Date of the phase'),
251 'phase_id_date_end': fields.related('phase_id', 'date_end', type='date', string='Ending Date of the phase'),
252 'useability': fields.float('Usability', help="Usability of this resource for this project phase in percentage (=50%)"),
258 project_resource_allocation()
260 class project(osv.osv):
261 _inherit = "project.project"
263 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
264 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
270 _inherit = "project.task"
272 'phase_id': fields.many2one('project.phase', 'Project Phase'),
276 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: