27bbb2f8c7f6e155e5b95dc480808b9a567b7325
[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 from operator import itemgetter
26 from itertools import groupby
27
28 from tools.misc import flatten
29 from tools.translate import _
30 from osv import fields, osv
31
32
33 class project_task_type(osv.osv):
34     _name = 'project.task.type'
35     _description = 'Task Stage'
36     _order = 'sequence'
37     _columns = {
38         'name': fields.char('Stage Name', required=True, size=64, translate=True),
39         'description': fields.text('Description'),
40         'sequence': fields.integer('Sequence'),
41     }
42
43     _defaults = {
44         'sequence': 1
45     }
46
47 project_task_type()
48
49 class project(osv.osv):
50     _name = "project.project"
51     _description = "Project"
52     _inherits = {'account.analytic.account': "analytic_account_id"}
53
54     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
55         if user == 1:
56             return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
57         if context and context.has_key('user_prefence') and context['user_prefence']:
58                 cr.execute("""SELECT project.id FROM project_project project
59                            LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
60                            LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
61                            WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
62                 return [(r[0]) for r in cr.fetchall()]
63         return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
64             context=context, count=count)
65
66     def _complete_name(self, cr, uid, ids, name, args, context=None):
67         res = {}
68         for m in self.browse(cr, uid, ids, context=context):
69             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
70         return res
71
72     def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
73         partner_obj = self.pool.get('res.partner')
74         if not part:
75             return {'value':{'contact_id': False, 'pricelist_id': False}}
76         addr = partner_obj.address_get(cr, uid, [part], ['contact'])
77         pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
78         pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
79         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
80
81     def _get_user_and_default_uom_ids(self, cr, uid):
82         users_obj = self.pool.get('res.users')
83         model_data_obj = self.pool.get('ir.model.data')
84         model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
85         default_uom = user_uom = model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
86         obj_tm = users_obj.browse(cr, uid, uid).company_id.project_time_mode_id
87         if obj_tm:
88             user_uom = obj_tm.id
89         return user_uom, default_uom
90
91     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
92         def _get_all_child_projects(ids):
93             """Recursively get child project ids"""
94             child_ids = flatten([project_hierarchy.get(idn, []) for idn in ids])
95             if child_ids:
96                 child_ids = _get_all_child_projects(child_ids)
97             return ids + child_ids
98         # END _get_all_child_projects
99
100         res = {}.fromkeys(ids, 0.0)
101         progress = {}
102         if not ids:
103             return res
104
105         par_child_projects = {}
106         all_projects = list(ids)
107
108         # get project hierarchy:
109         cr.execute('''SELECT prp.id AS pr_parent_id, prpc.id AS pr_child_id
110             FROM account_analytic_account AS p
111             JOIN account_analytic_account AS c ON p.id = c.parent_id
112             JOIN project_project AS prp ON prp.analytic_account_id = p.id
113             JOIN project_project AS prpc ON prpc.analytic_account_id = c.id''')
114
115         project_hierarchy = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(cr.fetchall(), itemgetter(0)))
116
117         for id in ids:
118             child_projects = _get_all_child_projects([id])
119             par_child_projects[id] = child_projects
120             all_projects.extend(child_projects)
121
122         all_projects = dict.fromkeys(all_projects).keys()
123         cr.execute('''SELECT
124                 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
125             FROM
126                 project_task
127             WHERE
128                 project_id IN %s AND
129                 state<>'cancelled'
130             GROUP BY
131                 project_id''',(tuple(all_projects),))
132         progress = dict(map(lambda x: (x[0], (x[1], x[2], x[3])), cr.fetchall()))
133
134         user_uom, def_uom = self._get_user_and_default_uom_ids(cr, uid)
135         for project in self.browse(cr, uid, par_child_projects.keys(), context=context):
136             s = [0.0, 0.0, 0.0]
137             tocompute = par_child_projects[project.id]
138             while tocompute:
139                 p = tocompute.pop()
140                 for i in range(3):
141                     s[i] += progress.get(p, (0.0, 0.0, 0.0))[i]
142
143             uom_obj = self.pool.get('product.uom')
144             if user_uom != def_uom:
145                 s[0] = uom_obj._compute_qty(cr, uid, user_uom, s[0], def_uom)
146                 s[1] = uom_obj._compute_qty(cr, uid, user_uom, s[1], def_uom)
147                 s[2] = uom_obj._compute_qty(cr, uid, user_uom, s[2], def_uom)
148
149             if project.state == 'close':
150                 progress_rate = 100.0
151             else:
152                 progress_rate = s[1] and round(min(100.0 * s[2] / s[1], 99.99), 2)
153
154             res[project.id] = {
155                 'planned_hours': s[0],
156                 'effective_hours': s[2],
157                 'total_hours': s[1],
158                 'progress_rate': progress_rate
159             }
160         return res
161
162     def unlink(self, cr, uid, ids, *args, **kwargs):
163         for proj in self.browse(cr, uid, ids):
164             if proj.tasks:
165                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
166         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
167
168     _columns = {
169         'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
170         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
171         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
172         '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),
173         'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
174         '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)]}),
175         'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
176         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
177         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.", store=True),
178         'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.", store=True),
179         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.", store=True),
180         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.", store=True),
181         '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)]}),
182         '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)]}),
183         '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)]}),
184         'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
185      }
186
187     _order = "sequence"
188
189     _defaults = {
190         'active': True,
191         'priority': 1,
192         'sequence': 10,
193     }
194
195     def _check_dates(self, cr, uid, ids):
196          leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
197          if leave['date_start'] and leave['date']:
198              if leave['date_start'] > leave['date']:
199                  return False
200          return True
201
202     _constraints = [
203         (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
204     ]
205
206     def set_template(self, cr, uid, ids, context=None):
207         res = self.setActive(cr, uid, ids, value=False, context=context)
208         return res
209
210     def set_done(self, cr, uid, ids, context=None):
211         task_obj = self.pool.get('project.task')
212         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
213         task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
214         self.write(cr, uid, ids, {'state':'close'}, context=context)
215         for (id, name) in self.name_get(cr, uid, ids):
216             message = _('Project ') + " '" + name + "' "+ _("is Closed.")
217             self.log(cr, uid, id, message)
218         return True
219
220     def set_cancel(self, cr, uid, ids, context=None):
221         task_obj = self.pool.get('project.task')
222         task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
223         task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
224         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
225         return True
226
227     def set_pending(self, cr, uid, ids, context=None):
228         self.write(cr, uid, ids, {'state':'pending'}, context=context)
229         return True
230
231     def set_open(self, cr, uid, ids, context=None):
232         self.write(cr, uid, ids, {'state':'open'}, context=context)
233         return True
234
235     def reset_project(self, cr, uid, ids, context=None):
236         res = self.setActive(cr, uid, ids, value=True, context=context)
237         for (id, name) in self.name_get(cr, uid, ids):
238             message = _('Project ') + " '" + name + "' "+ _("is Open.")
239             self.log(cr, uid, id, message)
240         return res
241
242     def copy(self, cr, uid, id, default={}, context=None):
243         if context is None:
244             context = {}
245
246         proj = self.browse(cr, uid, id, context=context)
247         default = default or {}
248         context['active_test'] = False
249         default['state'] = 'open'
250         if not default.get('name', False):
251             default['name'] = proj.name + _(' (copy)')
252         res = super(project, self).copy(cr, uid, id, default, context)
253
254         return res
255
256     def duplicate_template(self, cr, uid, ids, context=None):
257         if context is None:
258             context = {}
259         project_obj = self.pool.get('project.project')
260         data_obj = self.pool.get('ir.model.data')
261         result = []
262         for proj in self.browse(cr, uid, ids, context=context):
263             parent_id = context.get('parent_id', False)
264             context.update({'analytic_project_copy': True})
265             new_date_start = time.strftime('%Y-%m-%d')
266             new_date_end = False
267             if proj.date_start and proj.date:
268                 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
269                 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
270                 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
271             new_id = project_obj.copy(cr, uid, proj.id, default = {
272                                     'name': proj.name +_(' (copy)'),
273                                     'state':'open',
274                                     'date_start':new_date_start,
275                                     'date':new_date_end,
276                                     'parent_id':parent_id}, context=context)
277             result.append(new_id)
278
279             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
280             parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
281             if child_ids:
282                 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
283
284         if result and len(result):
285             res_id = result[0]
286             form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
287             form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
288             tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
289             tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
290             search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
291             search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
292             return {
293                 'name': _('Projects'),
294                 'view_type': 'form',
295                 'view_mode': 'form,tree',
296                 'res_model': 'project.project',
297                 'view_id': False,
298                 'res_id': res_id,
299                 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
300                 'type': 'ir.actions.act_window',
301                 'search_view_id': search_view['res_id'],
302                 'nodestroy': True
303             }
304
305     # set active value for a project, its sub projects and its tasks
306     def setActive(self, cr, uid, ids, value=True, context=None):
307         task_obj = self.pool.get('project.task')
308         for proj in self.browse(cr, uid, ids, context=None):
309             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
310             cr.execute('select id from project_task where project_id=%s', (proj.id,))
311             tasks_id = [x[0] for x in cr.fetchall()]
312             if tasks_id:
313                 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
314             child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
315             if child_ids:
316                 self.setActive(cr, uid, child_ids, value, context=None)
317         return True
318     
319 project()
320
321 class users(osv.osv):
322     _inherit = 'res.users'
323     _columns = {
324         'context_project_id': fields.many2one('project.project', 'Project')
325     }
326 users()
327
328 class task(osv.osv):
329     _name = "project.task"
330     _description = "Task"
331     _log_create = True
332     _date_name = "date_start"
333
334     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
335         obj_project = self.pool.get('project.project')
336         for domain in args:
337             if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
338                 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
339                 if id and isinstance(id, (long, int)):
340                     if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
341                         args.append(('active', '=', False))
342         return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
343
344     def _str_get(self, task, level=0, border='***', context=None):
345         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'+ \
346             border[0]+' '+(task.name or '')+'\n'+ \
347             (task.description or '')+'\n\n'
348
349     # Compute: effective_hours, total_hours, progress
350     def _hours_get(self, cr, uid, ids, field_names, args, context=None):
351         project_obj = self.pool.get('project.project')
352         res = {}
353         cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
354         hours = dict(cr.fetchall())
355
356         uom_obj = self.pool.get('product.uom')
357         user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
358         if user_uom != default_uom:
359             for task in self.browse(cr, uid, ids, context=context):
360                 if hours.get(task.id, False):
361                     dur_in_user_uom =  uom_obj._compute_qty(cr, uid, default_uom, hours.get(task.id, 0.0), user_uom)
362                     hours[task.id] = dur_in_user_uom
363
364         for task in self.browse(cr, uid, ids, context=context):
365             res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
366             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
367             res[task.id]['progress'] = 0.0
368             if (task.remaining_hours + hours.get(task.id, 0.0)):
369                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
370             if task.state in ('done','cancelled'):
371                 res[task.id]['progress'] = 100.0
372         return res
373
374
375     def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
376         if remaining and not planned:
377             return {'value':{'planned_hours': remaining}}
378         return {}
379
380     def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
381         return {'value':{'remaining_hours': planned - effective}}
382
383     def _default_project(self, cr, uid, context=None):
384         if context is None:
385             context = {}
386         if 'project_id' in context and context['project_id']:
387             return int(context['project_id'])
388         return False
389
390     def copy_data(self, cr, uid, id, default={}, context=None):
391         default = default or {}
392         default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
393         if not default.get('remaining_hours', False):
394             default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
395         default['active'] = True
396         default['type_id'] = False
397         if not default.get('name', False):
398             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
399         return super(task, self).copy_data(cr, uid, id, default, context)
400
401     def _check_dates(self, cr, uid, ids, context=None):
402         task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
403         if task['date_start'] and task['date_end']:
404              if task['date_start'] > task['date_end']:
405                  return False
406         return True
407
408     def _is_template(self, cr, uid, ids, field_name, arg, context=None):
409         res = {}
410         for task in self.browse(cr, uid, ids, context=context):
411             res[task.id] = True
412             if task.project_id:
413                 if task.project_id.active == False or task.project_id.state == 'template':
414                     res[task.id] = False
415         return res
416
417     _columns = {
418         'active': fields.function(_is_template, method=True, 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."),
419         'name': fields.char('Task Summary', size=128, required=True),
420         'description': fields.text('Description'),
421         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
422         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
423         'type_id': fields.many2one('project.task.type', 'Stage',),
424         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
425                                   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.\
426                                   \n If the task is over, the states is set to \'Done\'.'),
427         'create_date': fields.datetime('Create Date', readonly=True),
428         'date_start': fields.datetime('Starting Date'),
429         'date_end': fields.datetime('Ending Date'),
430         'date_deadline': fields.date('Deadline'),
431         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
432         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
433         'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
434         'notes': fields.text('Notes'),
435         '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.'),
436         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
437         'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
438         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
439         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", store=True, help="Computed as: Time Spent / Total Time."),
440         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as difference of the time estimated by the project manager and the real time to close the task."),
441
442         'user_id': fields.many2one('res.users', 'Assigned to'),
443         'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
444         'partner_id': fields.many2one('res.partner', 'Partner'),
445         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
446         'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
447         'company_id': fields.many2one('res.company', 'Company'),
448         'id': fields.integer('ID'),
449     }
450
451     _defaults = {
452         'state': 'draft',
453         'priority': '2',
454         'progress': 0,
455         'sequence': 10,
456         'active': True,
457         'project_id': _default_project,
458         'user_id': lambda obj, cr, uid, context: uid,
459         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
460     }
461
462     _order = "sequence, priority, date_start, id"
463
464     def _check_recursion(self, cr, uid, ids):
465         obj_task = self.browse(cr, uid, ids[0])
466         parent_ids = [x.id for x in obj_task.parent_ids]
467         children_ids = [x.id for x in obj_task.child_ids]
468
469         if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
470             return False
471
472         while(ids):
473             cr.execute('SELECT DISTINCT task_id '\
474                        'FROM project_task_parent_rel '\
475                        'WHERE parent_id IN %s', (tuple(ids),))
476             child_ids = map(lambda x: x[0], cr.fetchall())
477             c_ids = child_ids
478             if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
479                 return False
480             while len(c_ids):
481                 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
482                 if (list(set(parent_ids).intersection(set(s_ids)))):
483                     return False
484                 c_ids = s_ids
485             ids = child_ids
486         return True
487
488     _constraints = [
489         (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
490     ]
491     #
492     # Override view according to the company definition
493     #
494
495     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
496         users_obj = self.pool.get('res.users')
497         obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
498         tm = obj_tm and obj_tm.name or 'Hours'
499
500         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
501
502         if tm in ['Hours','Hour']:
503             return res
504
505         eview = etree.fromstring(res['arch'])
506
507         def _check_rec(eview):
508             if eview.attrib.get('widget','') == 'float_time':
509                 eview.set('widget','float')
510             for child in eview:
511                 _check_rec(child)
512             return True
513
514         _check_rec(eview)
515
516         res['arch'] = etree.tostring(eview)
517
518         for f in res['fields']:
519             if 'Hours' in res['fields'][f]['string']:
520                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
521         return res
522
523     def action_close(self, cr, uid, ids, context=None):
524         # This action open wizard to send email to partner or project manager after close task.
525         project_id = len(ids) and ids[0] or False
526         if not project_id: return False
527         task = self.browse(cr, uid, project_id, context=context)
528         project = task.project_id
529         res = self.do_close(cr, uid, [project_id], context=context) 
530         if project.warn_manager or project.warn_customer:
531            return {
532                 'name': _('Send Email after close task'),
533                 'view_type': 'form',
534                 'view_mode': 'form',
535                 'res_model': 'project.task.close',
536                 'type': 'ir.actions.act_window',
537                 'target': 'new',
538                 'nodestroy': True,
539                 'context': {'active_id': task.id}
540            } 
541         return res
542
543     def do_close(self, cr, uid, ids, context=None):
544         """
545         Close Task
546         """
547         if context is None:
548             context = {}
549         request = self.pool.get('res.request')
550         for task in self.browse(cr, uid, ids, context=context):
551             project = task.project_id
552             if project:
553                 # Send request to project manager
554                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
555                     request.create(cr, uid, {
556                         'name': _("Task '%s' closed") % task.name,
557                         'state': 'waiting',
558                         'act_from': uid,
559                         'act_to': project.user_id.id,
560                         'ref_partner_id': task.partner_id.id,
561                         'ref_doc1': 'project.task,%d'% (task.id,),
562                         'ref_doc2': 'project.project,%d'% (project.id,),
563                     })
564
565             for parent_id in task.parent_ids:
566                 if parent_id.state in ('pending','draft'):
567                     reopen = True
568                     for child in parent_id.child_ids:
569                         if child.id != task.id and child.state not in ('done','cancelled'):
570                             reopen = False
571                     if reopen:
572                         self.do_reopen(cr, uid, [parent_id.id])
573             self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
574             message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
575             self.log(cr, uid, task.id, message)
576         return True
577
578     def do_reopen(self, cr, uid, ids, context=None):
579         if context is None:
580             context = {}
581         request = self.pool.get('res.request')
582         for task in self.browse(cr, uid, ids, context=context):
583             project = task.project_id
584             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
585                 request.create(cr, uid, {
586                     'name': _("Task '%s' set in progress") % task.name,
587                     'state': 'waiting',
588                     'act_from': uid,
589                     'act_to': project.user_id.id,
590                     'ref_partner_id': task.partner_id.id,
591                     'ref_doc1': 'project.task,%d' % task.id,
592                     'ref_doc2': 'project.project,%d' % project.id,
593                 })
594
595             self.write(cr, uid, [task.id], {'state': 'open'})
596         return True
597
598     def do_cancel(self, cr, uid, ids, *args):
599         request = self.pool.get('res.request')
600         tasks = self.browse(cr, uid, ids)
601         for task in tasks:
602             project = task.project_id
603             if project.warn_manager and project.user_id and (project.user_id.id != uid):
604                 request.create(cr, uid, {
605                     'name': _("Task '%s' cancelled") % task.name,
606                     'state': 'waiting',
607                     'act_from': uid,
608                     'act_to': project.user_id.id,
609                     'ref_partner_id': task.partner_id.id,
610                     'ref_doc1': 'project.task,%d' % task.id,
611                     'ref_doc2': 'project.project,%d' % project.id,
612                 })
613             message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
614             self.log(cr, uid, task.id, message)
615             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
616         return True
617
618     def do_open(self, cr, uid, ids, *args):
619         tasks= self.browse(cr,uid,ids)
620         for t in tasks:
621             data = {'state': 'open'}
622             if not t.date_start:
623                 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
624             self.write(cr, uid, [t.id], data)
625             message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
626             self.log(cr, uid, t.id, message)
627         return True
628
629     def do_draft(self, cr, uid, ids, *args):
630         self.write(cr, uid, ids, {'state': 'draft'})
631         return True
632
633     def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
634         """
635         Delegate Task to another users.
636         """
637         if context is None:
638             context = {}
639         task = self.browse(cr, uid, task_id, context=context)
640         new_task_id = self.copy(cr, uid, task.id, {
641             'name': delegate_data['name'],
642             'user_id': delegate_data['user_id'],
643             'planned_hours': delegate_data['planned_hours'],
644             'remaining_hours': delegate_data['planned_hours'],
645             'parent_ids': [(6, 0, [task.id])],
646             'state': 'draft',
647             'description': delegate_data['new_task_description'] or '',
648             'child_ids': [],
649             'work_ids': []
650         }, context)
651         newname = delegate_data['prefix'] or ''
652         self.write(cr, uid, [task.id], {
653             'remaining_hours': delegate_data['planned_hours_me'],
654             'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
655             'name': newname,
656         }, context)
657         if delegate_data['state'] == 'pending':
658             self.do_pending(cr, uid, [task.id], context)
659         else:
660             self.do_close(cr, uid, [task.id], context)
661         user_pool = self.pool.get('res.users')
662         delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
663         message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_user.name +"' "
664         self.log(cr, uid, task.id, message)
665         return True
666
667     def do_pending(self, cr, uid, ids, *args):
668         self.write(cr, uid, ids, {'state': 'pending'})
669         for (id, name) in self.name_get(cr, uid, ids):
670             message = _('Task ') + " '" + name + "' "+ _("is Pending.")
671             self.log(cr, uid, id, message)
672         return True
673
674     def next_type(self, cr, uid, ids, *args):
675         for task in self.browse(cr, uid, ids):
676             typeid = task.type_id.id
677             types = map(lambda x:x.id, task.project_id.type_ids or [])
678             if types:
679                 if not typeid:
680                     self.write(cr, uid, task.id, {'type_id': types[0]})
681                 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
682                     index = types.index(typeid)
683                     self.write(cr, uid, task.id, {'type_id': types[index+1]})
684         return True
685
686     def prev_type(self, cr, uid, ids, *args):
687         for task in self.browse(cr, uid, ids):
688             typeid = task.type_id.id
689             types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
690             if types:
691                 if typeid and typeid in types:
692                     index = types.index(typeid)
693                     self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
694         return True
695
696 task()
697
698 class project_work(osv.osv):
699     _name = "project.task.work"
700     _description = "Project Task Work"
701     _columns = {
702         'name': fields.char('Work summary', size=128),
703         'date': fields.datetime('Date'),
704         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
705         'hours': fields.float('Time Spent'),
706         'user_id': fields.many2one('res.users', 'Done by', required=True),
707         'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
708     }
709
710     _defaults = {
711         'user_id': lambda obj, cr, uid, context: uid,
712         'date': time.strftime('%Y-%m-%d %H:%M:%S')
713     }
714
715     _order = "date desc"
716
717     def create(self, cr, uid, vals, *args, **kwargs):
718         project_obj = self.pool.get('project.project')
719         uom_obj = self.pool.get('product.uom')
720         if vals.get('hours', False):
721             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
722             duration = vals['hours']
723             if user_uom != default_uom:
724                 duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
725             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
726         return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
727
728     def write(self, cr, uid, ids, vals, context=None):
729         project_obj = self.pool.get('project.project')
730         uom_obj = self.pool.get('product.uom')
731         if vals.get('hours', False):
732             old_hours = self.browse(cr, uid, ids, context=context)
733             user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
734             duration = vals['hours']
735             for old in old_hours:
736                 if vals.get('hours') != old.hours:
737                     # this code is only needed when we update the hours of the project
738                     # TODO: it may still a second calculation if the task.id is changed
739                     # at this task.
740                     if user_uom == default_uom:
741                         for work in self.browse(cr, uid, ids, context=context):
742                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
743                     else:
744                         for work in self.browse(cr, uid, ids, context=context):
745                             duration =  uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
746                             del_work =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
747                             cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
748         return super(project_work,self).write(cr, uid, ids, vals, context=context)
749
750     def unlink(self, cr, uid, ids, *args, **kwargs):
751         context = kwargs.get('context', {})
752         project_obj = self.pool.get('project.project')
753         uom_obj = self.pool.get('product.uom')
754         user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
755         if user_uom == default_uom:
756             for work in self.browse(cr, uid, ids, context):
757                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
758         else:
759             for work in self.browse(cr, uid, ids, context):
760                 duration =  uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
761                 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
762         return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
763
764 project_work()
765
766 class account_analytic_account(osv.osv):
767
768     _inherit = 'account.analytic.account'
769     _description = 'Analytic Account'
770
771     def create(self, cr, uid, vals, context=None):
772         if context is None:
773             context = {}
774         if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
775             vals['child_ids'] = []
776         return super(account_analytic_account, self).create(cr, uid, vals, context=context)
777
778 account_analytic_account()
779
780 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: