[IMP]
[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 from openerp.addons.resource.faces import task as Task
29
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 #    _name = 'project.project'
33 #project_project()
34
35 class project_task_type(osv.osv):
36     _name = 'project.task.type'
37     _description = 'Task Stage'
38     _order = 'sequence'
39     _columns = {
40         'name': fields.char('Stage Name', required=True, size=64, translate=True),
41         'description': fields.text('Description'),
42         'sequence': fields.integer('Sequence'),
43         'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44         'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
45     }
46     _defaults = {
47         'sequence': 1
48     }
49     _order = 'sequence'
50 project_task_type()
51
52 class project(osv.osv):
53     _name = "project.project"
54     _description = "Project"
55     _inherits = {'account.analytic.account': "analytic_account_id"}
56     _inherit = ['ir.needaction_mixin', 'mail.thread']
57
58     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
59         if user == 1:
60             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
61         if context and context.get('user_preference'):
62                 cr.execute("""SELECT project.id FROM project_project project
63                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
64                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
65                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
66                 return [(r[0]) for r in cr.fetchall()]
67         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
68             context=context, count=count)
69
70     def _complete_name(self, cr, uid, ids, name, args, context=None):
71         res = {}
72         for m in self.browse(cr, uid, ids, context=context):
73             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
74         return res
75
76     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
77         partner_obj = self.pool.get('res.partner')
78         if not part:
79             return {'value':{}}
80         val = {}
81         if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82             pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83             pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84             val['pricelist_id'] = pricelist_id
85         return {'value': val}
86
87     def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
88         tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
89         project_ids = [task.project_id.id for task in tasks if task.project_id]
90         return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
91
92     def _get_project_and_parents(self, cr, uid, ids, context=None):
93         """ return the project ids and all their parent projects """
94         res = set(ids)
95         while ids:
96             cr.execute("""
97                 SELECT DISTINCT parent.id
98                 FROM project_project project, project_project parent, account_analytic_account account
99                 WHERE project.analytic_account_id = account.id
100                 AND parent.analytic_account_id = account.parent_id
101                 AND project.id IN %s
102                 """, (tuple(ids),))
103             ids = [t[0] for t in cr.fetchall()]
104             res.update(ids)
105         return list(res)
106
107     def _get_project_and_children(self, cr, uid, ids, context=None):
108         """ retrieve all children projects of project ids;
109             return a dictionary mapping each project to its parent project (or None)
110         """
111         res = dict.fromkeys(ids, None)
112         while ids:
113             cr.execute("""
114                 SELECT project.id, parent.id
115                 FROM project_project project, project_project parent, account_analytic_account account
116                 WHERE project.analytic_account_id = account.id
117                 AND parent.analytic_account_id = account.parent_id
118                 AND parent.id IN %s
119                 """, (tuple(ids),))
120             dic = dict(cr.fetchall())
121             res.update(dic)
122             ids = dic.keys()
123         return res
124
125     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
126         child_parent = self._get_project_and_children(cr, uid, ids, context)
127         # compute planned_hours, total_hours, effective_hours specific to each project
128         cr.execute("""
129             SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
130                 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
131             FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
132             GROUP BY project_id
133             """, (tuple(child_parent.keys()),))
134         # aggregate results into res
135         res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
136         for id, planned, total, effective in cr.fetchall():
137             # add the values specific to id to all parent projects of id in the result
138             while id:
139                 if id in ids:
140                     res[id]['planned_hours'] += planned
141                     res[id]['total_hours'] += total
142                     res[id]['effective_hours'] += effective
143                 id = child_parent[id]
144         # compute progress rates
145         for id in ids:
146             if res[id]['total_hours']:
147                 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
148             else:
149                 res[id]['progress_rate'] = 0.0
150         return res
151
152     def unlink(self, cr, uid, ids, *args, **kwargs):
153         for proj in self.browse(cr, uid, ids):
154             if proj.tasks:
155                 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
156         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
157     
158     def _open_task(self, cr, uid, ids, field_name, arg, context=None):
159         open_task={}
160         task_pool=self.pool.get('project.task')
161         for id in ids:
162             task_ids = task_pool.search(cr, uid, [('project_id', '=', id)])
163             open_task[id] = len(task_ids)
164         return open_task
165
166     _columns = {
167         'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
168         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
169         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
170         '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),
171         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
172         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive an email each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
173
174         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
175             help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
176         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
177         'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
178             store = {
179                 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
180                 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
181             }),
182         'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
183             store = {
184                 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
185                 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
186             }),
187         'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
188             store = {
189                 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
190                 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
191             }),
192         'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
193             store = {
194                 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
195                 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20),
196             }),
197         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
198         '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)]}),
199         '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)]}),
200         '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)]}),
201         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
202         'task': fields.boolean('Task',help = "If you check this field tasks appears in kanban view"),
203         'open_task': fields.function(_open_task , type='integer',string="Open Tasks"),
204         'color': fields.integer('Color Index'),
205      }
206     def dummy(self, cr, uid, ids, context=None):
207             return False
208          
209     def open_tasks(self, cr, uid, ids, context=None):
210         #Open the View for the Tasks for the project
211         """
212         This opens Tasks views
213         @return :Dictionary value for task view
214         """
215         if context is None:
216             context = {}
217         value = {}
218         data_obj = self.pool.get('ir.model.data')
219         for project in self.browse(cr, uid, ids, context=context):
220             # Get Task views
221             tree_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_tree2')
222             form_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_form2')
223             calander_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_calendar')
224             search_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_search_form')
225             kanban_view = data_obj.get_object_reference(cr, uid, 'project', 'view_task_kanban')
226             context.update({
227                 #'search_default_user_id': uid,
228                 'search_default_project_id':project.id,
229                 #'search_default_open':1,
230             })
231             value = {
232                 'name': _('Task'),
233                 'context': context,
234                 'view_type': 'form',
235                 'view_mode': 'form,tree',
236                 'res_model': 'project.task',
237                 'view_id': False,
238                 'domain':[('project_id','in',ids)],
239                 'context': context,
240                 'views': [(kanban_view and kanban_view[1] or False, 'kanban'),(tree_view and tree_view[1] or False, 'tree'),(calander_view and calander_view[1] or False, 'calendar'),(form_view and form_view[1] or False, 'form')],
241                 'type': 'ir.actions.act_window',
242                 'search_view_id': search_view and search_view[1] or False,
243                 'nodestroy': True
244             }
245         return value
246     
247     def open_users(self, cr, uid, ids, context=None):
248         #Open the View for the Tasks for the project
249         """
250         This opens Tasks views
251         @return :Dictionary value for task view
252         """
253         if context is None:
254             context = {}
255         value = {}
256         data_obj = self.pool.get('ir.model.data')
257         for project in self.browse(cr, uid, ids, context=context):
258             # Get Task views
259             tree_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_tree')
260             form_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_form')
261             search_view = data_obj.get_object_reference(cr, uid, 'base', 'view_users_search')
262             
263             value = {
264                 'name': _('User'),
265                 'context': context,
266                 'view_type': 'form',
267                 'view_mode': 'form,tree',
268                 'res_model': 'res.users',
269                 'view_id': False,
270                 'context': context,
271                 'res_id': project.user_id.id,
272                 'views': [(form_view and form_view[1] or False, 'form'),(tree_view and tree_view[1] or False, 'tree')],
273                 'type': 'ir.actions.act_window',
274                 'search_view_id': search_view and search_view[1] or False,
275                 'nodestroy': True
276             }
277         return value
278     
279     def _get_type_common(self, cr, uid, context):
280         ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
281         return ids
282
283     _order = "sequence"
284     _defaults = {
285         'active': True,
286         'priority': 1,
287         'sequence': 10,
288         'type_ids': _get_type_common,
289         'task' : True,
290     }
291
292     # TODO: Why not using a SQL contraints ?
293     def _check_dates(self, cr, uid, ids, context=None):
294         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
295             if leave['date_start'] and leave['date']:
296                 if leave['date_start'] > leave['date']:
297                     return False
298         return True
299
300     _constraints = [
301         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
302     ]
303
304     def set_template(self, cr, uid, ids, context=None):
305         res = self.setActive(cr, uid, ids, value=False, context=context)
306         return res
307
308     def set_done(self, cr, uid, ids, context=None):
309         task_obj = self.pool.get('project.task')
310         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
311         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
312         self.write(cr, uid, ids, {'state':'close'}, context=context)
313         self.set_close_send_note(cr, uid, ids, context=context)
314         return True
315
316     def set_cancel(self, cr, uid, ids, context=None):
317         task_obj = self.pool.get('project.task')
318         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
319         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
320         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
321         self.set_cancel_send_note(cr, uid, ids, context=context)
322         return True
323
324     def set_pending(self, cr, uid, ids, context=None):
325         self.write(cr, uid, ids, {'state':'pending'}, context=context)
326         self.set_pending_send_note(cr, uid, ids, context=context)
327         return True
328
329     def set_open(self, cr, uid, ids, context=None):
330         self.write(cr, uid, ids, {'state':'open'}, context=context)
331         self.set_open_send_note(cr, uid, ids, context=context)
332         return True
333
334     def reset_project(self, cr, uid, ids, context=None):
335         res = self.setActive(cr, uid, ids, value=True, context=context)
336         self.set_open_send_note(cr, uid, ids, context=context)
337         return res
338
339     def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
340         """ copy and map tasks from old to new project """
341         if context is None:
342             context = {}
343         map_task_id = {}
344         task_obj = self.pool.get('project.task')
345         proj = self.browse(cr, uid, old_project_id, context=context)
346         for task in proj.tasks:
347             map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
348         self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
349         task_obj.duplicate_task(cr, uid, map_task_id, context=context)
350         return True
351
352     def copy(self, cr, uid, id, default={}, context=None):
353         if context is None:
354             context = {}
355
356         default = default or {}
357         context['active_test'] = False
358         default['state'] = 'open'
359         default['tasks'] = []
360         proj = self.browse(cr, uid, id, context=context)
361         if not default.get('name', False):
362             default['name'] = proj.name + _(' (copy)')
363
364         res = super(project, self).copy(cr, uid, id, default, context)
365         self.map_tasks(cr,uid,id,res,context)
366         return res
367
368     def duplicate_template(self, cr, uid, ids, context=None):
369         if context is None:
370             context = {}
371         data_obj = self.pool.get('ir.model.data')
372         result = []
373         for proj in self.browse(cr, uid, ids, context=context):
374             parent_id = context.get('parent_id', False)
375             context.update({'analytic_project_copy': True})
376             new_date_start = time.strftime('%Y-%m-%d')
377             new_date_end = False
378             if proj.date_start and proj.date:
379                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
380                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
381                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
382             context.update({'copy':True})
383             new_id = self.copy(cr, uid, proj.id, default = {
384                                     'name': proj.name +_(' (copy)'),
385                                     'state':'open',
386                                     'date_start':new_date_start,
387                                     'date':new_date_end,
388                                     'parent_id':parent_id}, context=context)
389             result.append(new_id)
390
391             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
392             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
393             if child_ids:
394                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
395
396         if result and len(result):
397             res_id = result[0]
398             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
399             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
400             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
401             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
402             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
403             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
404             return {
405                 'name': _('Projects'),
406                 'view_type': 'form',
407                 'view_mode': 'form,tree',
408                 'res_model': 'project.project',
409                 'view_id': False,
410                 'res_id': res_id,
411                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
412                 'type': 'ir.actions.act_window',
413                 'search_view_id': search_view['res_id'],
414                 'nodestroy': True
415             }
416
417     # set active value for a project, its sub projects and its tasks
418     def setActive(self, cr, uid, ids, value=True, context=None):
419         task_obj = self.pool.get('project.task')
420         for proj in self.browse(cr, uid, ids, context=None):
421             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
422             cr.execute('select id from project_task where project_id=%s', (proj.id,))
423             tasks_id = [x[0] for x in cr.fetchall()]
424             if tasks_id:
425                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
426             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
427             if child_ids:
428                 self.setActive(cr, uid, child_ids, value, context=None)
429         return True
430
431     def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
432         context = context or {}
433         if type(ids) in (long, int,):
434             ids = [ids]
435         projects = self.browse(cr, uid, ids, context=context)
436
437         for project in projects:
438             if (not project.members) and force_members:
439                 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
440
441         resource_pool = self.pool.get('resource.resource')
442
443         result = "from openerp.addons.resource.faces import *\n"
444         result += "import datetime\n"
445         for project in self.browse(cr, uid, ids, context=context):
446             u_ids = [i.id for i in project.members]
447             if project.user_id and (project.user_id.id not in u_ids):
448                 u_ids.append(project.user_id.id)
449             for task in project.tasks:
450                 if task.state in ('done','cancelled'):
451                     continue
452                 if task.user_id and (task.user_id.id not in u_ids):
453                     u_ids.append(task.user_id.id)
454             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
455             resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
456             for key, vals in resource_objs.items():
457                 result +='''
458 class User_%s(Resource):
459     efficiency = %s
460 ''' % (key,  vals.get('efficiency', False))
461
462         result += '''
463 def Project():
464         '''
465         return result
466
467     def _schedule_project(self, cr, uid, project, context=None):
468         resource_pool = self.pool.get('resource.resource')
469         calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
470         working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
471         # TODO: check if we need working_..., default values are ok.
472         puids = [x.id for x in project.members]
473         if project.user_id:
474             puids.append(project.user_id.id)
475         result = """
476   def Project_%d():
477     start = \'%s\'
478     working_days = %s
479     resource = %s
480 """       % (
481             project.id,
482             project.date_start, working_days,
483             '|'.join(['User_'+str(x) for x in puids])
484         )
485         vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
486         if vacation:
487             result+= """
488     vacation = %s
489 """ %   ( vacation, )
490         return result
491
492     #TODO: DO Resource allocation and compute availability
493     def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
494         if context ==  None:
495             context = {}
496         allocation = {}
497         return allocation
498
499     def schedule_tasks(self, cr, uid, ids, context=None):
500         context = context or {}
501         if type(ids) in (long, int,):
502             ids = [ids]
503         projects = self.browse(cr, uid, ids, context=context)
504         result = self._schedule_header(cr, uid, ids, False, context=context)
505         for project in projects:
506             result += self._schedule_project(cr, uid, project, context=context)
507             result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
508
509         local_dict = {}
510         exec result in local_dict
511         projects_gantt = Task.BalancedProject(local_dict['Project'])
512
513         for project in projects:
514             project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
515             for task in project.tasks:
516                 if task.state in ('done','cancelled'):
517                     continue
518
519                 p = getattr(project_gantt, 'Task_%d' % (task.id,))
520
521                 self.pool.get('project.task').write(cr, uid, [task.id], {
522                     'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
523                     'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
524                 }, context=context)
525                 if (not task.user_id) and (p.booked_resource):
526                     self.pool.get('project.task').write(cr, uid, [task.id], {
527                         'user_id': int(p.booked_resource[0].name[5:]),
528                     }, context=context)
529         return True
530
531     # ------------------------------------------------
532     # OpenChatter methods and notifications
533     # ------------------------------------------------
534     
535     def get_needaction_user_ids(self, cr, uid, ids, context=None):
536         result = dict.fromkeys(ids)
537         for obj in self.browse(cr, uid, ids, context=context):
538             result[obj.id] = []
539             if obj.state == 'draft' and obj.user_id:
540                 result[obj.id] = [obj.user_id.id]
541         return result
542
543     def message_get_subscribers(self, cr, uid, ids, context=None):
544         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
545         for obj in self.browse(cr, uid, ids, context=context):
546             if obj.user_id:
547                 sub_ids.append(obj.user_id.id)
548         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
549
550     def create(self, cr, uid, vals, context=None):
551         obj_id = super(project, self).create(cr, uid, vals, context=context)
552         self.create_send_note(cr, uid, [obj_id], context=context)
553         return obj_id
554
555     def create_send_note(self, cr, uid, ids, context=None):
556         return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
557
558     def set_open_send_note(self, cr, uid, ids, context=None):
559         message = _("Project has been <b>opened</b>.")
560         return self.message_append_note(cr, uid, ids, body=message, context=context)
561
562     def set_pending_send_note(self, cr, uid, ids, context=None):
563         message = _("Project is now <b>pending</b>.")
564         return self.message_append_note(cr, uid, ids, body=message, context=context)
565
566     def set_cancel_send_note(self, cr, uid, ids, context=None):
567         message = _("Project has been <b>cancelled</b>.")
568         return self.message_append_note(cr, uid, ids, body=message, context=context)
569
570     def set_close_send_note(self, cr, uid, ids, context=None):
571         message = _("Project has been <b>closed</b>.")
572         return self.message_append_note(cr, uid, ids, body=message, context=context)
573     
574 project()
575
576 class users(osv.osv):
577     _inherit = 'res.users'
578     _columns = {
579         'context_project_id': fields.many2one('project.project', 'Project')
580     }
581 users()
582
583 class task(osv.osv):
584     _name = "project.task"
585     _description = "Task"
586     _log_create = True
587     _date_name = "date_start"
588     _inherit = ['ir.needaction_mixin', 'mail.thread']
589
590
591     def _resolve_project_id_from_context(self, cr, uid, context=None):
592         """Return ID of project based on the value of 'project_id'
593            context key, or None if it cannot be resolved to a single project.
594         """
595         if context is None: context = {}
596         if type(context.get('project_id')) in (int, long):
597             project_id = context['project_id']
598             return project_id
599         if isinstance(context.get('project_id'), basestring):
600             project_name = context['project_id']
601             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
602             if len(project_ids) == 1:
603                 return project_ids[0][0]
604
605     def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
606         stage_obj = self.pool.get('project.task.type')
607         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
608         order = stage_obj._order
609         access_rights_uid = access_rights_uid or uid
610         if read_group_order == 'type_id desc':
611             # lame way to allow reverting search, should just work in the trivial case
612             order = '%s desc' % order
613         if project_id:
614             domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
615         else:
616             domain = ['|', ('id','in',ids), ('project_default','=',1)]
617         stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
618         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
619         # restore order of the search
620         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
621         return result
622
623     def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
624         res_users = self.pool.get('res.users')
625         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
626         access_rights_uid = access_rights_uid or uid
627         if project_id:
628             ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
629             order = res_users._order
630             # lame way to allow reverting search, should just work in the trivial case
631             if read_group_order == 'user_id desc':
632                 order = '%s desc' % order
633             # de-duplicate and apply search order
634             ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
635         result = res_users.name_get(cr, access_rights_uid, ids, context=context)
636         # restore order of the search
637         result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
638         return result
639
640     _group_by_full = {
641         'type_id': _read_group_type_id,
642         'user_id': _read_group_user_id
643     }
644
645
646     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
647         obj_project = self.pool.get('project.project')
648         for domain in args:
649             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
650                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
651                 if id and isinstance(id, (long, int)):
652                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
653                         args.append(('active', '=', False))
654         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
655
656     def _str_get(self, task, level=0, border='***', context=None):
657         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'+ \
658             border[0]+' '+(task.name or '')+'\n'+ \
659             (task.description or '')+'\n\n'
660
661     # Compute: effective_hours, total_hours, progress
662     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
663         res = {}
664         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
665         hours = dict(cr.fetchall())
666         for task in self.browse(cr, uid, ids, context=context):
667             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)}
668             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
669             res[task.id]['progress'] = 0.0
670             if (task.remaining_hours + hours.get(task.id, 0.0)):
671                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
672             if task.state in ('done','cancelled'):
673                 res[task.id]['progress'] = 100.0
674         return res
675
676
677     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
678         if remaining and not planned:
679             return {'value':{'planned_hours': remaining}}
680         return {}
681
682     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
683         return {'value':{'remaining_hours': planned - effective}}
684
685     def onchange_project(self, cr, uid, id, project_id):
686         if not project_id:
687             return {}
688         data = self.pool.get('project.project').browse(cr, uid, [project_id])
689         partner_id=data and data[0].partner_id
690         if partner_id:
691             return {'value':{'partner_id':partner_id.id}}
692         return {}
693
694     def duplicate_task(self, cr, uid, map_ids, context=None):
695         for new in map_ids.values():
696             task = self.browse(cr, uid, new, context)
697             child_ids = [ ch.id for ch in task.child_ids]
698             if task.child_ids:
699                 for child in task.child_ids:
700                     if child.id in map_ids.keys():
701                         child_ids.remove(child.id)
702                         child_ids.append(map_ids[child.id])
703
704             parent_ids = [ ch.id for ch in task.parent_ids]
705             if task.parent_ids:
706                 for parent in task.parent_ids:
707                     if parent.id in map_ids.keys():
708                         parent_ids.remove(parent.id)
709                         parent_ids.append(map_ids[parent.id])
710             #FIXME why there is already the copy and the old one
711             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
712
713     def copy_data(self, cr, uid, id, default={}, context=None):
714         default = default or {}
715         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
716         if not default.get('remaining_hours', False):
717             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
718         default['active'] = True
719         default['type_id'] = False
720         if not default.get('name', False):
721             default['name'] = self.browse(cr, uid, id, context=context).name or ''
722             if not context.get('copy',False):
723                 new_name = _("%s (copy)")%default.get('name','')
724                 default.update({'name':new_name})
725         return super(task, self).copy_data(cr, uid, id, default, context)
726
727
728     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
729         res = {}
730         for task in self.browse(cr, uid, ids, context=context):
731             res[task.id] = True
732             if task.project_id:
733                 if task.project_id.active == False or task.project_id.state == 'template':
734                     res[task.id] = False
735         return res
736
737     def _get_task(self, cr, uid, ids, context=None):
738         result = {}
739         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
740             if work.task_id: result[work.task_id.id] = True
741         return result.keys()
742
743     _columns = {
744         'active': fields.function(_is_template, 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."),
745         'name': fields.char('Task Summary', size=128, required=True, select=True),
746         'description': fields.text('Description'),
747         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
748         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
749         'type_id': fields.many2one('project.task.type', 'Stage'),
750         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
751                                   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.\
752                                   \n If the task is over, the states is set to \'Done\'.'),
753         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
754                                          help="A task's kanban state indicates special situations affecting it:\n"
755                                               " * Normal is the default situation\n"
756                                               " * Blocked indicates something is preventing the progress of this task\n"
757                                               " * Ready To Pull indicates the task is ready to be pulled to the next stage",
758                                          readonly=True, required=False),
759         'create_date': fields.datetime('Create Date', readonly=True,select=True),
760         'date_start': fields.datetime('Starting Date',select=True),
761         'date_end': fields.datetime('Ending Date',select=True),
762         'date_deadline': fields.date('Deadline',select=True),
763         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
764         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
765         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
766         'notes': fields.text('Notes'),
767         '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.'),
768         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
769             store = {
770                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
771                 'project.task.work': (_get_task, ['hours'], 10),
772             }),
773         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
774         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
775             store = {
776                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
777                 'project.task.work': (_get_task, ['hours'], 10),
778             }),
779         'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
780             store = {
781                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
782                 'project.task.work': (_get_task, ['hours'], 10),
783             }),
784         'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference between planned hours by the project manager and the total hours of the task.",
785             store = {
786                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
787                 'project.task.work': (_get_task, ['hours'], 10),
788             }),
789         'user_id': fields.many2one('res.users', 'Assigned to'),
790         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
791         'partner_id': fields.many2one('res.partner', 'Partner'),
792         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
793         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
794         'company_id': fields.many2one('res.company', 'Company'),
795         'id': fields.integer('ID', readonly=True),
796         'color': fields.integer('Color Index'),
797         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
798     }
799
800     _defaults = {
801         'state': 'draft',
802         'kanban_state': 'normal',
803         'priority': '2',
804         'progress': 0,
805         'sequence': 10,
806         'active': True,
807         'user_id': lambda obj, cr, uid, context: uid,
808         'project_id':lambda self, cr, uid, context: context.get('active_id',False),
809         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
810     }
811
812     _order = "priority, sequence, date_start, name, id"
813
814     def set_priority(self, cr, uid, ids, priority):
815         """Set task priority
816         """
817         return self.write(cr, uid, ids, {'priority' : priority})
818
819     def set_high_priority(self, cr, uid, ids, *args):
820         """Set task priority to high
821         """
822         return self.set_priority(cr, uid, ids, '1')
823
824     def set_normal_priority(self, cr, uid, ids, *args):
825         """Set task priority to normal
826         """
827         return self.set_priority(cr, uid, ids, '2')
828
829     def _check_recursion(self, cr, uid, ids, context=None):
830         for id in ids:
831             visited_branch = set()
832             visited_node = set()
833             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
834             if not res:
835                 return False
836
837         return True
838
839     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
840         if id in visited_branch: #Cycle
841             return False
842
843         if id in visited_node: #Already tested don't work one more time for nothing
844             return True
845
846         visited_branch.add(id)
847         visited_node.add(id)
848
849         #visit child using DFS
850         task = self.browse(cr, uid, id, context=context)
851         for child in task.child_ids:
852             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
853             if not res:
854                 return False
855
856         visited_branch.remove(id)
857         return True
858
859     def _check_dates(self, cr, uid, ids, context=None):
860         if context == None:
861             context = {}
862         obj_task = self.browse(cr, uid, ids[0], context=context)
863         start = obj_task.date_start or False
864         end = obj_task.date_end or False
865         if start and end :
866             if start > end:
867                 return False
868         return True
869
870     _constraints = [
871         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
872         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
873     ]
874     #
875     # Override view according to the company definition
876     #
877     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
878         users_obj = self.pool.get('res.users')
879
880         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
881         # this should be safe (no context passed to avoid side-effects)
882         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
883         tm = obj_tm and obj_tm.name or 'Hours'
884
885         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
886
887         if tm in ['Hours','Hour']:
888             return res
889
890         eview = etree.fromstring(res['arch'])
891
892         def _check_rec(eview):
893             if eview.attrib.get('widget','') == 'float_time':
894                 eview.set('widget','float')
895             for child in eview:
896                 _check_rec(child)
897             return True
898
899         _check_rec(eview)
900
901         res['arch'] = etree.tostring(eview)
902
903         for f in res['fields']:
904             if 'Hours' in res['fields'][f]['string']:
905                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
906         return res
907
908     def _check_child_task(self, cr, uid, ids, context=None):
909         if context == None:
910             context = {}
911         tasks = self.browse(cr, uid, ids, context=context)
912         for task in tasks:
913             if task.child_ids:
914                 for child in task.child_ids:
915                     if child.state in ['draft', 'open', 'pending']:
916                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
917         return True
918
919     def action_close(self, cr, uid, ids, context=None):
920         # This action open wizard to send email to partner or project manager after close task.
921         if context == None:
922             context = {}
923         task_id = len(ids) and ids[0] or False
924         self._check_child_task(cr, uid, ids, context=context)
925         if not task_id: return False
926         task = self.browse(cr, uid, task_id, context=context)
927         project = task.project_id
928         res = self.do_close(cr, uid, [task_id], context=context)
929         if project.warn_manager or project.warn_customer:
930             return {
931                 'name': _('Send Email after close task'),
932                 'view_type': 'form',
933                 'view_mode': 'form',
934                 'res_model': 'mail.compose.message',
935                 'type': 'ir.actions.act_window',
936                 'target': 'new',
937                 'nodestroy': True,
938                 'context': {'active_id': task.id,
939                             'active_model': 'project.task'}
940            }
941         return res
942
943     def do_close(self, cr, uid, ids, context={}):
944         """
945         Close Task
946         """
947         request = self.pool.get('res.request')
948         if not isinstance(ids,list): ids = [ids]
949         for task in self.browse(cr, uid, ids, context=context):
950             vals = {}
951             project = task.project_id
952             if project:
953                 # Send request to project manager
954                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
955                     request.create(cr, uid, {
956                         'name': _("Task '%s' closed") % task.name,
957                         'state': 'waiting',
958                         'act_from': uid,
959                         'act_to': project.user_id.id,
960                         'ref_partner_id': task.partner_id.id,
961                         'ref_doc1': 'project.task,%d'% (task.id,),
962                         'ref_doc2': 'project.project,%d'% (project.id,),
963                     }, context=context)
964
965             for parent_id in task.parent_ids:
966                 if parent_id.state in ('pending','draft'):
967                     reopen = True
968                     for child in parent_id.child_ids:
969                         if child.id != task.id and child.state not in ('done','cancelled'):
970                             reopen = False
971                     if reopen:
972                         self.do_reopen(cr, uid, [parent_id.id], context=context)
973             vals.update({'state': 'done'})
974             vals.update({'remaining_hours': 0.0})
975             if not task.date_end:
976                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
977             self.write(cr, uid, [task.id],vals, context=context)
978             self.do_close_send_note(cr, uid, [task.id], context)
979         return True
980
981     def do_reopen(self, cr, uid, ids, context=None):
982         request = self.pool.get('res.request')
983
984         for task in self.browse(cr, uid, ids, context=context):
985             project = task.project_id
986             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
987                 request.create(cr, uid, {
988                     'name': _("Task '%s' set in progress") % task.name,
989                     'state': 'waiting',
990                     'act_from': uid,
991                     'act_to': project.user_id.id,
992                     'ref_partner_id': task.partner_id.id,
993                     'ref_doc1': 'project.task,%d' % task.id,
994                     'ref_doc2': 'project.project,%d' % project.id,
995                 }, context=context)
996
997             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
998             self.do_open_send_note(cr, uid, [task.id], context)
999         return True
1000
1001     def do_cancel(self, cr, uid, ids, context={}):
1002         request = self.pool.get('res.request')
1003         tasks = self.browse(cr, uid, ids, context=context)
1004         self._check_child_task(cr, uid, ids, context=context)
1005         for task in tasks:
1006             project = task.project_id
1007             if project.warn_manager and project.user_id and (project.user_id.id != uid):
1008                 request.create(cr, uid, {
1009                     'name': _("Task '%s' cancelled") % task.name,
1010                     'state': 'waiting',
1011                     'act_from': uid,
1012                     'act_to': project.user_id.id,
1013                     'ref_partner_id': task.partner_id.id,
1014                     'ref_doc1': 'project.task,%d' % task.id,
1015                     'ref_doc2': 'project.project,%d' % project.id,
1016                 }, context=context)
1017             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
1018             self.do_cancel_send_note(cr, uid, [task.id], context)
1019         return True
1020
1021     def do_open(self, cr, uid, ids, context={}):
1022         if not isinstance(ids,list): ids = [ids]
1023         tasks= self.browse(cr, uid, ids, context=context)
1024         for t in tasks:
1025             data = {'state': 'open'}
1026             if not t.date_start:
1027                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
1028             self.write(cr, uid, [t.id], data, context=context)
1029             self.do_open_send_note(cr, uid, [t.id], context)
1030         return True
1031
1032     def do_draft(self, cr, uid, ids, context={}):
1033         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
1034         self.do_draft_send_note(cr, uid, ids, context)
1035         return True
1036
1037
1038     def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
1039         attachment = self.pool.get('ir.attachment')
1040         attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
1041         new_attachment_ids = []
1042         for attachment_id in attachment_ids:
1043             new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
1044         return new_attachment_ids
1045
1046
1047     def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
1048         """
1049         Delegate Task to another users.
1050         """
1051         assert delegate_data['user_id'], _("Delegated User should be specified")
1052         delegated_tasks = {}
1053         for task in self.browse(cr, uid, ids, context=context):
1054             delegated_task_id = self.copy(cr, uid, task.id, {
1055                 'name': delegate_data['name'],
1056                 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
1057                 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
1058                 'planned_hours': delegate_data['planned_hours'] or 0.0,
1059                 'parent_ids': [(6, 0, [task.id])],
1060                 'state': 'draft',
1061                 'description': delegate_data['new_task_description'] or '',
1062                 'child_ids': [],
1063                 'work_ids': []
1064             }, context=context)
1065             self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
1066             newname = delegate_data['prefix'] or ''
1067             task.write({
1068                 'remaining_hours': delegate_data['planned_hours_me'],
1069                 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
1070                 'name': newname,
1071             }, context=context)
1072             if delegate_data['state'] == 'pending':
1073                 self.do_pending(cr, uid, [task.id], context=context)
1074             elif delegate_data['state'] == 'done':
1075                 self.do_close(cr, uid, [task.id], context=context)
1076             self.do_delegation_send_note(cr, uid, [task.id], context)
1077             delegated_tasks[task.id] = delegated_task_id
1078         return delegated_tasks
1079
1080     def do_pending(self, cr, uid, ids, context={}):
1081         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
1082         self.do_pending_send_note(cr, uid, ids, context)
1083         return True
1084
1085     def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1086         for task in self.browse(cr, uid, ids, context=context):
1087             if (task.state=='draft') or (task.planned_hours==0.0):
1088                 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1089         self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1090         return True
1091
1092     def set_remaining_time_1(self, cr, uid, ids, context=None):
1093         return self.set_remaining_time(cr, uid, ids, 1.0, context)
1094
1095     def set_remaining_time_2(self, cr, uid, ids, context=None):
1096         return self.set_remaining_time(cr, uid, ids, 2.0, context)
1097
1098     def set_remaining_time_5(self, cr, uid, ids, context=None):
1099         return self.set_remaining_time(cr, uid, ids, 5.0, context)
1100
1101     def set_remaining_time_10(self, cr, uid, ids, context=None):
1102         return self.set_remaining_time(cr, uid, ids, 10.0, context)
1103
1104     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1105         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1106
1107     def set_kanban_state_normal(self, cr, uid, ids, context=None):
1108         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1109
1110     def set_kanban_state_done(self, cr, uid, ids, context=None):
1111         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1112
1113     def _change_type(self, cr, uid, ids, next, context=None):
1114         """
1115             go to the next stage
1116             if next is False, go to previous stage
1117         """
1118         for task in self.browse(cr, uid, ids):
1119             if  task.project_id.type_ids:
1120                 typeid = task.type_id.id
1121                 types_seq={}
1122                 for type in task.project_id.type_ids :
1123                     types_seq[type.id] = type.sequence
1124                 if next:
1125                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1126                 else:
1127                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1128                 sorted_types = [x[0] for x in types]
1129                 if not typeid:
1130                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1131                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1132                     index = sorted_types.index(typeid)
1133                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1134                 self.state_change_send_note(cr, uid, [task.id], context)
1135         return True
1136
1137     def next_type(self, cr, uid, ids, context=None):
1138         return self._change_type(cr, uid, ids, True, context=context)
1139
1140     def prev_type(self, cr, uid, ids, context=None):
1141         return self._change_type(cr, uid, ids, False, context=context)
1142
1143     def _store_history(self, cr, uid, ids, context=None):
1144         for task in self.browse(cr, uid, ids, context=context):
1145             self.pool.get('project.task.history').create(cr, uid, {
1146                 'task_id': task.id,
1147                 'remaining_hours': task.remaining_hours,
1148                 'planned_hours': task.planned_hours,
1149                 'kanban_state': task.kanban_state,
1150                 'type_id': task.type_id.id,
1151                 'state': task.state,
1152                 'user_id': task.user_id.id
1153
1154             }, context=context)
1155         return True
1156
1157     def create(self, cr, uid, vals, context=None):
1158         task_id = super(task, self).create(cr, uid, vals, context=context)
1159         self._store_history(cr, uid, [task_id], context=context)
1160         self.create_send_note(cr, uid, [task_id], context=context)
1161         return task_id
1162
1163     # Overridden to reset the kanban_state to normal whenever
1164     # the stage (type_id) of the task changes.
1165     def write(self, cr, uid, ids, vals, context=None):
1166         if isinstance(ids, (int, long)):
1167             ids = [ids]
1168         if vals and not 'kanban_state' in vals and 'type_id' in vals:
1169             new_stage = vals.get('type_id')
1170             vals_reset_kstate = dict(vals, kanban_state='normal')
1171             for t in self.browse(cr, uid, ids, context=context):
1172                 write_vals = vals_reset_kstate if t.type_id != new_stage else vals
1173                 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1174             result = True
1175         else:
1176             result = super(task,self).write(cr, uid, ids, vals, context=context)
1177         if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1178             self._store_history(cr, uid, ids, context=context)
1179             self.state_change_send_note(cr, uid, ids, context)
1180         return result
1181
1182     def unlink(self, cr, uid, ids, context=None):
1183         if context == None:
1184             context = {}
1185         self._check_child_task(cr, uid, ids, context=context)
1186         res = super(task, self).unlink(cr, uid, ids, context)
1187         return res
1188
1189     def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1190         context = context or {}
1191         result = ""
1192         ident = ' '*ident
1193         for task in tasks:
1194             if task.state in ('done','cancelled'):
1195                 continue
1196             result += '''
1197 %sdef Task_%s():
1198 %s  todo = \"%.2fH\"
1199 %s  effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1200             start = []
1201             for t2 in task.parent_ids:
1202                 start.append("up.Task_%s.end" % (t2.id,))
1203             if start:
1204                 result += '''
1205 %s  start = max(%s)
1206 ''' % (ident,','.join(start))
1207
1208             if task.user_id:
1209                 result += '''
1210 %s  resource = %s
1211 ''' % (ident, 'User_'+str(task.user_id.id))
1212
1213         result += "\n"
1214         return result
1215     
1216     # ---------------------------------------------------
1217     # OpenChatter methods and notifications
1218     # ---------------------------------------------------
1219     
1220     def get_needaction_user_ids(self, cr, uid, ids, context=None):
1221         result = dict.fromkeys(ids, [])
1222         for obj in self.browse(cr, uid, ids, context=context):
1223             if obj.state == 'draft' and obj.user_id:
1224                 result[obj.id] = [obj.user_id.id]
1225         return result
1226
1227     def message_get_subscribers(self, cr, uid, ids, context=None):
1228         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
1229         for obj in self.browse(cr, uid, ids, context=context):
1230             if obj.user_id:
1231                 sub_ids.append(obj.user_id.id)
1232             if obj.manager_id:
1233                 sub_ids.append(obj.manager_id.id)
1234         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
1235
1236     def create_send_note(self, cr, uid, ids, context=None):
1237         return self.message_append_note(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
1238
1239     def do_pending_send_note(self, cr, uid, ids, context=None):
1240         if not isinstance(ids,list): ids = [ids]
1241         msg = _('Task is now <b>pending</b>.')
1242         return self.message_append_note(cr, uid, ids, body=msg, context=context)
1243
1244     def do_open_send_note(self, cr, uid, ids, context=None):
1245         msg = _('Task has been <b>opened</b>.')
1246         return self.message_append_note(cr, uid, ids, body=msg, context=context)
1247
1248     def do_cancel_send_note(self, cr, uid, ids, context=None):
1249         msg = _('Task has been <b>canceled</b>.')
1250         return self.message_append_note(cr, uid, ids, body=msg, context=context)
1251
1252     def do_close_send_note(self, cr, uid, ids, context=None):
1253         msg = _('Task has been <b>closed</b>.')
1254         return self.message_append_note(cr, uid, ids, body=msg, context=context)
1255
1256     def do_draft_send_note(self, cr, uid, ids, context=None):
1257         msg = _('Task has been <b>renewed</b>.')
1258         return self.message_append_note(cr, uid, ids, body=msg, context=context)
1259
1260     def do_delegation_send_note(self, cr, uid, ids, context=None):
1261         for task in self.browse(cr, uid, ids, context=context):
1262             msg = _('Task has been <b>delegated</b> to <em>%s</em>.') % (task.user_id.name)
1263             self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1264         return True
1265
1266     def state_change_send_note(self, cr, uid, ids, context=None):
1267         for task in self.browse(cr, uid, ids, context=context):
1268             msg = _('Stage changed to <b>%s</b>') % (task.type_id.name)
1269             self.message_append_note(cr, uid, [task.id], body=msg, context=context)
1270         return True
1271
1272 task()
1273
1274 class project_work(osv.osv):
1275     _name = "project.task.work"
1276     _description = "Project Task Work"
1277     _columns = {
1278         'name': fields.char('Work summary', size=128),
1279         'date': fields.datetime('Date', select="1"),
1280         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1281         'hours': fields.float('Time Spent'),
1282         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1283         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1284     }
1285
1286     _defaults = {
1287         'user_id': lambda obj, cr, uid, context: uid,
1288         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1289     }
1290
1291     _order = "date desc"
1292     def create(self, cr, uid, vals, *args, **kwargs):
1293         if 'hours' in vals and (not vals['hours']):
1294             vals['hours'] = 0.00
1295         if 'task_id' in vals:
1296             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1297         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1298
1299     def write(self, cr, uid, ids, vals, context=None):
1300         if 'hours' in vals and (not vals['hours']):
1301             vals['hours'] = 0.00
1302         if 'hours' in vals:
1303             for work in self.browse(cr, uid, ids, context=context):
1304                 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))
1305         return super(project_work,self).write(cr, uid, ids, vals, context)
1306
1307     def unlink(self, cr, uid, ids, *args, **kwargs):
1308         for work in self.browse(cr, uid, ids):
1309             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1310         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1311 project_work()
1312
1313 class account_analytic_account(osv.osv):
1314
1315     _inherit = 'account.analytic.account'
1316     _description = 'Analytic Account'
1317
1318     def create(self, cr, uid, vals, context=None):
1319         if context is None:
1320             context = {}
1321         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1322             vals['child_ids'] = []
1323         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1324
1325     def unlink(self, cr, uid, ids, *args, **kwargs):
1326         project_obj = self.pool.get('project.project')
1327         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1328         if analytic_ids:
1329             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1330         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1331
1332 account_analytic_account()
1333
1334 #
1335 # Tasks History, used for cumulative flow charts (Lean/Agile)
1336 #
1337
1338 class project_task_history(osv.osv):
1339     _name = 'project.task.history'
1340     _description = 'History of Tasks'
1341     _rec_name = 'task_id'
1342     _log_access = False
1343     def _get_date(self, cr, uid, ids, name, arg, context=None):
1344         result = {}
1345         for history in self.browse(cr, uid, ids, context=context):
1346             if history.state in ('done','cancelled'):
1347                 result[history.id] = history.date
1348                 continue
1349             cr.execute('''select
1350                     date
1351                 from
1352                     project_task_history
1353                 where
1354                     task_id=%s and
1355                     id>%s
1356                 order by id limit 1''', (history.task_id.id, history.id))
1357             res = cr.fetchone()
1358             result[history.id] = res and res[0] or False
1359         return result
1360
1361     def _get_related_date(self, cr, uid, ids, context=None):
1362         result = []
1363         for history in self.browse(cr, uid, ids, context=context):
1364             cr.execute('''select
1365                     id
1366                 from
1367                     project_task_history
1368                 where
1369                     task_id=%s and
1370                     id<%s
1371                 order by id desc limit 1''', (history.task_id.id, history.id))
1372             res = cr.fetchone()
1373             if res:
1374                 result.append(res[0])
1375         return result
1376
1377     _columns = {
1378         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1379         'type_id': fields.many2one('project.task.type', 'Stage'),
1380         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1381         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1382         'date': fields.date('Date', select=True),
1383         'end_date': fields.function(_get_date, string='End Date', type="date", store={
1384             'project.task.history': (_get_related_date, None, 20)
1385         }),
1386         'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1387         'planned_hours': fields.float('Planned Time', digits=(16,2)),
1388         'user_id': fields.many2one('res.users', 'Responsible'),
1389     }
1390     _defaults = {
1391         'date': fields.date.context_today,
1392     }
1393 project_task_history()
1394
1395 class project_task_history_cumulative(osv.osv):
1396     _name = 'project.task.history.cumulative'
1397     _table = 'project_task_history_cumulative'
1398     _inherit = 'project.task.history'
1399     _auto = False
1400     _columns = {
1401         'end_date': fields.date('End Date'),
1402         'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1403     }
1404     def init(self, cr):
1405         cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1406             SELECT
1407                 history.date::varchar||'-'||history.history_id::varchar as id,
1408                 history.date as end_date,
1409                 *
1410             FROM (
1411                 SELECT
1412                     id as history_id,
1413                     date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1414                     task_id, type_id, user_id, kanban_state, state,
1415                     remaining_hours, planned_hours
1416                 FROM
1417                     project_task_history
1418             ) as history
1419         )
1420         """)
1421 project_task_history_cumulative()