[IMP] project: performance issues
[odoo/odoo.git] / addons / project / project.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 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 lxml import etree
23 import time
24 from datetime import datetime, date
25
26 from tools.translate import _
27 from osv import fields, osv
28
29
30 class project_task_type(osv.osv):
31     _name = 'project.task.type'
32     _description = 'Task Stage'
33     _order = 'sequence'
34     _columns = {
35         'name': fields.char('Stage Name', required=True, size=64, translate=True),
36         'description': fields.text('Description'),
37         'sequence': fields.integer('Sequence'),
38     }
39
40     _defaults = {
41         'sequence': 1
42     }
43
44 project_task_type()
45
46 class project(osv.osv):
47     _name = "project.project"
48     _description = "Project"
49     _inherits = {'account.analytic.account': "analytic_account_id"}
50
51     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
52         if user == 1:
53             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
54         if context and context.has_key('user_prefence') and context['user_prefence']:
55                 cr.execute("""SELECT project.id FROM project_project project
56                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
57                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
58                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
59                 return [(r[0]) for r in cr.fetchall()]
60         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
61             context=context, count=count)
62
63     def _complete_name(self, cr, uid, ids, name, args, context=None):
64         res = {}
65         for m in self.browse(cr, uid, ids, context=context):
66             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
67         return res
68
69     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
70         partner_obj = self.pool.get('res.partner')
71         if not part:
72             return {'value':{'contact_id': False, 'pricelist_id': False}}
73         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
74         pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
75         pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
76         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
77
78     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
79         res = {}.fromkeys(ids, 0.0)
80         progress = {}
81         if not ids:
82             return res
83         cr.execute('''SELECT
84                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
85             FROM
86                 project_task
87             WHERE
88                 project_id in %s AND
89                 state<>'cancelled'
90             GROUP BY
91                 project_id''', (tuple(ids),))
92         progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
93         for project in self.browse(cr, uid, ids, context=context):
94             s = progress.get(project.id, (0.0,0.0,0.0,0.0))
95             res[project.id] = {
96                 'planned_hours': s[0],
97                 'effective_hours': s[2],
98                 'total_hours': s[1],
99                 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
100             }
101         return res
102
103     def _get_project_task(self, cr, uid, ids, context=None):
104         result = {}
105         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
106             if task.project_id: result[task.project_id.id] = True
107         return result.keys()
108
109     def _get_project_work(self, cr, uid, ids, context=None):
110         result = {}
111         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
112             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
113         return result.keys()
114
115     def unlink(self, cr, uid, ids, *args, **kwargs):
116         for proj in self.browse(cr, uid, ids):
117             if proj.tasks:
118                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
119         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
120
121     _columns = {
122         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
123         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
124         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
125         'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True),
126         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
127         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
128
129         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose, but a user has to be member of a project to add a the to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
130         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
131         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
132             store = {
133                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
134                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
135             }),
136         'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
137             store = {
138                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
139                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
140             }),
141         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
142             store = {
143                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
144                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
145             }),
146         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
147             store = {
148                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
149                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
150                 'project.task.work': (_get_project_work, ['hours'], 10),
151             }),
152         'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
153         'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
154         'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
155         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
156      }
157     _order = "sequence"
158     _defaults = {
159         'active': True,
160         'priority': 1,
161         'sequence': 10,
162     }
163
164     # TODO: Why not using a SQL contraints ?
165     def _check_dates(self, cr, uid, ids, context=None):
166         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
167              if leave['date_start'] and leave['date']:
168                  if leave['date_start'] > leave['date']:
169                      return False
170         return True
171
172     _constraints = [
173         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
174     ]
175
176     def set_template(self, cr, uid, ids, context=None):
177         res = self.setActive(cr, uid, ids, value=False, context=context)
178         return res
179
180     def set_done(self, cr, uid, ids, context=None):
181         task_obj = self.pool.get('project.task')
182         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
183         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
184         self.write(cr, uid, ids, {'state':'close'}, context=context)
185         for (id, name) in self.name_get(cr, uid, ids):
186             message = _("The project '%s' has been closed.") % name
187             self.log(cr, uid, id, message)
188         return True
189
190     def set_cancel(self, cr, uid, ids, context=None):
191         task_obj = self.pool.get('project.task')
192         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
193         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
194         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
195         return True
196
197     def set_pending(self, cr, uid, ids, context=None):
198         self.write(cr, uid, ids, {'state':'pending'}, context=context)
199         return True
200
201     def set_open(self, cr, uid, ids, context=None):
202         self.write(cr, uid, ids, {'state':'open'}, context=context)
203         return True
204
205     def reset_project(self, cr, uid, ids, context=None):
206         res = self.setActive(cr, uid, ids, value=True, context=context)
207         for (id, name) in self.name_get(cr, uid, ids):
208             message = _("The project '%s' has been opened.") % name
209             self.log(cr, uid, id, message)
210         return res
211
212     def copy(self, cr, uid, id, default={}, context=None):
213         if context is None:
214             context = {}
215
216         proj = self.browse(cr, uid, id, context=context)
217         default = default or {}
218         context['active_test'] = False
219         default['state'] = 'open'
220         if not default.get('name', False):
221             default['name'] = proj.name + _(' (copy)')
222         res = super(project, self).copy(cr, uid, id, default, context)
223
224         return res
225
226     def duplicate_template(self, cr, uid, ids, context=None):
227         if context is None:
228             context = {}
229         project_obj = self.pool.get('project.project')
230         data_obj = self.pool.get('ir.model.data')
231         result = []
232         for proj in self.browse(cr, uid, ids, context=context):
233             parent_id = context.get('parent_id', False)
234             context.update({'analytic_project_copy': True})
235             new_date_start = time.strftime('%Y-%m-%d')
236             new_date_end = False
237             if proj.date_start and proj.date:
238                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
239                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
240                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
241             new_id = project_obj.copy(cr, uid, proj.id, default = {
242                                     'name': proj.name +_(' (copy)'),
243                                     'state':'open',
244                                     'date_start':new_date_start,
245                                     'date':new_date_end,
246                                     'parent_id':parent_id}, context=context)
247             result.append(new_id)
248
249             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
250             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
251             if child_ids:
252                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
253
254         if result and len(result):
255             res_id = result[0]
256             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
257             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
258             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
259             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
260             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
261             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
262             return {
263                 'name': _('Projects'),
264                 'view_type': 'form',
265                 'view_mode': 'form,tree',
266                 'res_model': 'project.project',
267                 'view_id': False,
268                 'res_id': res_id,
269                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
270                 'type': 'ir.actions.act_window',
271                 'search_view_id': search_view['res_id'],
272                 'nodestroy': True
273             }
274
275     # set active value for a project, its sub projects and its tasks
276     def setActive(self, cr, uid, ids, value=True, context=None):
277         task_obj = self.pool.get('project.task')
278         for proj in self.browse(cr, uid, ids, context=None):
279             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
280             cr.execute('select id from project_task where project_id=%s', (proj.id,))
281             tasks_id = [x[0] for x in cr.fetchall()]
282             if tasks_id:
283                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
284             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
285             if child_ids:
286                 self.setActive(cr, uid, child_ids, value, context=None)
287         return True
288
289 project()
290
291 class users(osv.osv):
292     _inherit = 'res.users'
293     _columns = {
294         'context_project_id': fields.many2one('project.project', 'Project')
295     }
296 users()
297
298 class task(osv.osv):
299     _name = "project.task"
300     _description = "Task"
301     _log_create = True
302     _date_name = "date_start"
303
304     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
305         obj_project = self.pool.get('project.project')
306         for domain in args:
307             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
308                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
309                 if id and isinstance(id, (long, int)):
310                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
311                         args.append(('active', '=', False))
312         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
313
314     def _str_get(self, task, level=0, border='***', context=None):
315         return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
316             border[0]+' '+(task.name or '')+'\n'+ \
317             (task.description or '')+'\n\n'
318
319     # Compute: effective_hours, total_hours, progress
320     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
321         res = {}
322         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
323         hours = dict(cr.fetchall())
324         for task in self.browse(cr, uid, ids, context=context):
325             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
326             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
327             res[task.id]['progress'] = 0.0
328             if (task.remaining_hours + hours.get(task.id, 0.0)):
329                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
330             if task.state in ('done','cancelled'):
331                 res[task.id]['progress'] = 100.0
332         return res
333
334
335     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
336         if remaining and not planned:
337             return {'value':{'planned_hours': remaining}}
338         return {}
339
340     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
341         return {'value':{'remaining_hours': planned - effective}}
342     
343     def onchange_project(self, cr, uid, id, project_id):
344         if not project_id:
345             return {}
346         data = self.pool.get('project.project').browse(cr, uid, [project_id])
347         partner_id=data and data[0].parent_id.partner_id
348         if partner_id:
349             return {'value':{'partner_id':partner_id.id}}
350         return {}
351     
352     def _default_project(self, cr, uid, context=None):
353         if context is None:
354             context = {}
355         if 'project_id' in context and context['project_id']:
356             return int(context['project_id'])
357         return False
358
359     def copy_data(self, cr, uid, id, default={}, context=None):
360         default = default or {}
361         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
362         if not default.get('remaining_hours', False):
363             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
364         default['active'] = True
365         default['type_id'] = False
366         if not default.get('name', False):
367             default['name'] = self.browse(cr, uid, id, context=context).name
368         return super(task, self).copy_data(cr, uid, id, default, context)
369
370     def _check_dates(self, cr, uid, ids, context=None):
371         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
372         if task['date_start'] and task['date_end']:
373              if task['date_start'] > task['date_end']:
374                  return False
375         return True
376
377     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
378         res = {}
379         for task in self.browse(cr, uid, ids, context=context):
380             res[task.id] = True
381             if task.project_id:
382                 if task.project_id.active == False or task.project_id.state == 'template':
383                     res[task.id] = False
384         return res
385
386     def _get_task(self, cr, uid, ids, context=None):
387         result = {}
388         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
389             if work.task_id: result[work.task_id.id] = True
390         return result.keys()
391
392     _columns = {
393         'active': fields.function(_is_template, method=True, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
394         'name': fields.char('Task Summary', size=128, required=True),
395         'description': fields.text('Description'),
396         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
397         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
398         'type_id': fields.many2one('project.task.type', 'Stage'),
399         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
400                                   help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
401                                   \n If the task is over, the states is set to \'Done\'.'),
402         'create_date': fields.datetime('Create Date', readonly=True,select=True),
403         'date_start': fields.datetime('Starting Date',select=True),
404         'date_end': fields.datetime('Ending Date',select=True),
405         'date_deadline': fields.date('Deadline',select=True),
406         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
407         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
408         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
409         'notes': fields.text('Notes'),
410         'planned_hours': fields.float('Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
411         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
412             store = {
413                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
414                 'project.task.work': (_get_task, ['hours'], 10),
415             }),
416         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
417         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
418             store = {
419                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
420                 'project.task.work': (_get_task, ['hours'], 10),
421             }),
422         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
423             store = {
424                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
425                 'project.task.work': (_get_task, ['hours'], 10),
426             }),
427         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
428             store = {
429                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
430                 'project.task.work': (_get_task, ['hours'], 10),
431             }),
432         'user_id': fields.many2one('res.users', 'Assigned to'),
433         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
434         'partner_id': fields.many2one('res.partner', 'Partner'),
435         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
436         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
437         'company_id': fields.many2one('res.company', 'Company'),
438         'id': fields.integer('ID'),
439     }
440
441     _defaults = {
442         'state': 'draft',
443         'priority': '2',
444         'progress': 0,
445         'sequence': 10,
446         'active': True,
447         'project_id': _default_project,
448         'user_id': lambda obj, cr, uid, context: uid,
449         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
450     }
451
452     _order = "sequence,priority, date_start, name, id"
453
454     def _check_recursion(self, cr, uid, ids, context=None):
455         obj_task = self.browse(cr, uid, ids[0], context=context)
456         parent_ids = [x.id for x in obj_task.parent_ids]
457         children_ids = [x.id for x in obj_task.child_ids]
458
459         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
460             return False
461
462         while(ids):
463             cr.execute('SELECT DISTINCT task_id '\
464                        'FROM project_task_parent_rel '\
465                        'WHERE parent_id IN %s', (tuple(ids),))
466             child_ids = map(lambda x: x[0], cr.fetchall())
467             c_ids = child_ids
468             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
469                 return False
470             while len(c_ids):
471                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
472                 if (list(set(parent_ids).intersection(set(s_ids)))):
473                     return False
474                 c_ids = s_ids
475             ids = child_ids
476         return True
477
478     _constraints = [
479         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids'])
480     ]
481     #
482     # Override view according to the company definition
483     #
484
485
486     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
487         users_obj = self.pool.get('res.users')
488
489         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
490         # this should be safe (no context passed to avoid side-effects)
491         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
492         tm = obj_tm and obj_tm.name or 'Hours'
493
494         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
495
496         if tm in ['Hours','Hour']:
497             return res
498
499         eview = etree.fromstring(res['arch'])
500
501         def _check_rec(eview):
502             if eview.attrib.get('widget','') == 'float_time':
503                 eview.set('widget','float')
504             for child in eview:
505                 _check_rec(child)
506             return True
507
508         _check_rec(eview)
509
510         res['arch'] = etree.tostring(eview)
511
512         for f in res['fields']:
513             if 'Hours' in res['fields'][f]['string']:
514                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
515         return res
516
517     def action_close(self, cr, uid, ids, context=None):
518         # This action open wizard to send email to partner or project manager after close task.
519         project_id = len(ids) and ids[0] or False
520         if not project_id: return False
521         task = self.browse(cr, uid, project_id, context=context)
522         project = task.project_id
523         res = self.do_close(cr, uid, [project_id], context=context)
524         if project.warn_manager or project.warn_customer:
525            return {
526                 'name': _('Send Email after close task'),
527                 'view_type': 'form',
528                 'view_mode': 'form',
529                 'res_model': 'project.task.close',
530                 'type': 'ir.actions.act_window',
531                 'target': 'new',
532                 'nodestroy': True,
533                 'context': {'active_id': task.id}
534            }
535         return res
536
537     def do_close(self, cr, uid, ids, context=None):
538         """
539         Close Task
540         """
541         request = self.pool.get('res.request')
542         for task in self.browse(cr, uid, ids, context=context):
543             project = task.project_id
544             if project:
545                 # Send request to project manager
546                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
547                     request.create(cr, uid, {
548                         'name': _("Task '%s' closed") % task.name,
549                         'state': 'waiting',
550                         'act_from': uid,
551                         'act_to': project.user_id.id,
552                         'ref_partner_id': task.partner_id.id,
553                         'ref_doc1': 'project.task,%d'% (task.id,),
554                         'ref_doc2': 'project.project,%d'% (project.id,),
555                     })
556
557             for parent_id in task.parent_ids:
558                 if parent_id.state in ('pending','draft'):
559                     reopen = True
560                     for child in parent_id.child_ids:
561                         if child.id != task.id and child.state not in ('done','cancelled'):
562                             reopen = False
563                     if reopen:
564                         self.do_reopen(cr, uid, [parent_id.id])
565             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
566             message = _("The task '%s' is done") % (task.name,)
567             self.log(cr, uid, task.id, message)
568         return True
569
570     def do_reopen(self, cr, uid, ids, context=None):
571         request = self.pool.get('res.request')
572
573         for task in self.browse(cr, uid, ids, context=context):
574             project = task.project_id
575             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
576                 request.create(cr, uid, {
577                     'name': _("Task '%s' set in progress") % task.name,
578                     'state': 'waiting',
579                     'act_from': uid,
580                     'act_to': project.user_id.id,
581                     'ref_partner_id': task.partner_id.id,
582                     'ref_doc1': 'project.task,%d' % task.id,
583                     'ref_doc2': 'project.project,%d' % project.id,
584                 })
585
586             self.write(cr, uid, [task.id], {'state': 'open'})
587
588         return True
589
590     def do_cancel(self, cr, uid, ids, *args):
591         request = self.pool.get('res.request')
592         tasks = self.browse(cr, uid, ids)
593         for task in tasks:
594             project = task.project_id
595             if project.warn_manager and project.user_id and (project.user_id.id != uid):
596                 request.create(cr, uid, {
597                     'name': _("Task '%s' cancelled") % task.name,
598                     'state': 'waiting',
599                     'act_from': uid,
600                     'act_to': project.user_id.id,
601                     'ref_partner_id': task.partner_id.id,
602                     'ref_doc1': 'project.task,%d' % task.id,
603                     'ref_doc2': 'project.project,%d' % project.id,
604                 })
605             message = _("The task '%s' is cancelled.") % (task.name,)
606             self.log(cr, uid, task.id, message)
607             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
608         return True
609
610     def do_open(self, cr, uid, ids, *args):
611         tasks= self.browse(cr,uid,ids)
612         for t in tasks:
613             data = {'state': 'open'}
614             if not t.date_start:
615                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
616             self.write(cr, uid, [t.id], data)
617             message = _("The task '%s' is opened.") % (t.name,)
618             self.log(cr, uid, t.id, message)
619         return True
620
621     def do_draft(self, cr, uid, ids, *args):
622         self.write(cr, uid, ids, {'state': 'draft'})
623         return True
624
625     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
626         """
627         Delegate Task to another users.
628         """
629         task = self.browse(cr, uid, task_id, context=context)
630         self.copy(cr, uid, task.id, {
631             'name': delegate_data['name'],
632             'user_id': delegate_data['user_id'],
633             'planned_hours': delegate_data['planned_hours'],
634             'remaining_hours': delegate_data['planned_hours'],
635             'parent_ids': [(6, 0, [task.id])],
636             'state': 'draft',
637             'description': delegate_data['new_task_description'] or '',
638             'child_ids': [],
639             'work_ids': []
640         }, context=context)
641         newname = delegate_data['prefix'] or ''
642         self.write(cr, uid, [task.id], {
643             'remaining_hours': delegate_data['planned_hours_me'],
644             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
645             'name': newname,
646         }, context=context)
647         if delegate_data['state'] == 'pending':
648             self.do_pending(cr, uid, [task.id], context)
649         else:
650             self.do_close(cr, uid, [task.id], context=context)
651         user_pool = self.pool.get('res.users')
652         delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
653         message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
654         self.log(cr, uid, task.id, message)
655         return True
656
657     def do_pending(self, cr, uid, ids, *args):
658         self.write(cr, uid, ids, {'state': 'pending'})
659         for (id, name) in self.name_get(cr, uid, ids):
660             message = _("The task '%s' is pending.") % name
661             self.log(cr, uid, id, message)
662         return True
663
664     def next_type(self, cr, uid, ids, *args):
665         for task in self.browse(cr, uid, ids):
666             typeid = task.type_id.id
667             types = map(lambda x:x.id, task.project_id.type_ids or [])
668             if types:
669                 if not typeid:
670                     self.write(cr, uid, task.id, {'type_id': types[0]})
671                 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
672                     index = types.index(typeid)
673                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
674         return True
675
676     def prev_type(self, cr, uid, ids, *args):
677         for task in self.browse(cr, uid, ids):
678             typeid = task.type_id.id
679             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
680             if types:
681                 if typeid and typeid in types:
682                     index = types.index(typeid)
683                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
684         return True
685
686 task()
687
688 class project_work(osv.osv):
689     _name = "project.task.work"
690     _description = "Project Task Work"
691     _columns = {
692         'name': fields.char('Work summary', size=128),
693         'date': fields.datetime('Date'),
694         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
695         'hours': fields.float('Time Spent'),
696         'user_id': fields.many2one('res.users', 'Done by', required=True),
697         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
698     }
699
700     _defaults = {
701         'user_id': lambda obj, cr, uid, context: uid,
702         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
703     }
704
705     _order = "date desc"
706     def create(self, cr, uid, vals, *args, **kwargs):
707         if 'hours' in vals and (not vals['hours']):
708             vals['hours'] = 0.00
709         if 'task_id' in vals:
710             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
711         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
712
713     def write(self, cr, uid, ids, vals, context=None):
714         if 'hours' in vals and (not vals['hours']):
715             vals['hours'] = 0.00
716         if 'hours' in vals:
717             for work in self.browse(cr, uid, ids, context=context):
718                 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
719         return super(project_work,self).write(cr, uid, ids, vals, context)
720
721     def unlink(self, cr, uid, ids, *args, **kwargs):
722         for work in self.browse(cr, uid, ids):
723             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
724         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
725 project_work()
726
727 class account_analytic_account(osv.osv):
728
729     _inherit = 'account.analytic.account'
730     _description = 'Analytic Account'
731
732     def create(self, cr, uid, vals, context=None):
733         if context is None:
734             context = {}
735         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
736             vals['child_ids'] = []
737         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
738
739 account_analytic_account()
740
741 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: