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