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