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