[IMP] search_read: use search_read from the orm instead of rewritting it
[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 openerp.tools.translate import _
24 from openerp.osv import fields, osv
25 from openerp.addons.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 _compute_progress(self, cr, uid, ids, field_name, arg, context=None):
77         res = {}
78         if not ids:
79             return res
80         for phase in self.browse(cr, uid, ids, context=context):
81             if phase.state=='done':
82                 res[phase.id] = 100.0
83                 continue
84             elif phase.state=="cancelled":
85                 res[phase.id] = 0.0
86                 continue
87             elif not phase.task_ids:
88                 res[phase.id] = 0.0
89                 continue
90
91             tot = done = 0.0
92             for task in phase.task_ids:
93                 tot += task.total_hours
94                 done += min(task.effective_hours, task.total_hours)
95
96             if not tot:
97                 res[phase.id] = 0.0
98             else:
99                 res[phase.id] = round(100.0 * done / tot, 2)
100         return res
101
102     _columns = {
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"),
122      }
123     _defaults = {
124         'state': 'draft',
125         'sequence': 10,
126     }
127     _order = "project_id, date_start, sequence"
128     _constraints = [
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']),
131     ]
132
133     def onchange_project(self, cr, uid, ids, project, context=None):
134         return {}
135
136     def copy(self, cr, uid, id, default=None, context=None):
137         if default is None:
138             default = {}
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)
142
143     def set_draft(self, cr, uid, ids, *args):
144         self.write(cr, uid, ids, {'state': 'draft'})
145         return True
146
147     def set_open(self, cr, uid, ids, *args):
148         self.write(cr, uid, ids, {'state': 'open'})
149         return True
150
151     def set_pending(self, cr, uid, ids, *args):
152         self.write(cr, uid, ids, {'state': 'pending'})
153         return True
154
155     def set_cancel(self, cr, uid, ids, *args):
156         self.write(cr, uid, ids, {'state': 'cancelled'})
157         return True
158
159     def set_done(self, cr, uid, ids, *args):
160         self.write(cr, uid, ids, {'state': 'done'})
161         return True
162
163     def generate_phase(self, cr, uid, phases, context=None):
164         context = context or {}
165         result = ""
166
167         task_pool = self.pool.get('project.task')
168         for phase in phases:
169             if phase.state in ('done','cancelled'):
170                 continue
171             # FIXME: brittle and not working if context['lang'] != 'en_US'
172             duration_uom = {
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
179             result += '''
180     def Phase_%s():
181         effort = \"%s\"''' % (phase.id, duration)
182             start = []
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,))
187             if start:
188                 result += '''
189         start = max(%s)
190 ''' % (','.join(start))
191
192             if phase.user_force_ids:
193                 result += '''
194         resource = %s
195 ''' % '|'.join(map(lambda x: 'User_'+str(x.id), phase.user_force_ids))
196
197             result += task_pool._generate_task(cr, uid, phase.task_ids, ident=8, context=context)
198             result += "\n"
199
200         return result
201
202 class project_user_allocation(osv.osv):
203     _name = 'project.user.allocation'
204     _description = 'Phase User Allocation'
205     _rec_name = 'user_id'
206     _columns = {
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"),
212     }
213
214 class project(osv.osv):
215     _inherit = "project.project"
216
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
222         return res
223
224     _columns = {
225         'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
226         'phase_count': fields.function(_phase_count, type='integer', string="Open Phases"),
227     }
228
229     def schedule_phases(self, cr, uid, ids, context=None):
230         context = context or {}
231         if type(ids) in (long, int,):
232             ids = [ids]
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)
238
239         local_dict = {}
240         exec result in local_dict
241         projects_gantt = Task.BalancedProject(local_dict['Project'])
242
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'):
247                     continue
248                 # Maybe it's better to update than unlink/create if it already exists ?
249                 p = getattr(project_gantt, 'Phase_%d' % (phase.id,))
250
251                 self.pool.get('project.user.allocation').unlink(cr, uid,
252                     [x.id for x in phase.user_ids],
253                     context=context
254                 )
255
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')
262                     }, context=context)
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')
266                 }, context=context)
267         return True
268
269 class account_analytic_account(osv.osv):
270     _inherit = 'account.analytic.account'
271     _description = 'Analytic Account'
272     _columns = {
273         'use_phases': fields.boolean('Phases', help="Check this field if you plan to use phase-based scheduling"),
274     }
275
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
281         return res
282
283
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)
288
289
290 class project_task(osv.osv):
291     _inherit = "project.task"
292     _columns = {
293         'phase_id': fields.many2one('project.phase', 'Project Phase', domain="[('project_id', '=', project_id)]"),
294     }
295
296 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: