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