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