[CLEAN] Cleaned code.
[odoo/odoo.git] / addons / project / project.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from lxml import etree
23 import time
24 from datetime import datetime, date
25
26 from tools.translate import _
27 from osv import fields, osv
28 from openerp.addons.resource.faces import task as Task
29
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 #    _name = 'project.project'
33 #project_project()
34
35 class project_task_type(osv.osv):
36     _name = 'project.task.type'
37     _description = 'Task Stage'
38     _order = 'sequence'
39     _columns = {
40         'name': fields.char('Stage Name', required=True, size=64, translate=True),
41         'description': fields.text('Description'),
42         'sequence': fields.integer('Sequence'),
43         'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44         'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
45     }
46     _defaults = {
47         'sequence': 1
48     }
49     _order = 'sequence'
50 project_task_type()
51
52 class project(osv.osv):
53     _name = "project.project"
54     _description = "Project"
55     _inherits = {'account.analytic.account': "analytic_account_id"}
56     _inherit = ['ir.needaction', 'mail.thread']
57
58     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
59         if user == 1:
60             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
61         if context and context.get('user_preference'):
62                 cr.execute("""SELECT project.id FROM project_project project
63                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
64                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
65                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
66                 return [(r[0]) for r in cr.fetchall()]
67         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
68             context=context, count=count)
69
70     def _complete_name(self, cr, uid, ids, name, args, context=None):
71         res = {}
72         for m in self.browse(cr, uid, ids, context=context):
73             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
74         return res
75
76     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
77         partner_obj = self.pool.get('res.partner')
78         if not part:
79             return {'value':{}}
80         val = {}
81         if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82             pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83             pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84             val['pricelist_id'] = pricelist_id
85         return {'value': val}
86
87     def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
88         tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
89         project_ids = [task.project_id.id for task in tasks if task.project_id]
90         return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
91
92     def _get_project_and_parents(self, cr, uid, ids, context=None):
93         """ return the project ids and all their parent projects """
94         res = set(ids)
95         while ids:
96             cr.execute("""
97                 SELECT DISTINCT parent.id
98                 FROM project_project project, project_project parent, account_analytic_account account
99                 WHERE project.analytic_account_id = account.id
100                 AND parent.analytic_account_id = account.parent_id
101                 AND project.id IN %s
102                 """, (tuple(ids),))
103             ids = [t[0] for t in cr.fetchall()]
104             res.update(ids)
105         return list(res)
106
107     def _get_project_and_children(self, cr, uid, ids, context=None):
108         """ retrieve all children projects of project ids;
109             return a dictionary mapping each project to its parent project (or None)
110         """
111         res = dict.fromkeys(ids, None)
112         while ids:
113             cr.execute("""
114                 SELECT project.id, parent.id
115                 FROM project_project project, project_project parent, account_analytic_account account
116                 WHERE project.analytic_account_id = account.id
117                 AND parent.analytic_account_id = account.parent_id
118                 AND parent.id IN %s
119                 """, (tuple(ids),))
120             dic = dict(cr.fetchall())
121             res.update(dic)
122             ids = dic.keys()
123         return res
124
125     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
126         child_parent = self._get_project_and_children(cr, uid, ids, context)
127         # compute planned_hours, total_hours, effective_hours specific to each project
128         cr.execute("""
129             SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
130                 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
131             FROM project_task WHERE project_id IN %s AND state <> 'cancelled'
132             GROUP BY project_id
133             """, (tuple(child_parent.keys()),))
134         # aggregate results into res
135         res = dict([(id, {'planned_hours':0.0,'total_hours':0.0,'effective_hours':0.0}) for id in ids])
136         for id, planned, total, effective in cr.fetchall():
137             # add the values specific to id to all parent projects of id in the result
138             while id:
139                 if id in ids:
140                     res[id]['planned_hours'] += planned
141                     res[id]['total_hours'] += total
142                     res[id]['effective_hours'] += effective
143                 id = child_parent[id]
144         # compute progress rates
145         for id in ids:
146             if res[id]['total_hours']:
147                 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
148             else:
149                 res[id]['progress_rate'] = 0.0
150         return res
151
152     def unlink(self, cr, uid, ids, *args, **kwargs):
153         for proj in self.browse(cr, uid, ids):
154             if proj.tasks:
155                 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
156         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
157
158     _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         self.set_close_send_note(cr, uid, ids, context=context)
229         return True
230
231     def set_cancel(self, cr, uid, ids, context=None):
232         task_obj = self.pool.get('project.task')
233         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
234         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
235         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
236         self.set_cancel_send_note(cr, uid, ids, 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         self.set_pending_send_note(cr, uid, ids, 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         self.set_open_send_note(cr, uid, ids, context=context)
247         return True
248
249     def reset_project(self, cr, uid, ids, context=None):
250         res = self.setActive(cr, uid, ids, value=True, context=context)
251         self.set_open_send_note(cr, uid, ids, context=context)
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
446     # ------------------------------------------------
447     # OpenChatter methods and notifications
448     # ------------------------------------------------
449     
450     def get_needaction_user_ids(self, cr, uid, ids, context=None):
451         result = dict.fromkeys(ids)
452         for obj in self.browse(cr, uid, ids, context=context):
453             result[obj.id] = []
454             if obj.state == 'draft' and obj.user_id:
455                 result[obj.id] = [obj.user_id.id]
456         return result
457
458     def message_get_subscribers(self, cr, uid, ids, context=None):
459         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
460         for obj in self.browse(cr, uid, ids, context=context):
461             if obj.user_id:
462                 sub_ids.append(obj.user_id.id)
463         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
464
465     def create(self, cr, uid, vals, context=None):
466         obj_id = super(project, self).create(cr, uid, vals, context=context)
467         self.create_send_note(cr, uid, [obj_id], context=context)
468         return obj_id
469
470     def create_send_note(self, cr, uid, ids, context=None):
471         return self.message_append_note(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
472
473     def set_open_send_note(self, cr, uid, ids, context=None):
474         message = _("Project has been <b>opened</b>.")
475         return self.message_append_note(cr, uid, ids, body=message, context=context)
476
477     def set_pending_send_note(self, cr, uid, ids, context=None):
478         message = _("Project is now <b>pending</b>.")
479         return self.message_append_note(cr, uid, ids, body=message, context=context)
480
481     def set_cancel_send_note(self, cr, uid, ids, context=None):
482         message = _("Project has been <b>cancelled</b>.")
483         return self.message_append_note(cr, uid, ids, body=message, context=context)
484
485     def set_close_send_note(self, cr, uid, ids, context=None):
486         message = _("Project has been <b>closed</b>.")
487         return self.message_append_note(cr, uid, ids, body=message, context=context)
488     
489 project()
490
491 class users(osv.osv):
492     _inherit = 'res.users'
493     _columns = {
494         'context_project_id': fields.many2one('project.project', 'Project')
495     }
496 users()
497
498 class task(osv.osv):
499     _name = "project.task"
500     _description = "Task"
501     _log_create = True
502     _date_name = "date_start"
503
504
505     def _resolve_project_id_from_context(self, cr, uid, context=None):
506         """Return ID of project based on the value of 'project_id'
507            context key, or None if it cannot be resolved to a single project.
508         """
509         if context is None: context = {}
510         if type(context.get('project_id')) in (int, long):
511             project_id = context['project_id']
512             return project_id
513         if isinstance(context.get('project_id'), basestring):
514             project_name = context['project_id']
515             project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name)
516             if len(project_ids) == 1:
517                 return project_ids[0][0]
518
519     def _read_group_type_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
520         stage_obj = self.pool.get('project.task.type')
521         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
522         order = stage_obj._order
523         access_rights_uid = access_rights_uid or uid
524         if read_group_order == 'type_id desc':
525             # lame way to allow reverting search, should just work in the trivial case
526             order = '%s desc' % order
527         if project_id:
528             domain = ['|', ('id','in',ids), ('project_ids','in',project_id)]
529         else:
530             domain = ['|', ('id','in',ids), ('project_default','=',1)]
531         stage_ids = stage_obj._search(cr, uid, domain, order=order, access_rights_uid=access_rights_uid, context=context)
532         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
533         # restore order of the search
534         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
535         return result
536
537     def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
538         res_users = self.pool.get('res.users')
539         project_id = self._resolve_project_id_from_context(cr, uid, context=context)
540         access_rights_uid = access_rights_uid or uid
541         if project_id:
542             ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
543             order = res_users._order
544             # lame way to allow reverting search, should just work in the trivial case
545             if read_group_order == 'user_id desc':
546                 order = '%s desc' % order
547             # de-duplicate and apply search order
548             ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
549         result = res_users.name_get(cr, access_rights_uid, ids, context=context)
550         # restore order of the search
551         result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
552         return result
553
554     _group_by_full = {
555         'type_id': _read_group_type_id,
556         'user_id': _read_group_user_id
557     }
558
559
560     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
561         obj_project = self.pool.get('project.project')
562         for domain in args:
563             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
564                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
565                 if id and isinstance(id, (long, int)):
566                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
567                         args.append(('active', '=', False))
568         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
569
570     def _str_get(self, task, level=0, border='***', context=None):
571         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'+ \
572             border[0]+' '+(task.name or '')+'\n'+ \
573             (task.description or '')+'\n\n'
574
575     # Compute: effective_hours, total_hours, progress
576     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
577         res = {}
578         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
579         hours = dict(cr.fetchall())
580         for task in self.browse(cr, uid, ids, context=context):
581             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)}
582             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
583             res[task.id]['progress'] = 0.0
584             if (task.remaining_hours + hours.get(task.id, 0.0)):
585                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
586             if task.state in ('done','cancelled'):
587                 res[task.id]['progress'] = 100.0
588         return res
589
590
591     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
592         if remaining and not planned:
593             return {'value':{'planned_hours': remaining}}
594         return {}
595
596     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
597         return {'value':{'remaining_hours': planned - effective}}
598
599     def onchange_project(self, cr, uid, id, project_id):
600         if not project_id:
601             return {}
602         data = self.pool.get('project.project').browse(cr, uid, [project_id])
603         partner_id=data and data[0].partner_id
604         if partner_id:
605             return {'value':{'partner_id':partner_id.id}}
606         return {}
607
608     def duplicate_task(self, cr, uid, map_ids, context=None):
609         for new in map_ids.values():
610             task = self.browse(cr, uid, new, context)
611             child_ids = [ ch.id for ch in task.child_ids]
612             if task.child_ids:
613                 for child in task.child_ids:
614                     if child.id in map_ids.keys():
615                         child_ids.remove(child.id)
616                         child_ids.append(map_ids[child.id])
617
618             parent_ids = [ ch.id for ch in task.parent_ids]
619             if task.parent_ids:
620                 for parent in task.parent_ids:
621                     if parent.id in map_ids.keys():
622                         parent_ids.remove(parent.id)
623                         parent_ids.append(map_ids[parent.id])
624             #FIXME why there is already the copy and the old one
625             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
626
627     def copy_data(self, cr, uid, id, default={}, context=None):
628         default = default or {}
629         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
630         if not default.get('remaining_hours', False):
631             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
632         default['active'] = True
633         default['type_id'] = False
634         if not default.get('name', False):
635             default['name'] = self.browse(cr, uid, id, context=context).name or ''
636             if not context.get('copy',False):
637                 new_name = _("%s (copy)")%default.get('name','')
638                 default.update({'name':new_name})
639         return super(task, self).copy_data(cr, uid, id, default, context)
640
641
642     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
643         res = {}
644         for task in self.browse(cr, uid, ids, context=context):
645             res[task.id] = True
646             if task.project_id:
647                 if task.project_id.active == False or task.project_id.state == 'template':
648                     res[task.id] = False
649         return res
650
651     def _get_task(self, cr, uid, ids, context=None):
652         result = {}
653         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
654             if work.task_id: result[work.task_id.id] = True
655         return result.keys()
656
657     _columns = {
658         '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."),
659         'name': fields.char('Task Summary', size=128, required=True, select=True),
660         'description': fields.text('Description'),
661         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
662         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
663         'type_id': fields.many2one('project.task.type', 'Stage'),
664         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
665                                   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.\
666                                   \n If the task is over, the states is set to \'Done\'.'),
667         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
668                                          help="A task's kanban state indicates special situations affecting it:\n"
669                                               " * Normal is the default situation\n"
670                                               " * Blocked indicates something is preventing the progress of this task\n"
671                                               " * Ready To Pull indicates the task is ready to be pulled to the next stage",
672                                          readonly=True, required=False),
673         'create_date': fields.datetime('Create Date', readonly=True,select=True),
674         'date_start': fields.datetime('Starting Date',select=True),
675         'date_end': fields.datetime('Ending Date',select=True),
676         'date_deadline': fields.date('Deadline',select=True),
677         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
678         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
679         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
680         'notes': fields.text('Notes'),
681         '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.'),
682         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
683             store = {
684                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
685                 'project.task.work': (_get_task, ['hours'], 10),
686             }),
687         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
688         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
689             store = {
690                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
691                 'project.task.work': (_get_task, ['hours'], 10),
692             }),
693         '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",
694             store = {
695                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
696                 'project.task.work': (_get_task, ['hours'], 10),
697             }),
698         '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.",
699             store = {
700                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
701                 'project.task.work': (_get_task, ['hours'], 10),
702             }),
703         'user_id': fields.many2one('res.users', 'Assigned to'),
704         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
705         'partner_id': fields.many2one('res.partner', 'Partner'),
706         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
707         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
708         'company_id': fields.many2one('res.company', 'Company'),
709         'id': fields.integer('ID', readonly=True),
710         'color': fields.integer('Color Index'),
711         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
712     }
713
714     _defaults = {
715         'state': 'draft',
716         'kanban_state': 'normal',
717         'priority': '2',
718         'progress': 0,
719         'sequence': 10,
720         'active': True,
721         'user_id': lambda obj, cr, uid, context: uid,
722         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
723     }
724
725     _order = "priority, sequence, date_start, name, id"
726
727     def set_priority(self, cr, uid, ids, priority):
728         """Set task priority
729         """
730         return self.write(cr, uid, ids, {'priority' : priority})
731
732     def set_high_priority(self, cr, uid, ids, *args):
733         """Set task priority to high
734         """
735         return self.set_priority(cr, uid, ids, '1')
736
737     def set_normal_priority(self, cr, uid, ids, *args):
738         """Set task priority to normal
739         """
740         return self.set_priority(cr, uid, ids, '2')
741
742     def _check_recursion(self, cr, uid, ids, context=None):
743         for id in ids:
744             visited_branch = set()
745             visited_node = set()
746             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
747             if not res:
748                 return False
749
750         return True
751
752     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
753         if id in visited_branch: #Cycle
754             return False
755
756         if id in visited_node: #Already tested don't work one more time for nothing
757             return True
758
759         visited_branch.add(id)
760         visited_node.add(id)
761
762         #visit child using DFS
763         task = self.browse(cr, uid, id, context=context)
764         for child in task.child_ids:
765             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
766             if not res:
767                 return False
768
769         visited_branch.remove(id)
770         return True
771
772     def _check_dates(self, cr, uid, ids, context=None):
773         if context == None:
774             context = {}
775         obj_task = self.browse(cr, uid, ids[0], context=context)
776         start = obj_task.date_start or False
777         end = obj_task.date_end or False
778         if start and end :
779             if start > end:
780                 return False
781         return True
782
783     _constraints = [
784         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
785         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
786     ]
787     #
788     # Override view according to the company definition
789     #
790     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
791         users_obj = self.pool.get('res.users')
792
793         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
794         # this should be safe (no context passed to avoid side-effects)
795         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
796         tm = obj_tm and obj_tm.name or 'Hours'
797
798         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
799
800         if tm in ['Hours','Hour']:
801             return res
802
803         eview = etree.fromstring(res['arch'])
804
805         def _check_rec(eview):
806             if eview.attrib.get('widget','') == 'float_time':
807                 eview.set('widget','float')
808             for child in eview:
809                 _check_rec(child)
810             return True
811
812         _check_rec(eview)
813
814         res['arch'] = etree.tostring(eview)
815
816         for f in res['fields']:
817             if 'Hours' in res['fields'][f]['string']:
818                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
819         return res
820
821     def _check_child_task(self, cr, uid, ids, context=None):
822         if context == None:
823             context = {}
824         tasks = self.browse(cr, uid, ids, context=context)
825         for task in tasks:
826             if task.child_ids:
827                 for child in task.child_ids:
828                     if child.state in ['draft', 'open', 'pending']:
829                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
830         return True
831
832     def action_close(self, cr, uid, ids, context=None):
833         # This action open wizard to send email to partner or project manager after close task.
834         if context == None:
835             context = {}
836         task_id = len(ids) and ids[0] or False
837         self._check_child_task(cr, uid, ids, context=context)
838         if not task_id: return False
839         task = self.browse(cr, uid, task_id, context=context)
840         project = task.project_id
841         res = self.do_close(cr, uid, [task_id], context=context)
842         if project.warn_manager or project.warn_customer:
843             return {
844                 'name': _('Send Email after close task'),
845                 'view_type': 'form',
846                 'view_mode': 'form',
847                 'res_model': 'mail.compose.message',
848                 'type': 'ir.actions.act_window',
849                 'target': 'new',
850                 'nodestroy': True,
851                 'context': {'active_id': task.id,
852                             'active_model': 'project.task'}
853            }
854         return res
855
856     def do_close(self, cr, uid, ids, context={}):
857         """
858         Close Task
859         """
860         request = self.pool.get('res.request')
861         if not isinstance(ids,list): ids = [ids]
862         for task in self.browse(cr, uid, ids, context=context):
863             vals = {}
864             project = task.project_id
865             if project:
866                 # Send request to project manager
867                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
868                     request.create(cr, uid, {
869                         'name': _("Task '%s' closed") % task.name,
870                         'state': 'waiting',
871                         'act_from': uid,
872                         'act_to': project.user_id.id,
873                         'ref_partner_id': task.partner_id.id,
874                         'ref_doc1': 'project.task,%d'% (task.id,),
875                         'ref_doc2': 'project.project,%d'% (project.id,),
876                     }, context=context)
877
878             for parent_id in task.parent_ids:
879                 if parent_id.state in ('pending','draft'):
880                     reopen = True
881                     for child in parent_id.child_ids:
882                         if child.id != task.id and child.state not in ('done','cancelled'):
883                             reopen = False
884                     if reopen:
885                         self.do_reopen(cr, uid, [parent_id.id], context=context)
886             vals.update({'state': 'done'})
887             vals.update({'remaining_hours': 0.0})
888             if not task.date_end:
889                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
890             self.write(cr, uid, [task.id],vals, context=context)
891             message = _("The task '%s' is done") % (task.name,)
892             self.log(cr, uid, task.id, message)
893         return True
894
895     def do_reopen(self, cr, uid, ids, context=None):
896         request = self.pool.get('res.request')
897
898         for task in self.browse(cr, uid, ids, context=context):
899             project = task.project_id
900             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
901                 request.create(cr, uid, {
902                     'name': _("Task '%s' set in progress") % task.name,
903                     'state': 'waiting',
904                     'act_from': uid,
905                     'act_to': project.user_id.id,
906                     'ref_partner_id': task.partner_id.id,
907                     'ref_doc1': 'project.task,%d' % task.id,
908                     'ref_doc2': 'project.project,%d' % project.id,
909                 }, context=context)
910
911             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
912         return True
913
914     def do_cancel(self, cr, uid, ids, context={}):
915         request = self.pool.get('res.request')
916         tasks = self.browse(cr, uid, ids, context=context)
917         self._check_child_task(cr, uid, ids, context=context)
918         for task in tasks:
919             project = task.project_id
920             if project.warn_manager and project.user_id and (project.user_id.id != uid):
921                 request.create(cr, uid, {
922                     'name': _("Task '%s' cancelled") % task.name,
923                     'state': 'waiting',
924                     'act_from': uid,
925                     'act_to': project.user_id.id,
926                     'ref_partner_id': task.partner_id.id,
927                     'ref_doc1': 'project.task,%d' % task.id,
928                     'ref_doc2': 'project.project,%d' % project.id,
929                 }, context=context)
930             message = _("The task '%s' is cancelled.") % (task.name,)
931             self.log(cr, uid, task.id, message)
932             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
933         return True
934
935     def do_open(self, cr, uid, ids, context={}):
936         if not isinstance(ids,list): ids = [ids]
937         tasks= self.browse(cr, uid, ids, context=context)
938         for t in tasks:
939             data = {'state': 'open'}
940             if not t.date_start:
941                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
942             self.write(cr, uid, [t.id], data, context=context)
943             message = _("The task '%s' is opened.") % (t.name,)
944             self.log(cr, uid, t.id, message)
945         return True
946
947     def do_draft(self, cr, uid, ids, context={}):
948         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
949         return True
950
951
952     def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
953         attachment = self.pool.get('ir.attachment')
954         attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
955         new_attachment_ids = []
956         for attachment_id in attachment_ids:
957             new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
958         return new_attachment_ids
959         
960
961     def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
962         """
963         Delegate Task to another users.
964         """
965         assert delegate_data['user_id'], _("Delegated User should be specified")
966         delegated_tasks = {}
967         for task in self.browse(cr, uid, ids, context=context):
968             delegated_task_id = self.copy(cr, uid, task.id, {
969                 'name': delegate_data['name'],
970                 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
971                 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
972                 'planned_hours': delegate_data['planned_hours'] or 0.0,
973                 'parent_ids': [(6, 0, [task.id])],
974                 'state': 'draft',
975                 'description': delegate_data['new_task_description'] or '',
976                 'child_ids': [],
977                 'work_ids': []
978             }, context=context)
979             self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
980             newname = delegate_data['prefix'] or ''
981             task.write({
982                 'remaining_hours': delegate_data['planned_hours_me'],
983                 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
984                 'name': newname,
985             }, context=context)
986             if delegate_data['state'] == 'pending':
987                 self.do_pending(cr, uid, task.id, context=context)
988             elif delegate_data['state'] == 'done':
989                 self.do_close(cr, uid, task.id, context=context)
990             
991             message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
992             self.log(cr, uid, task.id, message)
993             delegated_tasks[task.id] = delegated_task_id
994         return delegated_tasks
995
996     def do_pending(self, cr, uid, ids, context={}):
997         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
998         for (id, name) in self.name_get(cr, uid, ids):
999             message = _("The task '%s' is pending.") % name
1000             self.log(cr, uid, id, message)
1001         return True
1002
1003     def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
1004         for task in self.browse(cr, uid, ids, context=context):
1005             if (task.state=='draft') or (task.planned_hours==0.0):
1006                 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
1007         self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
1008         return True
1009
1010     def set_remaining_time_1(self, cr, uid, ids, context=None):
1011         return self.set_remaining_time(cr, uid, ids, 1.0, context)
1012
1013     def set_remaining_time_2(self, cr, uid, ids, context=None):
1014         return self.set_remaining_time(cr, uid, ids, 2.0, context)
1015
1016     def set_remaining_time_5(self, cr, uid, ids, context=None):
1017         return self.set_remaining_time(cr, uid, ids, 5.0, context)
1018
1019     def set_remaining_time_10(self, cr, uid, ids, context=None):
1020         return self.set_remaining_time(cr, uid, ids, 10.0, context)
1021
1022     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
1023         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
1024
1025     def set_kanban_state_normal(self, cr, uid, ids, context=None):
1026         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1027
1028     def set_kanban_state_done(self, cr, uid, ids, context=None):
1029         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1030
1031     def _change_type(self, cr, uid, ids, next, *args):
1032         """
1033             go to the next stage
1034             if next is False, go to previous stage
1035         """
1036         for task in self.browse(cr, uid, ids):
1037             if  task.project_id.type_ids:
1038                 typeid = task.type_id.id
1039                 types_seq={}
1040                 for type in task.project_id.type_ids :
1041                     types_seq[type.id] = type.sequence
1042                 if next:
1043                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
1044                 else:
1045                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
1046                 sorted_types = [x[0] for x in types]
1047                 if not typeid:
1048                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
1049                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
1050                     index = sorted_types.index(typeid)
1051                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
1052         return True
1053
1054     def next_type(self, cr, uid, ids, *args):
1055         return self._change_type(cr, uid, ids, True, *args)
1056
1057     def prev_type(self, cr, uid, ids, *args):
1058         return self._change_type(cr, uid, ids, False, *args)
1059
1060     def _store_history(self, cr, uid, ids, context=None):
1061         for task in self.browse(cr, uid, ids, context=context):
1062             self.pool.get('project.task.history').create(cr, uid, {
1063                 'task_id': task.id,
1064                 'remaining_hours': task.remaining_hours,
1065                 'planned_hours': task.planned_hours,
1066                 'kanban_state': task.kanban_state,
1067                 'type_id': task.type_id.id,
1068                 'state': task.state,
1069                 'user_id': task.user_id.id
1070
1071             }, context=context)
1072         return True
1073
1074     def create(self, cr, uid, vals, context=None):
1075         result = super(task, self).create(cr, uid, vals, context=context)
1076         self._store_history(cr, uid, [result], context=context)
1077         return result
1078
1079     # Overridden to reset the kanban_state to normal whenever
1080     # the stage (type_id) of the task changes.
1081     def write(self, cr, uid, ids, vals, context=None):
1082         if isinstance(ids, (int, long)):
1083             ids = [ids]
1084         if vals and not 'kanban_state' in vals and 'type_id' in vals:
1085             new_stage = vals.get('type_id')
1086             vals_reset_kstate = dict(vals, kanban_state='normal')
1087             for t in self.browse(cr, uid, ids, context=context):
1088                 write_vals = vals_reset_kstate if t.type_id != new_stage else vals 
1089                 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1090             result = True
1091         else:
1092             result = super(task,self).write(cr, uid, ids, vals, context=context)
1093         if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1094             self._store_history(cr, uid, ids, context=context)
1095         return result
1096
1097     def unlink(self, cr, uid, ids, context=None):
1098         if context == None:
1099             context = {}
1100         self._check_child_task(cr, uid, ids, context=context)
1101         res = super(task, self).unlink(cr, uid, ids, context)
1102         return res
1103
1104     def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1105         context = context or {}
1106         result = ""
1107         ident = ' '*ident
1108         for task in tasks:
1109             if task.state in ('done','cancelled'):
1110                 continue
1111             result += '''
1112 %sdef Task_%s():
1113 %s  todo = \"%.2fH\"
1114 %s  effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1115             start = []
1116             for t2 in task.parent_ids:
1117                 start.append("up.Task_%s.end" % (t2.id,))
1118             if start:
1119                 result += '''
1120 %s  start = max(%s)
1121 ''' % (ident,','.join(start))
1122
1123             if task.user_id:
1124                 result += '''
1125 %s  resource = %s
1126 ''' % (ident, 'User_'+str(task.user_id.id))
1127
1128         result += "\n"
1129         return result
1130
1131 task()
1132
1133 class project_work(osv.osv):
1134     _name = "project.task.work"
1135     _description = "Project Task Work"
1136     _columns = {
1137         'name': fields.char('Work summary', size=128),
1138         'date': fields.datetime('Date', select="1"),
1139         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1140         'hours': fields.float('Time Spent'),
1141         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1142         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1143     }
1144
1145     _defaults = {
1146         'user_id': lambda obj, cr, uid, context: uid,
1147         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1148     }
1149
1150     _order = "date desc"
1151     def create(self, cr, uid, vals, *args, **kwargs):
1152         if 'hours' in vals and (not vals['hours']):
1153             vals['hours'] = 0.00
1154         if 'task_id' in vals:
1155             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1156         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1157
1158     def write(self, cr, uid, ids, vals, context=None):
1159         if 'hours' in vals and (not vals['hours']):
1160             vals['hours'] = 0.00
1161         if 'hours' in vals:
1162             for work in self.browse(cr, uid, ids, context=context):
1163                 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))
1164         return super(project_work,self).write(cr, uid, ids, vals, context)
1165
1166     def unlink(self, cr, uid, ids, *args, **kwargs):
1167         for work in self.browse(cr, uid, ids):
1168             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1169         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1170 project_work()
1171
1172 class account_analytic_account(osv.osv):
1173
1174     _inherit = 'account.analytic.account'
1175     _description = 'Analytic Account'
1176
1177     def create(self, cr, uid, vals, context=None):
1178         if context is None:
1179             context = {}
1180         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1181             vals['child_ids'] = []
1182         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1183
1184     def unlink(self, cr, uid, ids, *args, **kwargs):
1185         project_obj = self.pool.get('project.project')
1186         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1187         if analytic_ids:
1188             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1189         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1190
1191 account_analytic_account()
1192
1193 #
1194 # Tasks History, used for cumulative flow charts (Lean/Agile)
1195 #
1196
1197 class project_task_history(osv.osv):
1198     _name = 'project.task.history'
1199     _description = 'History of Tasks'
1200     _rec_name = 'task_id'
1201     _log_access = False
1202     def _get_date(self, cr, uid, ids, name, arg, context=None):
1203         result = {}
1204         for history in self.browse(cr, uid, ids, context=context):
1205             if history.state in ('done','cancelled'):
1206                 result[history.id] = history.date
1207                 continue
1208             cr.execute('''select
1209                     date
1210                 from
1211                     project_task_history
1212                 where
1213                     task_id=%s and
1214                     id>%s
1215                 order by id limit 1''', (history.task_id.id, history.id))
1216             res = cr.fetchone()
1217             result[history.id] = res and res[0] or False
1218         return result
1219
1220     def _get_related_date(self, cr, uid, ids, context=None):
1221         result = []
1222         for history in self.browse(cr, uid, ids, context=context):
1223             cr.execute('''select
1224                     id
1225                 from 
1226                     project_task_history
1227                 where
1228                     task_id=%s and
1229                     id<%s
1230                 order by id desc limit 1''', (history.task_id.id, history.id))
1231             res = cr.fetchone()
1232             if res:
1233                 result.append(res[0])
1234         return result
1235
1236     _columns = {
1237         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1238         'type_id': fields.many2one('project.task.type', 'Stage'),
1239         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1240         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1241         'date': fields.date('Date', select=True),
1242         'end_date': fields.function(_get_date, string='End Date', type="date", store={
1243             'project.task.history': (_get_related_date, None, 20)
1244         }),
1245         'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1246         'planned_hours': fields.float('Planned Time', digits=(16,2)),
1247         'user_id': fields.many2one('res.users', 'Responsible'),
1248     }
1249     _defaults = {
1250         'date': fields.date.context_today,
1251     }
1252 project_task_history()
1253
1254 class project_task_history_cumulative(osv.osv):
1255     _name = 'project.task.history.cumulative'
1256     _table = 'project_task_history_cumulative'
1257     _inherit = 'project.task.history'
1258     _auto = False
1259     _columns = {
1260         'end_date': fields.date('End Date'),
1261         'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1262     }
1263     def init(self, cr):
1264         cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1265             SELECT
1266                 history.date::varchar||'-'||history.history_id::varchar as id,
1267                 history.date as end_date,
1268                 *
1269             FROM (
1270                 SELECT
1271                     id as history_id,
1272                     date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1273                     task_id, type_id, user_id, kanban_state, state,
1274                     remaining_hours, planned_hours
1275                 FROM
1276                     project_task_history
1277             ) as history
1278         )
1279         """)
1280 project_task_history_cumulative()