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 openerp.tools.translate import _
24 from openerp.osv import fields, osv
25 from openerp.addons.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 _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
80 for phase in self.browse(cr, uid, ids, context=context):
81 if phase.state=='done':
84 elif phase.state=="cancelled":
87 elif not phase.task_ids:
92 for task in phase.task_ids:
93 tot += task.total_hours
94 done += min(task.effective_hours, task.total_hours)
99 res[phase.id] = round(100.0 * done / tot, 2)
103 'name': fields.char("Name", size=64, required=True),
104 'date_start': fields.datetime('Start Date', select=True, 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)]}),
105 '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)]}),
106 'constraint_date_start': fields.datetime('Minimum Start Date', help='force the phase to start after this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
107 'constraint_date_end': fields.datetime('Deadline', help='force the phase to finish before this date', states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
108 'project_id': fields.many2one('project.project', 'Project', required=True, select=True),
109 'next_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'prv_phase_id', 'next_phase_id', 'Next Phases', states={'cancelled':[('readonly',True)]}),
110 'previous_phase_ids': fields.many2many('project.phase', 'project_phase_rel', 'next_phase_id', 'prv_phase_id', 'Previous Phases', states={'cancelled':[('readonly',True)]}),
111 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of phases."),
112 'duration': fields.float('Duration', required=True, help="By default in days", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
113 'product_uom': fields.many2one('product.uom', 'Duration Unit of Measure', required=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for Duration", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
114 'task_ids': fields.one2many('project.task', 'phase_id', "Project Tasks", states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]}),
115 'user_force_ids': fields.many2many('res.users', string='Force Assigned Users'),
116 'user_ids': fields.one2many('project.user.allocation', 'phase_id', "Assigned Users",states={'done':[('readonly',True)], 'cancelled':[('readonly',True)]},
117 help="The resources on the project can be computed automatically by the scheduler."),
118 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'), ('pending', 'Pending'), ('done', 'Done')], 'Status', readonly=True, required=True,
119 help='If the phase is created the status \'Draft\'.\n If the phase is started, the status becomes \'In Progress\'.\n If review is needed the phase is in \'Pending\' status.\
120 \n If the phase is over, the status is set to \'Done\'.'),
121 'progress': fields.function(_compute_progress, string='Progress', help="Computed based on related tasks"),
127 _order = "project_id, date_start, sequence"
129 (_check_recursion,'Loops in phases not allowed',['next_phase_ids', 'previous_phase_ids']),
130 (_check_dates, 'Phase start-date must be lower than phase end-date.', ['date_start', 'date_end']),
133 def onchange_project(self, cr, uid, ids, project, context=None):
136 def copy(self, cr, uid, id, default=None, context=None):
139 if not default.get('name', False):
140 default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
141 return super(project_phase, self).copy(cr, uid, id, default, context)
143 def set_draft(self, cr, uid, ids, *args):
144 self.write(cr, uid, ids, {'state': 'draft'})
147 def set_open(self, cr, uid, ids, *args):
148 self.write(cr, uid, ids, {'state': 'open'})
151 def set_pending(self, cr, uid, ids, *args):
152 self.write(cr, uid, ids, {'state': 'pending'})
155 def set_cancel(self, cr, uid, ids, *args):
156 self.write(cr, uid, ids, {'state': 'cancelled'})
159 def set_done(self, cr, uid, ids, *args):
160 self.write(cr, uid, ids, {'state': 'done'})
163 def generate_phase(self, cr, uid, phases, context=None):
164 context = context or {}
167 task_pool = self.pool.get('project.task')
169 if phase.state in ('done','cancelled'):
171 # FIXME: brittle and not working if context['lang'] != 'en_US'
173 'day(s)': 'd', 'days': 'd', 'day': 'd', 'd':'d',
174 'month(s)': 'm', 'months': 'm', 'month':'month', 'm':'m',
175 'week(s)': 'w', 'weeks': 'w', 'week': 'w', 'w':'w',
176 'hour(s)': 'H', 'hours': 'H', 'hour': 'H', 'h':'H',
177 }.get(phase.product_uom.name.lower(), "H")
178 duration = str(phase.duration) + duration_uom
181 effort = \"%s\"''' % (phase.id, duration)
183 if phase.constraint_date_start:
184 start.append('datetime.datetime.strptime("'+str(phase.constraint_date_start)+'", "%Y-%m-%d %H:%M:%S")')
185 for previous_phase in phase.previous_phase_ids:
186 start.append("up.Phase_%s.end" % (previous_phase.id,))
190 ''' % (','.join(start))
192 if phase.user_force_ids:
195 ''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
197 result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
202 class project_user_allocation(osv.osv):
203 _name = 'project.user.allocation'
204 _description = 'Phase User Allocation'
205 _rec_name = 'user_id'
207 'user_id': fields.many2one('res.users', 'User', required=True),
208 'phase_id': fields.many2one('project.phase', 'Project Phase', ondelete='cascade', required=True),
209 'project_id': fields.related('phase_id', 'project_id', type='many2one', relation="project.project", string='Project', store=True),
210 'date_start': fields.datetime('Start Date', help="Starting Date"),
211 'date_end': fields.datetime('End Date', help="Ending Date"),
214 class project(osv.osv):
215 _inherit = "project.project"
217 def _phase_count(self, cr, uid, ids, field_name, arg, context=None):
218 res = dict.fromkeys(ids, 0)
219 phase_ids = self.pool.get('project.phase').search(cr, uid, [('project_id', 'in', ids)])
220 for phase in self.pool.get('project.phase').browse(cr, uid, phase_ids, context):
221 res[phase.project_id.id] += 1
225 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
226 'phase_count': fields.function(_phase_count, type='integer', string="Open Phases"),
229 def schedule_phases(self, cr, uid, ids, context=None):
230 context = context or {}
231 if type(ids) in (long, int,):
233 projects = self.browse(cr, uid, ids, context=context)
234 result = self._schedule_header(cr, uid, ids, context=context)
235 for project in projects:
236 result += self._schedule_project(cr, uid, project, context=context)
237 result += self.pool.get('project.phase').generate_phase(cr, uid, project.phase_ids, context=context)
240 exec result in local_dict
241 projects_gantt = Task.BalancedProject(local_dict['Project'])
243 for project in projects:
244 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
245 for phase in project.phase_ids:
246 if phase.state in ('done','cancelled'):
248 # Maybe it's better to update than unlink/create if it already exists ?
249 p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
251 self.pool.get('project.user.allocation').unlink(cr, uid,
252 [x.id for x in phase.user_ids],
256 for r in p.booked_resource:
257 self.pool.get('project.user.allocation').create(cr, uid, {
258 'user_id': int(r.name[5:]),
259 'phase_id': phase.id,
260 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
261 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
263 self.pool.get('project.phase').write(cr, uid, [phase.id], {
264 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
265 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
269 class account_analytic_account(osv.osv):
270 _inherit = 'account.analytic.account'
271 _description = 'Analytic Account'
273 'use_phases': fields.boolean('Phases', help="Check this field if you plan to use phase-based scheduling"),
276 def on_change_template(self, cr, uid, ids, template_id, context=None):
277 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
278 if template_id and 'value' in res:
279 template = self.browse(cr, uid, template_id, context=context)
280 res['value']['use_phases'] = template.use_phases
284 def _trigger_project_creation(self, cr, uid, vals, context=None):
285 if context is None: context = {}
286 res = super(account_analytic_account, self)._trigger_project_creation(cr, uid, vals, context=context)
287 return res or (vals.get('use_phases') and not 'project_creation_in_progress' in context)
290 class project_task(osv.osv):
291 _inherit = "project.task"
293 'phase_id': fields.many2one('project.phase', 'Project Phase', domain="[('project_id', '=', project_id)]"),
296 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: