[IMP] import: use `openerp.addons.<module>` instead of `<module>` with __import__.
[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 _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 openerp.addons.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].partner_id
521         if partner_id:
522             return {'value':{'partner_id':partner_id.id}}
523         return {}
524
525     def duplicate_task(self, cr, uid, map_ids, context=None):
526         for new in map_ids.values():
527             task = self.browse(cr, uid, new, context)
528             child_ids = [ ch.id for ch in task.child_ids]
529             if task.child_ids:
530                 for child in task.child_ids:
531                     if child.id in map_ids.keys():
532                         child_ids.remove(child.id)
533                         child_ids.append(map_ids[child.id])
534
535             parent_ids = [ ch.id for ch in task.parent_ids]
536             if task.parent_ids:
537                 for parent in task.parent_ids:
538                     if parent.id in map_ids.keys():
539                         parent_ids.remove(parent.id)
540                         parent_ids.append(map_ids[parent.id])
541             #FIXME why there is already the copy and the old one
542             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
543
544     def copy_data(self, cr, uid, id, default={}, context=None):
545         default = default or {}
546         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
547         if not default.get('remaining_hours', False):
548             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
549         default['active'] = True
550         default['type_id'] = False
551         if not default.get('name', False):
552             default['name'] = self.browse(cr, uid, id, context=context).name or ''
553             if not context.get('copy',False):
554                 new_name = _("%s (copy)")%default.get('name','')
555                 default.update({'name':new_name})
556         return super(task, self).copy_data(cr, uid, id, default, context)
557
558
559     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
560         res = {}
561         for task in self.browse(cr, uid, ids, context=context):
562             res[task.id] = True
563             if task.project_id:
564                 if task.project_id.active == False or task.project_id.state == 'template':
565                     res[task.id] = False
566         return res
567
568     def _get_task(self, cr, uid, ids, context=None):
569         result = {}
570         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
571             if work.task_id: result[work.task_id.id] = True
572         return result.keys()
573
574     _columns = {
575         '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."),
576         'name': fields.char('Task Summary', size=128, required=True, select=True),
577         'description': fields.text('Description'),
578         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
579         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
580         'type_id': fields.many2one('project.task.type', 'Stage'),
581         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
582                                   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.\
583                                   \n If the task is over, the states is set to \'Done\'.'),
584         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State',
585                                          help="A task's kanban state indicates special situations affecting it:\n"
586                                               " * Normal is the default situation\n"
587                                               " * Blocked indicates something is preventing the progress of this task\n"
588                                               " * Ready To Pull indicates the task is ready to be pulled to the next stage",
589                                          readonly=True, required=False),
590         'create_date': fields.datetime('Create Date', readonly=True,select=True),
591         'date_start': fields.datetime('Starting Date',select=True),
592         'date_end': fields.datetime('Ending Date',select=True),
593         'date_deadline': fields.date('Deadline',select=True),
594         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
595         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
596         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
597         'notes': fields.text('Notes'),
598         '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.'),
599         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
600             store = {
601                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
602                 'project.task.work': (_get_task, ['hours'], 10),
603             }),
604         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
605         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
606             store = {
607                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
608                 'project.task.work': (_get_task, ['hours'], 10),
609             }),
610         '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",
611             store = {
612                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
613                 'project.task.work': (_get_task, ['hours'], 10),
614             }),
615         '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.",
616             store = {
617                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
618                 'project.task.work': (_get_task, ['hours'], 10),
619             }),
620         'user_id': fields.many2one('res.users', 'Assigned to'),
621         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
622         'partner_id': fields.many2one('res.partner', 'Partner'),
623         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
624         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
625         'company_id': fields.many2one('res.company', 'Company'),
626         'id': fields.integer('ID', readonly=True),
627         'color': fields.integer('Color Index'),
628         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
629     }
630
631     _defaults = {
632         'state': 'draft',
633         'kanban_state': 'normal',
634         'priority': '2',
635         'progress': 0,
636         'sequence': 10,
637         'active': True,
638         'user_id': lambda obj, cr, uid, context: uid,
639         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
640     }
641
642     _order = "priority, sequence, date_start, name, id"
643
644     def set_priority(self, cr, uid, ids, priority):
645         """Set task priority
646         """
647         return self.write(cr, uid, ids, {'priority' : priority})
648
649     def set_high_priority(self, cr, uid, ids, *args):
650         """Set task priority to high
651         """
652         return self.set_priority(cr, uid, ids, '1')
653
654     def set_normal_priority(self, cr, uid, ids, *args):
655         """Set task priority to normal
656         """
657         return self.set_priority(cr, uid, ids, '3')
658
659     def _check_recursion(self, cr, uid, ids, context=None):
660         for id in ids:
661             visited_branch = set()
662             visited_node = set()
663             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
664             if not res:
665                 return False
666
667         return True
668
669     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
670         if id in visited_branch: #Cycle
671             return False
672
673         if id in visited_node: #Already tested don't work one more time for nothing
674             return True
675
676         visited_branch.add(id)
677         visited_node.add(id)
678
679         #visit child using DFS
680         task = self.browse(cr, uid, id, context=context)
681         for child in task.child_ids:
682             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
683             if not res:
684                 return False
685
686         visited_branch.remove(id)
687         return True
688
689     def _check_dates(self, cr, uid, ids, context=None):
690         if context == None:
691             context = {}
692         obj_task = self.browse(cr, uid, ids[0], context=context)
693         start = obj_task.date_start or False
694         end = obj_task.date_end or False
695         if start and end :
696             if start > end:
697                 return False
698         return True
699
700     _constraints = [
701         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
702         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
703     ]
704     #
705     # Override view according to the company definition
706     #
707     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
708         users_obj = self.pool.get('res.users')
709
710         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
711         # this should be safe (no context passed to avoid side-effects)
712         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
713         tm = obj_tm and obj_tm.name or 'Hours'
714
715         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
716
717         if tm in ['Hours','Hour']:
718             return res
719
720         eview = etree.fromstring(res['arch'])
721
722         def _check_rec(eview):
723             if eview.attrib.get('widget','') == 'float_time':
724                 eview.set('widget','float')
725             for child in eview:
726                 _check_rec(child)
727             return True
728
729         _check_rec(eview)
730
731         res['arch'] = etree.tostring(eview)
732
733         for f in res['fields']:
734             if 'Hours' in res['fields'][f]['string']:
735                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
736         return res
737
738     def _check_child_task(self, cr, uid, ids, context=None):
739         if context == None:
740             context = {}
741         tasks = self.browse(cr, uid, ids, context=context)
742         for task in tasks:
743             if task.child_ids:
744                 for child in task.child_ids:
745                     if child.state in ['draft', 'open', 'pending']:
746                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
747         return True
748
749     def action_close(self, cr, uid, ids, context=None):
750         # This action open wizard to send email to partner or project manager after close task.
751         if context == None:
752             context = {}
753         task_id = len(ids) and ids[0] or False
754         self._check_child_task(cr, uid, ids, context=context)
755         if not task_id: return False
756         task = self.browse(cr, uid, task_id, context=context)
757         project = task.project_id
758         res = self.do_close(cr, uid, [task_id], context=context)
759         if project.warn_manager or project.warn_customer:
760             return {
761                 'name': _('Send Email after close task'),
762                 'view_type': 'form',
763                 'view_mode': 'form',
764                 'res_model': 'mail.compose.message',
765                 'type': 'ir.actions.act_window',
766                 'target': 'new',
767                 'nodestroy': True,
768                 'context': {'active_id': task.id,
769                             'active_model': 'project.task'}
770            }
771         return res
772
773     def do_close(self, cr, uid, ids, context={}):
774         """
775         Close Task
776         """
777         request = self.pool.get('res.request')
778         if not isinstance(ids,list): ids = [ids]
779         for task in self.browse(cr, uid, ids, context=context):
780             vals = {}
781             project = task.project_id
782             if project:
783                 # Send request to project manager
784                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
785                     request.create(cr, uid, {
786                         'name': _("Task '%s' closed") % task.name,
787                         'state': 'waiting',
788                         'act_from': uid,
789                         'act_to': project.user_id.id,
790                         'ref_partner_id': task.partner_id.id,
791                         'ref_doc1': 'project.task,%d'% (task.id,),
792                         'ref_doc2': 'project.project,%d'% (project.id,),
793                     }, context=context)
794
795             for parent_id in task.parent_ids:
796                 if parent_id.state in ('pending','draft'):
797                     reopen = True
798                     for child in parent_id.child_ids:
799                         if child.id != task.id and child.state not in ('done','cancelled'):
800                             reopen = False
801                     if reopen:
802                         self.do_reopen(cr, uid, [parent_id.id], context=context)
803             vals.update({'state': 'done'})
804             vals.update({'remaining_hours': 0.0})
805             if not task.date_end:
806                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
807             self.write(cr, uid, [task.id],vals, context=context)
808             message = _("The task '%s' is done") % (task.name,)
809             self.log(cr, uid, task.id, message)
810         return True
811
812     def do_reopen(self, cr, uid, ids, context=None):
813         request = self.pool.get('res.request')
814
815         for task in self.browse(cr, uid, ids, context=context):
816             project = task.project_id
817             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
818                 request.create(cr, uid, {
819                     'name': _("Task '%s' set in progress") % task.name,
820                     'state': 'waiting',
821                     'act_from': uid,
822                     'act_to': project.user_id.id,
823                     'ref_partner_id': task.partner_id.id,
824                     'ref_doc1': 'project.task,%d' % task.id,
825                     'ref_doc2': 'project.project,%d' % project.id,
826                 }, context=context)
827
828             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
829         return True
830
831     def do_cancel(self, cr, uid, ids, context={}):
832         request = self.pool.get('res.request')
833         tasks = self.browse(cr, uid, ids, context=context)
834         self._check_child_task(cr, uid, ids, context=context)
835         for task in tasks:
836             project = task.project_id
837             if project.warn_manager and project.user_id and (project.user_id.id != uid):
838                 request.create(cr, uid, {
839                     'name': _("Task '%s' cancelled") % task.name,
840                     'state': 'waiting',
841                     'act_from': uid,
842                     'act_to': project.user_id.id,
843                     'ref_partner_id': task.partner_id.id,
844                     'ref_doc1': 'project.task,%d' % task.id,
845                     'ref_doc2': 'project.project,%d' % project.id,
846                 }, context=context)
847             message = _("The task '%s' is cancelled.") % (task.name,)
848             self.log(cr, uid, task.id, message)
849             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
850         return True
851
852     def do_open(self, cr, uid, ids, context={}):
853         if not isinstance(ids,list): ids = [ids]
854         tasks= self.browse(cr, uid, ids, context=context)
855         for t in tasks:
856             data = {'state': 'open'}
857             if not t.date_start:
858                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
859             self.write(cr, uid, [t.id], data, context=context)
860             message = _("The task '%s' is opened.") % (t.name,)
861             self.log(cr, uid, t.id, message)
862         return True
863
864     def do_draft(self, cr, uid, ids, context={}):
865         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
866         return True
867
868
869     def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
870         attachment = self.pool.get('ir.attachment')
871         attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
872         new_attachment_ids = []
873         for attachment_id in attachment_ids:
874             new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
875         return new_attachment_ids
876         
877
878     def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
879         """
880         Delegate Task to another users.
881         """
882         assert delegate_data['user_id'], _("Delegated User should be specified")
883         delegated_tasks = {}
884         for task in self.browse(cr, uid, ids, context=context):
885             delegated_task_id = self.copy(cr, uid, task.id, {
886                 'name': delegate_data['name'],
887                 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
888                 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
889                 'planned_hours': delegate_data['planned_hours'] or 0.0,
890                 'parent_ids': [(6, 0, [task.id])],
891                 'state': 'draft',
892                 'description': delegate_data['new_task_description'] or '',
893                 'child_ids': [],
894                 'work_ids': []
895             }, context=context)
896             self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
897             newname = delegate_data['prefix'] or ''
898             task.write({
899                 'remaining_hours': delegate_data['planned_hours_me'],
900                 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
901                 'name': newname,
902             }, context=context)
903             if delegate_data['state'] == 'pending':
904                 self.do_pending(cr, uid, task.id, context=context)
905             elif delegate_data['state'] == 'done':
906                 self.do_close(cr, uid, task.id, context=context)
907             
908             message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
909             self.log(cr, uid, task.id, message)
910             delegated_tasks[task.id] = delegated_task_id
911         return delegated_tasks
912
913     def do_pending(self, cr, uid, ids, context={}):
914         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
915         for (id, name) in self.name_get(cr, uid, ids):
916             message = _("The task '%s' is pending.") % name
917             self.log(cr, uid, id, message)
918         return True
919
920     def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
921         for task in self.browse(cr, uid, ids, context=context):
922             if (task.state=='draft') or (task.planned_hours==0.0):
923                 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
924         self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
925         return True
926
927     def set_remaining_time_1(self, cr, uid, ids, context=None):
928         return self.set_remaining_time(cr, uid, ids, 1.0, context)
929
930     def set_remaining_time_2(self, cr, uid, ids, context=None):
931         return self.set_remaining_time(cr, uid, ids, 2.0, context)
932
933     def set_remaining_time_5(self, cr, uid, ids, context=None):
934         return self.set_remaining_time(cr, uid, ids, 5.0, context)
935
936     def set_remaining_time_10(self, cr, uid, ids, context=None):
937         return self.set_remaining_time(cr, uid, ids, 10.0, context)
938
939     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
940         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
941
942     def set_kanban_state_normal(self, cr, uid, ids, context=None):
943         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
944
945     def set_kanban_state_done(self, cr, uid, ids, context=None):
946         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
947
948     def _change_type(self, cr, uid, ids, next, *args):
949         """
950             go to the next stage
951             if next is False, go to previous stage
952         """
953         for task in self.browse(cr, uid, ids):
954             if  task.project_id.type_ids:
955                 typeid = task.type_id.id
956                 types_seq={}
957                 for type in task.project_id.type_ids :
958                     types_seq[type.id] = type.sequence
959                 if next:
960                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
961                 else:
962                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
963                 sorted_types = [x[0] for x in types]
964                 if not typeid:
965                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
966                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
967                     index = sorted_types.index(typeid)
968                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
969         return True
970
971     def next_type(self, cr, uid, ids, *args):
972         return self._change_type(cr, uid, ids, True, *args)
973
974     def prev_type(self, cr, uid, ids, *args):
975         return self._change_type(cr, uid, ids, False, *args)
976
977     def _store_history(self, cr, uid, ids, context=None):
978         for task in self.browse(cr, uid, ids, context=context):
979             self.pool.get('project.task.history').create(cr, uid, {
980                 'task_id': task.id,
981                 'remaining_hours': task.remaining_hours,
982                 'planned_hours': task.planned_hours,
983                 'kanban_state': task.kanban_state,
984                 'type_id': task.type_id.id,
985                 'state': task.state,
986                 'user_id': task.user_id.id
987
988             }, context=context)
989         return True
990
991     def create(self, cr, uid, vals, context=None):
992         result = super(task, self).create(cr, uid, vals, context=context)
993         self._store_history(cr, uid, [result], context=context)
994         return result
995
996     # Overridden to reset the kanban_state to normal whenever
997     # the stage (type_id) of the task changes.
998     def write(self, cr, uid, ids, vals, context=None):
999         if isinstance(ids, (int, long)):
1000             ids = [ids]
1001         if vals and not 'kanban_state' in vals and 'type_id' in vals:
1002             new_stage = vals.get('type_id')
1003             vals_reset_kstate = dict(vals, kanban_state='normal')
1004             for t in self.browse(cr, uid, ids, context=context):
1005                 write_vals = vals_reset_kstate if t.type_id != new_stage else vals 
1006                 super(task,self).write(cr, uid, [t.id], write_vals, context=context)
1007             result = True
1008         else:
1009             result = super(task,self).write(cr, uid, ids, vals, context=context)
1010         if ('type_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals):
1011             self._store_history(cr, uid, ids, context=context)
1012         return result
1013
1014     def unlink(self, cr, uid, ids, context=None):
1015         if context == None:
1016             context = {}
1017         self._check_child_task(cr, uid, ids, context=context)
1018         res = super(task, self).unlink(cr, uid, ids, context)
1019         return res
1020
1021     def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1022         context = context or {}
1023         result = ""
1024         ident = ' '*ident
1025         for task in tasks:
1026             if task.state in ('done','cancelled'):
1027                 continue
1028             result += '''
1029 %sdef Task_%s():
1030 %s  todo = \"%.2fH\"
1031 %s  effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1032             start = []
1033             for t2 in task.parent_ids:
1034                 start.append("up.Task_%s.end" % (t2.id,))
1035             if start:
1036                 result += '''
1037 %s  start = max(%s)
1038 ''' % (ident,','.join(start))
1039
1040             if task.user_id:
1041                 result += '''
1042 %s  resource = %s
1043 ''' % (ident, 'User_'+str(task.user_id.id))
1044
1045         result += "\n"
1046         return result
1047
1048 task()
1049
1050 class project_work(osv.osv):
1051     _name = "project.task.work"
1052     _description = "Project Task Work"
1053     _columns = {
1054         'name': fields.char('Work summary', size=128),
1055         'date': fields.datetime('Date', select="1"),
1056         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1057         'hours': fields.float('Time Spent'),
1058         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1059         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1060     }
1061
1062     _defaults = {
1063         'user_id': lambda obj, cr, uid, context: uid,
1064         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1065     }
1066
1067     _order = "date desc"
1068     def create(self, cr, uid, vals, *args, **kwargs):
1069         if 'hours' in vals and (not vals['hours']):
1070             vals['hours'] = 0.00
1071         if 'task_id' in vals:
1072             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1073         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1074
1075     def write(self, cr, uid, ids, vals, context=None):
1076         if 'hours' in vals and (not vals['hours']):
1077             vals['hours'] = 0.00
1078         if 'hours' in vals:
1079             for work in self.browse(cr, uid, ids, context=context):
1080                 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))
1081         return super(project_work,self).write(cr, uid, ids, vals, context)
1082
1083     def unlink(self, cr, uid, ids, *args, **kwargs):
1084         for work in self.browse(cr, uid, ids):
1085             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1086         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1087 project_work()
1088
1089 class account_analytic_account(osv.osv):
1090
1091     _inherit = 'account.analytic.account'
1092     _description = 'Analytic Account'
1093
1094     def create(self, cr, uid, vals, context=None):
1095         if context is None:
1096             context = {}
1097         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1098             vals['child_ids'] = []
1099         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1100
1101     def unlink(self, cr, uid, ids, *args, **kwargs):
1102         project_obj = self.pool.get('project.project')
1103         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1104         if analytic_ids:
1105             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1106         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1107
1108 account_analytic_account()
1109
1110 #
1111 # Tasks History, used for cumulative flow charts (Lean/Agile)
1112 #
1113
1114 class project_task_history(osv.osv):
1115     _name = 'project.task.history'
1116     _description = 'History of Tasks'
1117     _rec_name = 'task_id'
1118     _log_access = False
1119     def _get_date(self, cr, uid, ids, name, arg, context=None):
1120         result = {}
1121         for history in self.browse(cr, uid, ids, context=context):
1122             if history.state in ('done','cancelled'):
1123                 result[history.id] = history.date
1124                 continue
1125             cr.execute('''select
1126                     date
1127                 from
1128                     project_task_history
1129                 where
1130                     task_id=%s and
1131                     id>%s
1132                 order by id limit 1''', (history.task_id.id, history.id))
1133             res = cr.fetchone()
1134             result[history.id] = res and res[0] or False
1135         return result
1136
1137     def _get_related_date(self, cr, uid, ids, context=None):
1138         result = []
1139         for history in self.browse(cr, uid, ids, context=context):
1140             cr.execute('''select
1141                     id
1142                 from 
1143                     project_task_history
1144                 where
1145                     task_id=%s and
1146                     id<%s
1147                 order by id desc limit 1''', (history.task_id.id, history.id))
1148             res = cr.fetchone()
1149             if res:
1150                 result.append(res[0])
1151         return result
1152
1153     _columns = {
1154         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1155         'type_id': fields.many2one('project.task.type', 'Stage'),
1156         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'),
1157         'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False),
1158         'date': fields.date('Date', select=True),
1159         'end_date': fields.function(_get_date, string='End Date', type="date", store={
1160             'project.task.history': (_get_related_date, None, 20)
1161         }),
1162         'remaining_hours': fields.float('Remaining Time', digits=(16,2)),
1163         'planned_hours': fields.float('Planned Time', digits=(16,2)),
1164         'user_id': fields.many2one('res.users', 'Responsible'),
1165     }
1166     _defaults = {
1167         'date': lambda s,c,u,ctx: time.strftime('%Y-%m-%d')
1168     }
1169 project_task_history()
1170
1171 class project_task_history_cumulative(osv.osv):
1172     _name = 'project.task.history.cumulative'
1173     _table = 'project_task_history_cumulative'
1174     _inherit = 'project.task.history'
1175     _auto = False
1176     _columns = {
1177         'end_date': fields.date('End Date'),
1178         'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project')
1179     }
1180     def init(self, cr):
1181         cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS (
1182             SELECT
1183                 history.date::varchar||'-'||history.history_id::varchar as id,
1184                 history.date as end_date,
1185                 *
1186             FROM (
1187                 SELECT
1188                     id as history_id,
1189                     date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date,
1190                     task_id, type_id, user_id, kanban_state, state,
1191                     remaining_hours, planned_hours
1192                 FROM
1193                     project_task_history
1194             ) as history
1195         )
1196         """)
1197 project_task_history_cumulative()
1198