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