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