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 ##############################################################################
22 from datetime import datetime
23 from tools.translate import _
24 from osv import fields, osv
25 from resource.faces import task as Task
27 class project_phase(osv.osv):
28 _name = "project.phase"
29 _description = "Project Phase"
31 def _check_recursion(self, cr, uid, ids, context=None):
35 data_phase = self.browse(cr, uid, ids[0], context=context)
36 prev_ids = data_phase.previous_phase_ids
37 next_ids = data_phase.next_phase_ids
38 # it should neither be in prev_ids nor in next_ids
39 if (data_phase in prev_ids) or (data_phase in next_ids):
41 ids = [id for id in prev_ids if id in next_ids]
42 # both prev_ids and next_ids must be unique
46 prev_ids = [rec.id for rec in prev_ids]
47 next_ids = [rec.id for rec in next_ids]
50 cr.execute('SELECT distinct prv_phase_id FROM project_phase_rel WHERE next_phase_id IN %s', (tuple(prev_ids),))
51 prv_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
52 if data_phase.id in prv_phase_ids:
54 ids = [id for id in prv_phase_ids if id in next_ids]
57 prev_ids = prv_phase_ids
60 cr.execute('SELECT distinct next_phase_id FROM project_phase_rel WHERE prv_phase_id IN %s', (tuple(next_ids),))
61 next_phase_ids = filter(None, map(lambda x: x[0], cr.fetchall()))
62 if data_phase.id in next_phase_ids:
64 ids = [id for id in next_phase_ids if id in prev_ids]
67 next_ids = next_phase_ids
70 def _check_dates(self, cr, uid, ids, context=None):
71 for phase in self.read(cr, uid, ids, ['date_start', 'date_end'], context=context):
72 if phase['date_start'] and phase['date_end'] and phase['date_start'] > phase['date_end']:
76 def _get_default_uom_id(self, cr, uid):
77 model_data_obj = self.pool.get('ir.model.data')
78 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
79 return model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
81 def _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
85 for phase in self.browse(cr, uid, ids, context=context):
86 if phase.state=='done':
89 elif phase.state=="cancelled":
92 elif not phase.task_ids:
97 for task in phase.task_ids:
98 tot += task.total_hours
99 done += min(task.effective_hours, task.total_hours)
104 res[phase.id] = round(100.0 * done / tot, 2)
108 'name': fields.char("Name", size=64, required=True),
109 'date_start': fields.datetime('Start Date', help="It's computed by the scheduler according the project date or the end date of the previous phase.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
110 'date_end': fields.datetime('End Date', help=" It's computed by the scheduler according to the start date and the duration.", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
111 'constraint_date_start': fields.datetime('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
112 'constraint_date_end': fields.datetime('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
113 'project_id': fields.many2one('project.project', 'Project', required=True),
114 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
115 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
116 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of phases."),
117 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
118 '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)]}),
119 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
120 'user_force_ids': fields.many2many('res.users', string='Force Assigned Users'),
121 'user_ids': fields.one2many('project.user.allocation', 'phase_id', "Assigned Users",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]},
122 help="The ressources on the project can be computed automatically by the scheduler"),
123 'state': fields.selection([('draft', 'New'), ('open', 'In Progress'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
124 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.\
125 \n If the phase is over, the states is set to \'Done\'.'),
126 'progress': fields.function(_compute_progress, string='Progress', help="Computed based on related tasks"),
131 'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
133 _order = "project_id, date_start, sequence"
135 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
136 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
139 def onchange_project(self, cr, uid, ids, project, context=None):
142 def copy(self, cr, uid, id, default=None, context=None):
145 if not default.get('name', False):
146 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
147 return super(project_phase, self).copy(cr, uid, id, default, context)
149 def set_draft(self, cr, uid, ids, *args):
150 self.write(cr, uid, ids, {'state': 'draft'})
153 def set_open(self, cr, uid, ids, *args):
154 self.write(cr, uid, ids, {'state': 'open'})
157 def set_pending(self, cr, uid, ids, *args):
158 self.write(cr, uid, ids, {'state': 'pending'})
161 def set_cancel(self, cr, uid, ids, *args):
162 self.write(cr, uid, ids, {'state': 'cancelled'})
165 def set_done(self, cr, uid, ids, *args):
166 self.write(cr, uid, ids, {'state': 'done'})
169 def generate_phase(self, cr, uid, phases, context=None):
170 context = context or {}
173 task_pool = self.pool.get('project.task')
175 if phase.state in ('done','cancelled'):
178 'days': 'd', 'day': 'd', 'd':'d',
179 'months': 'm', 'month':'month', 'm':'m',
180 'weeks': 'w', 'week': 'w', 'w':'w',
181 'hours': 'H', 'hour': 'H', 'h':'H',
182 }.get(phase.product_uom.name.lower(), "h")
183 duration = str(phase.duration) + duration_uom
186 effort = \"%s\"''' % (phase.id, duration)
188 if phase.constraint_date_start:
189 start.append('datetime.datetime.strptime("'+str(phase.constraint_date_start)+'", "%Y-%m-%d %H:%M:%S")')
190 for previous_phase in phase.previous_phase_ids:
191 start.append("up.Phase_%s.end" % (previous_phase.id,))
195 ''' % (','.join(start))
197 if phase.user_force_ids:
200 ''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
202 result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
208 class project_user_allocation(osv.osv):
209 _name = 'project.user.allocation'
210 _description = 'Phase User Allocation'
211 _rec_name = 'user_id'
213 'user_id': fields.many2one('res.users', 'User', required=True),
214 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
215 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
216 'date_start': fields.datetime('Start Date', help="Starting Date"),
217 'date_end': fields.datetime('End Date', help="Ending Date"),
219 project_user_allocation()
221 class project(osv.osv):
222 _inherit = "project.project"
224 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
226 def schedule_phases(self, cr, uid, ids, context=None):
227 context = context or {}
228 if type(ids) in (long, int,):
230 projects = self.browse(cr, uid, ids, context=context)
231 result = self._schedule_header(cr, uid, ids, context=context)
232 for project in projects:
233 result += self._schedule_project(cr, uid, project, context=context)
234 result += self.pool.get('project.phase').generate_phase(cr, uid, project.phase_ids, context=context)
237 exec result in local_dict
238 projects_gantt = Task.BalancedProject(local_dict['Project'])
240 for project in projects:
241 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
242 for phase in project.phase_ids:
243 if phase.state in ('done','cancelled'):
245 # Maybe it's better to update than unlink/create if it already exists ?
246 p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
248 self.pool.get('project.user.allocation').unlink(cr, uid,
249 [x.id for x in phase.user_ids],
253 for r in p.booked_resource:
254 self.pool.get('project.user.allocation').create(cr, uid, {
255 'user_id': int(r.name[5:]),
256 'phase_id': phase.id,
257 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
258 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
260 self.pool.get('project.phase').write(cr, uid, [phase.id], {
261 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
262 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
267 class project_task(osv.osv):
268 _inherit = "project.task"
270 'phase_id': fields.many2one('project.phase', 'Project Phase'),
274 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: