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