[IMP] project: added new field "project_id" on delegrate wizard and refactor stuff...
[odoo/odoo.git] / addons / project / project.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from lxml import etree
23 import time
24 from datetime import datetime, date
25
26 from tools.translate import _
27 from osv import fields, osv
28 from resource.faces import task as Task
29
30 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
31 #class project_project(osv.osv):
32 #    _name = 'project.project'
33 #project_project()
34
35 class project_task_type(osv.osv):
36     _name = 'project.task.type'
37     _description = 'Task Stage'
38     _order = 'sequence'
39     _columns = {
40         'name': fields.char('Stage Name', required=True, size=64, translate=True),
41         'description': fields.text('Description'),
42         'sequence': fields.integer('Sequence'),
43         'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
44         'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
45     }
46     _defaults = {
47         'sequence': 1
48     }
49     _order = 'sequence'
50 project_task_type()
51
52 class project(osv.osv):
53     _name = "project.project"
54     _description = "Project"
55     _inherits = {'account.analytic.account': "analytic_account_id"}
56
57     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
58         if user == 1:
59             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
60         if context and context.get('user_preference'):
61                 cr.execute("""SELECT project.id FROM project_project project
62                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
63                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
64                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
65                 return [(r[0]) for r in cr.fetchall()]
66         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
67             context=context, count=count)
68
69     def _complete_name(self, cr, uid, ids, name, args, context=None):
70         res = {}
71         for m in self.browse(cr, uid, ids, context=context):
72             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
73         return res
74
75     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
76         partner_obj = self.pool.get('res.partner')
77         if not part:
78             return {'value':{'contact_id': False}}
79         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
80         val = {'contact_id': addr['contact']}
81         if 'pricelist_id' in self.fields_get(cr, uid, context=context):
82             pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
83             pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
84             val['pricelist_id'] = pricelist_id
85         return {'value': val}
86
87     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
88         res = {}.fromkeys(ids, 0.0)
89         if not ids:
90             return res
91         cr.execute('''SELECT
92                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
93             FROM
94                 project_task
95             WHERE
96                 project_id in %s AND
97                 state<>'cancelled'
98             GROUP BY
99                 project_id''', (tuple(ids),))
100         progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
101         for project in self.browse(cr, uid, ids, context=context):
102             s = progress.get(project.id, (0.0,0.0,0.0,0.0))
103             res[project.id] = {
104                 'planned_hours': s[0],
105                 'effective_hours': s[2],
106                 'total_hours': s[1],
107                 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
108             }
109         return res
110
111     def _get_project_task(self, cr, uid, ids, context=None):
112         result = {}
113         for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
114             if task.project_id: result[task.project_id.id] = True
115         return result.keys()
116
117     def _get_project_work(self, cr, uid, ids, context=None):
118         result = {}
119         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
120             if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
121         return result.keys()
122
123     def unlink(self, cr, uid, ids, *args, **kwargs):
124         for proj in self.browse(cr, uid, ids):
125             if proj.tasks:
126                 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
127         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
128
129     _columns = {
130         'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
131         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
132         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
133         '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),
134         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
135         '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)]}),
136
137         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
138             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)]}),
139         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
140         '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.",
141             store = {
142                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
143                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
144             }),
145         '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."),
146         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
147         '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.",
148             store = {
149                 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
150                 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
151             }),
152         '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."),
153         '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)]}),
154         '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)]}),
155         '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)]}),
156         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
157      }
158     def _get_type_common(self, cr, uid, context):
159         ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
160         return ids
161
162     _order = "sequence"
163     _defaults = {
164         'active': True,
165         'priority': 1,
166         'sequence': 10,
167         'type_ids': _get_type_common
168     }
169
170     # TODO: Why not using a SQL contraints ?
171     def _check_dates(self, cr, uid, ids, context=None):
172         for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
173             if leave['date_start'] and leave['date']:
174                 if leave['date_start'] > leave['date']:
175                     return False
176         return True
177
178     _constraints = [
179         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
180     ]
181
182     def set_template(self, cr, uid, ids, context=None):
183         res = self.setActive(cr, uid, ids, value=False, context=context)
184         return res
185
186     def set_done(self, cr, uid, ids, context=None):
187         task_obj = self.pool.get('project.task')
188         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
189         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190         self.write(cr, uid, ids, {'state':'close'}, context=context)
191         for (id, name) in self.name_get(cr, uid, ids):
192             message = _("The project '%s' has been closed.") % name
193             self.log(cr, uid, id, message)
194         return True
195
196     def set_cancel(self, cr, uid, ids, context=None):
197         task_obj = self.pool.get('project.task')
198         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
199         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
200         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
201         return True
202
203     def set_pending(self, cr, uid, ids, context=None):
204         self.write(cr, uid, ids, {'state':'pending'}, context=context)
205         return True
206
207     def set_open(self, cr, uid, ids, context=None):
208         self.write(cr, uid, ids, {'state':'open'}, context=context)
209         return True
210
211     def reset_project(self, cr, uid, ids, context=None):
212         res = self.setActive(cr, uid, ids, value=True, context=context)
213         for (id, name) in self.name_get(cr, uid, ids):
214             message = _("The project '%s' has been opened.") % name
215             self.log(cr, uid, id, message)
216         return res
217
218     def copy(self, cr, uid, id, default={}, context=None):
219         if context is None:
220             context = {}
221
222         default = default or {}
223         context['active_test'] = False
224         default['state'] = 'open'
225         proj = self.browse(cr, uid, id, context=context)
226         if not default.get('name', False):
227             default['name'] = proj.name + _(' (copy)')
228
229         res = super(project, self).copy(cr, uid, id, default, context)
230         return res
231
232
233     def template_copy(self, cr, uid, id, default={}, context=None):
234         task_obj = self.pool.get('project.task')
235         proj = self.browse(cr, uid, id, context=context)
236
237         default['tasks'] = [] #avoid to copy all the task automaticly
238         res = self.copy(cr, uid, id, default=default, context=context)
239
240         #copy all the task manually
241         map_task_id = {}
242         for task in proj.tasks:
243             map_task_id[task.id] =  task_obj.copy(cr, uid, task.id, {}, context=context)
244
245         self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
246         task_obj.duplicate_task(cr, uid, map_task_id, context=context)
247
248         return res
249
250     def duplicate_template(self, cr, uid, ids, context=None):
251         if context is None:
252             context = {}
253         data_obj = self.pool.get('ir.model.data')
254         result = []
255         for proj in self.browse(cr, uid, ids, context=context):
256             parent_id = context.get('parent_id', False)
257             context.update({'analytic_project_copy': True})
258             new_date_start = time.strftime('%Y-%m-%d')
259             new_date_end = False
260             if proj.date_start and proj.date:
261                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
262                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
263                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
264             context.update({'copy':True})
265             new_id = self.template_copy(cr, uid, proj.id, default = {
266                                     'name': proj.name +_(' (copy)'),
267                                     'state':'open',
268                                     'date_start':new_date_start,
269                                     'date':new_date_end,
270                                     'parent_id':parent_id}, context=context)
271             result.append(new_id)
272
273             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
274             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
275             if child_ids:
276                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
277
278         if result and len(result):
279             res_id = result[0]
280             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
281             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
282             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
283             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
284             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
285             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
286             return {
287                 'name': _('Projects'),
288                 'view_type': 'form',
289                 'view_mode': 'form,tree',
290                 'res_model': 'project.project',
291                 'view_id': False,
292                 'res_id': res_id,
293                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
294                 'type': 'ir.actions.act_window',
295                 'search_view_id': search_view['res_id'],
296                 'nodestroy': True
297             }
298
299     # set active value for a project, its sub projects and its tasks
300     def setActive(self, cr, uid, ids, value=True, context=None):
301         task_obj = self.pool.get('project.task')
302         for proj in self.browse(cr, uid, ids, context=None):
303             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
304             cr.execute('select id from project_task where project_id=%s', (proj.id,))
305             tasks_id = [x[0] for x in cr.fetchall()]
306             if tasks_id:
307                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
308             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
309             if child_ids:
310                 self.setActive(cr, uid, child_ids, value, context=None)
311         return True
312
313     def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
314         context = context or {}
315         if type(ids) in (long, int,):
316             ids = [ids]
317         projects = self.browse(cr, uid, ids, context=context)
318
319         for project in projects:
320             if (not project.members) and force_members:
321                 raise osv.except_osv(_('Warning !'),_("You must assign members on the project '%s' !") % (project.name,))
322
323         resource_pool = self.pool.get('resource.resource')
324
325         result = "from resource.faces import *\n"
326         result += "import datetime\n"
327         for project in self.browse(cr, uid, ids, context=context):
328             u_ids = [i.id for i in project.members]
329             if project.user_id and (project.user_id.id not in u_ids):
330                 u_ids.append(project.user_id.id)
331             for task in project.tasks:
332                 if task.state in ('done','cancelled'):
333                     continue
334                 if task.user_id and (task.user_id.id not in u_ids):
335                     u_ids.append(task.user_id.id)
336             calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
337             resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
338             for key, vals in resource_objs.items():
339                 result +='''
340 class User_%s(Resource):
341     efficiency = %s
342 ''' % (key,  vals.get('efficiency', False))
343
344         result += '''
345 def Project():
346         '''
347         return result
348
349     def _schedule_project(self, cr, uid, project, context=None):
350         resource_pool = self.pool.get('resource.resource')
351         calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
352         working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
353         # TODO: check if we need working_..., default values are ok.
354         puids = [x.id for x in project.members]
355         if project.user_id:
356             puids.append(project.user_id.id)
357         result = """
358   def Project_%d():
359     start = \'%s\'
360     working_days = %s
361     resource = %s
362 """       % (
363             project.id, 
364             project.date_start, working_days,
365             '|'.join(['User_'+str(x) for x in puids])
366         )
367         vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
368         if vacation:
369             result+= """
370     vacation = %s
371 """ %   ( vacation, )
372         return result
373
374     #TODO: DO Resource allocation and compute availability
375     def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
376         if context ==  None:
377             context = {}
378         allocation = {}
379         return allocation
380
381     def schedule_tasks(self, cr, uid, ids, context=None):
382         context = context or {}
383         if type(ids) in (long, int,):
384             ids = [ids]
385         projects = self.browse(cr, uid, ids, context=context)
386         result = self._schedule_header(cr, uid, ids, False, context=context)
387         for project in projects:
388             result += self._schedule_project(cr, uid, project, context=context)
389             result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
390
391         local_dict = {}
392         exec result in local_dict
393         projects_gantt = Task.BalancedProject(local_dict['Project'])
394
395         for project in projects:
396             project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
397             for task in project.tasks:
398                 if task.state in ('done','cancelled'):
399                     continue
400
401                 p = getattr(project_gantt, 'Task_%d' % (task.id,))
402
403                 self.pool.get('project.task').write(cr, uid, [task.id], {
404                     'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
405                     'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
406                 }, context=context)
407                 if (not task.user_id) and (p.booked_resource):
408                     self.pool.get('project.task').write(cr, uid, [task.id], {
409                         'user_id': int(p.booked_resource[0].name[5:]),
410                     }, context=context)
411         return True
412 project()
413
414 class users(osv.osv):
415     _inherit = 'res.users'
416     _columns = {
417         'context_project_id': fields.many2one('project.project', 'Project')
418     }
419 users()
420
421 class task(osv.osv):
422     _name = "project.task"
423     _description = "Task"
424     _log_create = True
425     _date_name = "date_start"
426
427     def _read_group_type_id(self, cr, uid, ids, domain, context=None):
428         context = context or {}
429         stage_obj = self.pool.get('project.task.type')
430         stage_ids = stage_obj.search(cr, uid, ['|',('id','in',ids)] + [('project_default','=',1)], context=context)
431         return stage_obj.name_get(cr, uid, stage_ids, context=context)
432
433     def _read_group_user_id(self, cr, uid, ids, domain, context={}):
434         context = context or {}
435         if type(context.get('project_id', None)) not in (int, long):
436             return None
437         proj = self.pool.get('project.project').browse(cr, uid, context['project_id'], context=context)
438         ids += map(lambda x: x.id, proj.members)
439         stage_obj = self.pool.get('res.users')
440         stage_ids = stage_obj.search(cr, uid, [('id','in',ids)], context=context)
441         return stage_obj.name_get(cr, uid, ids, context=context)
442
443     _group_by_full = {
444         'type_id': _read_group_type_id,
445         'user_id': _read_group_user_id
446     }
447
448
449     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
450         obj_project = self.pool.get('project.project')
451         for domain in args:
452             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
453                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
454                 if id and isinstance(id, (long, int)):
455                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
456                         args.append(('active', '=', False))
457         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
458
459     def _str_get(self, task, level=0, border='***', context=None):
460         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'+ \
461             border[0]+' '+(task.name or '')+'\n'+ \
462             (task.description or '')+'\n\n'
463
464     # Compute: effective_hours, total_hours, progress
465     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
466         res = {}
467         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
468         hours = dict(cr.fetchall())
469         for task in self.browse(cr, uid, ids, context=context):
470             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)}
471             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
472             res[task.id]['progress'] = 0.0
473             if (task.remaining_hours + hours.get(task.id, 0.0)):
474                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
475             if task.state in ('done','cancelled'):
476                 res[task.id]['progress'] = 100.0
477         return res
478
479
480     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
481         if remaining and not planned:
482             return {'value':{'planned_hours': remaining}}
483         return {}
484
485     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
486         return {'value':{'remaining_hours': planned - effective}}
487
488     def onchange_project(self, cr, uid, id, project_id):
489         if not project_id:
490             return {}
491         data = self.pool.get('project.project').browse(cr, uid, [project_id])
492         partner_id=data and data[0].parent_id.partner_id
493         if partner_id:
494             return {'value':{'partner_id':partner_id.id}}
495         return {}
496
497     def _default_project(self, cr, uid, context=None):
498         if context is None:
499             context = {}
500         if 'project_id' in context and context['project_id']:
501             return int(context['project_id'])
502         return False
503
504     def duplicate_task(self, cr, uid, map_ids, context=None):
505         for new in map_ids.values():
506             task = self.browse(cr, uid, new, context)
507             child_ids = [ ch.id for ch in task.child_ids]
508             if task.child_ids:
509                 for child in task.child_ids:
510                     if child.id in map_ids.keys():
511                         child_ids.remove(child.id)
512                         child_ids.append(map_ids[child.id])
513
514             parent_ids = [ ch.id for ch in task.parent_ids]
515             if task.parent_ids:
516                 for parent in task.parent_ids:
517                     if parent.id in map_ids.keys():
518                         parent_ids.remove(parent.id)
519                         parent_ids.append(map_ids[parent.id])
520             #FIXME why there is already the copy and the old one
521             self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
522
523     def copy_data(self, cr, uid, id, default={}, context=None):
524         default = default or {}
525         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
526         if not default.get('remaining_hours', False):
527             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
528         default['active'] = True
529         default['type_id'] = False
530         if not default.get('name', False):
531             default['name'] = self.browse(cr, uid, id, context=context).name or ''
532             if not context.get('copy',False):
533                 new_name = _("%s (copy)")%default.get('name','')
534                 default.update({'name':new_name})
535         return super(task, self).copy_data(cr, uid, id, default, context)
536
537
538     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
539         res = {}
540         for task in self.browse(cr, uid, ids, context=context):
541             res[task.id] = True
542             if task.project_id:
543                 if task.project_id.active == False or task.project_id.state == 'template':
544                     res[task.id] = False
545         return res
546
547     def _get_task(self, cr, uid, ids, context=None):
548         result = {}
549         for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
550             if work.task_id: result[work.task_id.id] = True
551         return result.keys()
552
553     _columns = {
554         '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."),
555         'name': fields.char('Task Summary', size=128, required=True),
556         'description': fields.text('Description'),
557         'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
558         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
559         'type_id': fields.many2one('project.task.type', 'Stage'),
560         'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
561                                   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.\
562                                   \n If the task is over, the states is set to \'Done\'.'),
563         'kanban_state': fields.selection([('blocked', 'Blocked'),('normal', 'Normal'),('done', 'Done')], 'Kanban State', readonly=True, required=False),
564         'create_date': fields.datetime('Create Date', readonly=True,select=True),
565         'date_start': fields.datetime('Starting Date',select=True),
566         'date_end': fields.datetime('Ending Date',select=True),
567         'date_deadline': fields.date('Deadline',select=True),
568         'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
569         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
570         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
571         'notes': fields.text('Notes'),
572         '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.'),
573         'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
574             store = {
575                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
576                 'project.task.work': (_get_task, ['hours'], 10),
577             }),
578         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
579         'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
580             store = {
581                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
582                 'project.task.work': (_get_task, ['hours'], 10),
583             }),
584         '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",
585             store = {
586                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
587                 'project.task.work': (_get_task, ['hours'], 10),
588             }),
589         '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.",
590             store = {
591                 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
592                 'project.task.work': (_get_task, ['hours'], 10),
593             }),
594         'user_id': fields.many2one('res.users', 'Assigned to'),
595         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
596         'partner_id': fields.many2one('res.partner', 'Partner'),
597         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
598         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
599         'company_id': fields.many2one('res.company', 'Company'),
600         'id': fields.integer('ID', readonly=True),
601         'color': fields.integer('Color Index'),
602         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
603     }
604
605     _defaults = {
606         'state': 'draft',
607         'kanban_state': 'normal',
608         'priority': '2',
609         'progress': 0,
610         'sequence': 10,
611         'active': True,
612         'project_id': _default_project,
613         'user_id': lambda obj, cr, uid, context: uid,
614         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
615     }
616
617     _order = "priority, sequence, date_start, name, id"
618
619     def set_priority(self, cr, uid, ids, priority):
620         """Set task priority
621         """
622         return self.write(cr, uid, ids, {'priority' : priority})
623
624     def set_high_priority(self, cr, uid, ids, *args):
625         """Set task priority to high
626         """
627         return self.set_priority(cr, uid, ids, '1')
628
629     def set_normal_priority(self, cr, uid, ids, *args):
630         """Set task priority to normal
631         """
632         return self.set_priority(cr, uid, ids, '3')
633
634     def _check_recursion(self, cr, uid, ids, context=None):
635         for id in ids:
636             visited_branch = set()
637             visited_node = set()
638             res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
639             if not res:
640                 return False
641
642         return True
643
644     def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
645         if id in visited_branch: #Cycle
646             return False
647
648         if id in visited_node: #Already tested don't work one more time for nothing
649             return True
650
651         visited_branch.add(id)
652         visited_node.add(id)
653
654         #visit child using DFS
655         task = self.browse(cr, uid, id, context=context)
656         for child in task.child_ids:
657             res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
658             if not res:
659                 return False
660
661         visited_branch.remove(id)
662         return True
663
664     def _check_dates(self, cr, uid, ids, context=None):
665         if context == None:
666             context = {}
667         obj_task = self.browse(cr, uid, ids[0], context=context)
668         start = obj_task.date_start or False
669         end = obj_task.date_end or False
670         if start and end :
671             if start > end:
672                 return False
673         return True
674
675     _constraints = [
676         (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
677         (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
678     ]
679     #
680     # Override view according to the company definition
681     #
682     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
683         users_obj = self.pool.get('res.users')
684
685         # read uom as admin to avoid access rights issues, e.g. for portal/share users,
686         # this should be safe (no context passed to avoid side-effects)
687         obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
688         tm = obj_tm and obj_tm.name or 'Hours'
689
690         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
691
692         if tm in ['Hours','Hour']:
693             return res
694
695         eview = etree.fromstring(res['arch'])
696
697         def _check_rec(eview):
698             if eview.attrib.get('widget','') == 'float_time':
699                 eview.set('widget','float')
700             for child in eview:
701                 _check_rec(child)
702             return True
703
704         _check_rec(eview)
705
706         res['arch'] = etree.tostring(eview)
707
708         for f in res['fields']:
709             if 'Hours' in res['fields'][f]['string']:
710                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
711         return res
712
713     def _check_child_task(self, cr, uid, ids, context=None):
714         if context == None:
715             context = {}
716         tasks = self.browse(cr, uid, ids, context=context)
717         for task in tasks:
718             if task.child_ids:
719                 for child in task.child_ids:
720                     if child.state in ['draft', 'open', 'pending']:
721                         raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
722         return True
723
724     def action_close(self, cr, uid, ids, context=None):
725         # This action open wizard to send email to partner or project manager after close task.
726         if context == None:
727             context = {}
728         task_id = len(ids) and ids[0] or False
729         self._check_child_task(cr, uid, ids, context=context)
730         if not task_id: return False
731         task = self.browse(cr, uid, task_id, context=context)
732         project = task.project_id
733         res = self.do_close(cr, uid, [task_id], context=context)
734         if project.warn_manager or project.warn_customer:
735             return {
736                 'name': _('Send Email after close task'),
737                 'view_type': 'form',
738                 'view_mode': 'form',
739                 'res_model': 'mail.compose.message',
740                 'type': 'ir.actions.act_window',
741                 'target': 'new',
742                 'nodestroy': True,
743                 'context': {'active_id': task.id,
744                             'active_model': 'project.task'}
745            }
746         return res
747
748     def do_close(self, cr, uid, ids, context={}):
749         """
750         Close Task
751         """
752         request = self.pool.get('res.request')
753         if not isinstance(ids,list): ids = [ids]
754         for task in self.browse(cr, uid, ids, context=context):
755             vals = {}
756             project = task.project_id
757             if project:
758                 # Send request to project manager
759                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
760                     request.create(cr, uid, {
761                         'name': _("Task '%s' closed") % task.name,
762                         'state': 'waiting',
763                         'act_from': uid,
764                         'act_to': project.user_id.id,
765                         'ref_partner_id': task.partner_id.id,
766                         'ref_doc1': 'project.task,%d'% (task.id,),
767                         'ref_doc2': 'project.project,%d'% (project.id,),
768                     }, context=context)
769
770             for parent_id in task.parent_ids:
771                 if parent_id.state in ('pending','draft'):
772                     reopen = True
773                     for child in parent_id.child_ids:
774                         if child.id != task.id and child.state not in ('done','cancelled'):
775                             reopen = False
776                     if reopen:
777                         self.do_reopen(cr, uid, [parent_id.id], context=context)
778             vals.update({'state': 'done'})
779             vals.update({'remaining_hours': 0.0})
780             if not task.date_end:
781                 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
782             self.write(cr, uid, [task.id],vals, context=context)
783             message = _("The task '%s' is done") % (task.name,)
784             self.log(cr, uid, task.id, message)
785         return True
786
787     def do_reopen(self, cr, uid, ids, context=None):
788         request = self.pool.get('res.request')
789
790         for task in self.browse(cr, uid, ids, context=context):
791             project = task.project_id
792             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
793                 request.create(cr, uid, {
794                     'name': _("Task '%s' set in progress") % task.name,
795                     'state': 'waiting',
796                     'act_from': uid,
797                     'act_to': project.user_id.id,
798                     'ref_partner_id': task.partner_id.id,
799                     'ref_doc1': 'project.task,%d' % task.id,
800                     'ref_doc2': 'project.project,%d' % project.id,
801                 }, context=context)
802
803             self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
804         return True
805
806     def do_cancel(self, cr, uid, ids, context={}):
807         request = self.pool.get('res.request')
808         tasks = self.browse(cr, uid, ids, context=context)
809         self._check_child_task(cr, uid, ids, context=context)
810         for task in tasks:
811             project = task.project_id
812             if project.warn_manager and project.user_id and (project.user_id.id != uid):
813                 request.create(cr, uid, {
814                     'name': _("Task '%s' cancelled") % task.name,
815                     'state': 'waiting',
816                     'act_from': uid,
817                     'act_to': project.user_id.id,
818                     'ref_partner_id': task.partner_id.id,
819                     'ref_doc1': 'project.task,%d' % task.id,
820                     'ref_doc2': 'project.project,%d' % project.id,
821                 }, context=context)
822             message = _("The task '%s' is cancelled.") % (task.name,)
823             self.log(cr, uid, task.id, message)
824             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
825         return True
826
827     def do_open(self, cr, uid, ids, context={}):
828         if not isinstance(ids,list): ids = [ids]
829         tasks= self.browse(cr, uid, ids, context=context)
830         for t in tasks:
831             data = {'state': 'open'}
832             if not t.date_start:
833                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
834             self.write(cr, uid, [t.id], data, context=context)
835             message = _("The task '%s' is opened.") % (t.name,)
836             self.log(cr, uid, t.id, message)
837         return True
838
839     def do_draft(self, cr, uid, ids, context={}):
840         self.write(cr, uid, ids, {'state': 'draft'}, context=context)
841         return True
842
843     def do_delegate(self, cr, uid, ids, delegate_data={}, context=None):
844         """
845         Delegate Task to another users.
846         """
847         assert delegate_data['user_id'], _("Delegated User should be specified")
848         delegrated_tasks = {}
849         for task in self.browse(cr, uid, ids, context=context):
850             delegrated_task_id = self.copy(cr, uid, task.id, {
851                 'name': delegate_data['name'],
852                 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
853                 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
854                 'planned_hours': delegate_data['planned_hours'] or 0.0,
855                 'parent_ids': [(6, 0, [task.id])],
856                 'state': 'draft',
857                 'description': delegate_data['new_task_description'] or '',
858                 'child_ids': [],
859                 'work_ids': []
860             }, context=context)
861             newname = delegate_data['prefix'] or ''
862             task.write({
863                 'remaining_hours': delegate_data['planned_hours_me'],
864                 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
865                 'name': newname,
866             }, context=context)
867             if delegate_data['state'] == 'pending':
868                 self.do_pending(cr, uid, task.id, context=context)
869             elif delegate_data['state'] == 'done':
870                 self.do_close(cr, uid, task.id, context=context)
871             
872             message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_data['user_id'][1])
873             self.log(cr, uid, task.id, message)
874             delegrated_tasks[task.id] = delegrated_task_id
875         return delegrated_tasks
876
877     def do_pending(self, cr, uid, ids, context={}):
878         self.write(cr, uid, ids, {'state': 'pending'}, context=context)
879         for (id, name) in self.name_get(cr, uid, ids):
880             message = _("The task '%s' is pending.") % name
881             self.log(cr, uid, id, message)
882         return True
883
884     def set_remaining_time_1(self, cr, uid, ids, context=None):
885         self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
886         return True
887
888     def set_remaining_time_2(self, cr, uid, ids, context=None):
889         self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
890         return True
891
892     def set_remaining_time_5(self, cr, uid, ids, context=None):
893         self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
894         return True
895
896     def set_remaining_time_10(self, cr, uid, ids, context=None):
897         self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
898         return True
899
900     def set_kanban_state_blocked(self, cr, uid, ids, context=None):
901         self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
902
903     def set_kanban_state_normal(self, cr, uid, ids, context=None):
904         self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
905
906     def set_kanban_state_done(self, cr, uid, ids, context=None):
907         self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
908
909     def _change_type(self, cr, uid, ids, next, *args):
910         """
911             go to the next stage
912             if next is False, go to previous stage
913         """
914         for task in self.browse(cr, uid, ids):
915             if  task.project_id.type_ids:
916                 typeid = task.type_id.id
917                 types_seq={}
918                 for type in task.project_id.type_ids :
919                     types_seq[type.id] = type.sequence
920                 if next:
921                     types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
922                 else:
923                     types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
924                 sorted_types = [x[0] for x in types]
925                 if not typeid:
926                     self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
927                 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
928                     index = sorted_types.index(typeid)
929                     self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
930         return True
931
932     def next_type(self, cr, uid, ids, *args):
933         return self._change_type(cr, uid, ids, True, *args)
934
935     def prev_type(self, cr, uid, ids, *args):
936         return self._change_type(cr, uid, ids, False, *args)
937
938     def unlink(self, cr, uid, ids, context=None):
939         if context == None:
940             context = {}
941         self._check_child_task(cr, uid, ids, context=context)
942         res = super(task, self).unlink(cr, uid, ids, context)
943         return res
944
945     def _generate_task(self, cr, uid, tasks, ident=4, context=None):
946         context = context or {}
947         result = ""
948         ident = ' '*ident
949         for task in tasks:
950             if task.state in ('done','cancelled'):
951                 continue
952             result += '''
953 %sdef Task_%s():
954 %s  todo = \"%.2fH\"
955 %s  effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
956             start = []
957             for t2 in task.parent_ids:
958                 start.append("up.Task_%s.end" % (t2.id,))
959             if start:
960                 result += '''
961 %s  start = max(%s)
962 ''' % (ident,','.join(start))
963
964             if task.user_id:
965                 result += '''
966 %s  resource = %s
967 ''' % (ident, 'User_'+str(task.user_id.id))
968
969         result += "\n"
970         return result
971
972 task()
973
974 class project_work(osv.osv):
975     _name = "project.task.work"
976     _description = "Project Task Work"
977     _columns = {
978         'name': fields.char('Work summary', size=128),
979         'date': fields.datetime('Date', select="1"),
980         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
981         'hours': fields.float('Time Spent'),
982         'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
983         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
984     }
985
986     _defaults = {
987         'user_id': lambda obj, cr, uid, context: uid,
988         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
989     }
990
991     _order = "date desc"
992     def create(self, cr, uid, vals, *args, **kwargs):
993         if 'hours' in vals and (not vals['hours']):
994             vals['hours'] = 0.00
995         if 'task_id' in vals:
996             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
997         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
998
999     def write(self, cr, uid, ids, vals, context=None):
1000         if 'hours' in vals and (not vals['hours']):
1001             vals['hours'] = 0.00
1002         if 'hours' in vals:
1003             for work in self.browse(cr, uid, ids, context=context):
1004                 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))
1005         return super(project_work,self).write(cr, uid, ids, vals, context)
1006
1007     def unlink(self, cr, uid, ids, *args, **kwargs):
1008         for work in self.browse(cr, uid, ids):
1009             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1010         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1011 project_work()
1012
1013 class account_analytic_account(osv.osv):
1014
1015     _inherit = 'account.analytic.account'
1016     _description = 'Analytic Account'
1017
1018     def create(self, cr, uid, vals, context=None):
1019         if context is None:
1020             context = {}
1021         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1022             vals['child_ids'] = []
1023         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
1024     
1025     def unlink(self, cr, uid, ids, *args, **kwargs):
1026         project_obj = self.pool.get('project.project')
1027         analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1028         if analytic_ids:
1029             raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
1030         return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1031
1032 account_analytic_account()
1033
1034 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: