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