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