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