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