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