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