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