fix
[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
22 from datetime import datetime
23 from tools.translate import _
24 from osv import fields, osv
25 from resource.faces import task as Task
26
27 class project_phase(osv.osv):
28     _name = "project.phase"
29     _description = "Project Phase"
30
31     def _check_recursion(self, cr, uid, ids, context=None):
32          if context is None:
33             context = {}
34
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):
40              return False
41          ids = [id for id in prev_ids if id in next_ids]
42          # both prev_ids and next_ids must be unique
43          if ids:
44              return False
45          # unrelated project
46          prev_ids = [rec.id for rec in prev_ids]
47          next_ids = [rec.id for rec in next_ids]
48          # iter prev_ids
49          while prev_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:
53                  return False
54              ids = [id for id in prv_phase_ids if id in next_ids]
55              if ids:
56                  return False
57              prev_ids = prv_phase_ids
58          # iter next_ids
59          while next_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:
63                  return False
64              ids = [id for id in next_phase_ids if id in prev_ids]
65              if ids:
66                  return False
67              next_ids = next_phase_ids
68          return True
69
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']:
73                  return False
74          return True
75
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']
80
81     def _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
82         res = {}
83         if not ids:
84             return res
85         for phase in self.browse(cr, uid, ids, context=context):
86             if phase.state=='done':
87                 res[phase.id] = 100.0
88                 continue
89             elif phase.state=="cancelled":
90                 res[phase.id] = 0.0
91                 continue
92             elif not phase.task_ids:
93                 res[phase.id] = 0.0
94                 continue
95
96             tot = done = 0.0
97             for task in phase.task_ids:
98                 tot += task.total_hours
99                 done += min(task.effective_hours, task.total_hours)
100
101             if not tot:
102                 res[phase.id] = 0.0
103             else:
104                 res[phase.id] = round(100.0 * done / tot, 2)
105         return res
106
107     _columns = {
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"),
127      }
128     _defaults = {
129         'state': 'draft',
130         'sequence': 10,
131         'product_uom': lambda self,cr,uid,c: self.pool.get('product.uom').search(cr, uid, [('name', '=', _('Day'))], context=c)[0]
132     }
133     _order = "project_id, date_start, sequence"
134     _constraints = [
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']),
137     ]
138
139     def onchange_project(self, cr, uid, ids, project, context=None):
140         return {}
141
142     def copy(self, cr, uid, id, default=None, context=None):
143         if default is None:
144             default = {}
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)
148
149     def set_draft(self, cr, uid, ids, *args):
150         self.write(cr, uid, ids, {'state': 'draft'})
151         return True
152
153     def set_open(self, cr, uid, ids, *args):
154         self.write(cr, uid, ids, {'state': 'open'})
155         return True
156
157     def set_pending(self, cr, uid, ids, *args):
158         self.write(cr, uid, ids, {'state': 'pending'})
159         return True
160
161     def set_cancel(self, cr, uid, ids, *args):
162         self.write(cr, uid, ids, {'state': 'cancelled'})
163         return True
164
165     def set_done(self, cr, uid, ids, *args):
166         self.write(cr, uid, ids, {'state': 'done'})
167         return True
168
169     def generate_phase(self, cr, uid, phases, context=None):
170         context = context or {}
171         result = ""
172
173         task_pool = self.pool.get('project.task')
174         for phase in phases:
175             if phase.state in ('done','cancelled'):
176                 continue
177             duration_uom = {
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
184             result += '''
185     def Phase_%s():
186         effort = \"%s\"''' % (phase.id, duration)
187             start = []
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,))
192             if start:
193                 result += '''
194         start = max(%s)
195 ''' % (','.join(start))
196
197             if phase.user_force_ids:
198                 result += '''
199         resource = %s
200 ''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
201
202             result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
203             result += "\n"
204
205         return result
206 project_phase()
207
208 class project_user_allocation(osv.osv):
209     _name = 'project.user.allocation'
210     _description = 'Phase User Allocation'
211     _rec_name = 'user_id'
212     _columns = {
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"),
218     }
219 project_user_allocation()
220
221 class project(osv.osv):
222     _inherit = "project.project"
223     _columns = {
224         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
225     }
226     def schedule_phases(self, cr, uid, ids, context=None):
227         context = context or {}
228         if type(ids) in (long, int,):
229             ids = [ids]
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)
235
236         local_dict = {}
237         exec result in local_dict
238         projects_gantt = Task.BalancedProject(local_dict['Project'])
239
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'):
244                     continue
245                 # Maybe it's better to update than unlink/create if it already exists ?
246                 p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
247
248                 self.pool.get('project.user.allocation').unlink(cr, uid, 
249                     [x.id for x in phase.user_ids],
250                     context=context
251                 )
252
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')
259                     }, context=context)
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')
263                 }, context=context)
264         return True
265 project()
266
267 class project_task(osv.osv):
268     _inherit = "project.task"
269     _columns = {
270         'phase_id': fields.many2one('project.phase', 'Project Phase'),
271     }
272 project_task()
273
274 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: