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