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