1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from lxml import etree
24 from datetime import datetime, date
25 from operator import itemgetter
26 from itertools import groupby
28 from tools.misc import flatten
29 from tools.translate import _
30 from osv import fields, osv
33 class project_task_type(osv.osv):
34 _name = 'project.task.type'
35 _description = 'Task Stage'
38 'name': fields.char('Stage Name', required=True, size=64, translate=True),
39 'description': fields.text('Description'),
40 'sequence': fields.integer('Sequence'),
49 class project(osv.osv):
50 _name = "project.project"
51 _description = "Project"
52 _inherits = {'account.analytic.account': "analytic_account_id"}
54 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
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)
66 def _complete_name(self, cr, uid, ids, name, args, context=None):
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
72 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
73 partner_obj = self.pool.get('res.partner')
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}}
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
89 return user_uom, default_uom
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])
96 child_ids = _get_all_child_projects(child_ids)
97 return ids + child_ids
98 # END _get_all_child_projects
100 res = {}.fromkeys(ids, 0.0)
105 par_child_projects = {}
106 all_projects = list(ids)
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''')
115 project_hierarchy = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(cr.fetchall(), itemgetter(0)))
118 child_projects = _get_all_child_projects([id])
119 par_child_projects[id] = child_projects
120 all_projects.extend(child_projects)
122 all_projects = dict.fromkeys(all_projects).keys()
124 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
131 project_id''',(tuple(all_projects),))
132 progress = dict(map(lambda x: (x[0], (x[1] or 0.0, x[2] or 0.0, x[3] or 0.0)), cr.fetchall()))
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):
137 tocompute = par_child_projects[project.id]
141 s[i] += progress.get(p, (0.0, 0.0, 0.0))[i]
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 if project.state == 'close':
149 progress_rate = 100.0
151 progress_rate = s[1] and round(min(100.0 * s[2] / s[1], 99.99), 2)
153 'planned_hours': s[0],
154 'effective_hours': s[2],
156 'progress_rate': progress_rate
160 def _get_project_task(self, cr, uid, ids, context=None):
164 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
165 if task.project_id: result[task.project_id.id] = True
168 def _get_project_work(self, cr, uid, ids, context=None):
172 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
173 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
177 def unlink(self, cr, uid, ids, *args, **kwargs):
178 for proj in self.browse(cr, uid, ids):
180 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
181 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
184 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
185 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
186 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
187 '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),
188 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
189 '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)]}),
190 '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)]}),
191 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
192 '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.",
194 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
195 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours'], 10),
196 'project.task.work': (_get_project_work, ['hours'], 10),
198 '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.",
200 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
201 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours'], 10),
202 'project.task.work': (_get_project_work, ['hours'], 10),
204 '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.",
206 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
207 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours'], 10),
208 'project.task.work': (_get_project_work, ['hours'], 10),
210 '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.",
212 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
213 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours'], 10),
214 'project.task.work': (_get_project_work, ['hours'], 10),
216 '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)]}),
217 '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)]}),
218 '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)]}),
219 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
230 def _check_dates(self, cr, uid, ids):
231 leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
232 if leave['date_start'] and leave['date']:
233 if leave['date_start'] > leave['date']:
238 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
241 def set_template(self, cr, uid, ids, context=None):
242 res = self.setActive(cr, uid, ids, value=False, context=context)
245 def set_done(self, cr, uid, ids, context=None):
246 task_obj = self.pool.get('project.task')
247 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
248 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
249 self.write(cr, uid, ids, {'state':'close'}, context=context)
250 for (id, name) in self.name_get(cr, uid, ids):
251 message = _('Project ') + " '" + name + "' "+ _("is Closed.")
252 self.log(cr, uid, id, message)
255 def set_cancel(self, cr, uid, ids, context=None):
256 task_obj = self.pool.get('project.task')
257 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
258 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
259 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
262 def set_pending(self, cr, uid, ids, context=None):
263 self.write(cr, uid, ids, {'state':'pending'}, context=context)
266 def set_open(self, cr, uid, ids, context=None):
267 self.write(cr, uid, ids, {'state':'open'}, context=context)
270 def reset_project(self, cr, uid, ids, context=None):
271 res = self.setActive(cr, uid, ids, value=True, context=context)
272 for (id, name) in self.name_get(cr, uid, ids):
273 message = _('Project ') + " '" + name + "' "+ _("is Open.")
274 self.log(cr, uid, id, message)
277 def copy(self, cr, uid, id, default={}, context=None):
281 proj = self.browse(cr, uid, id, context=context)
282 default = default or {}
283 context['active_test'] = False
284 default['state'] = 'open'
285 if not default.get('name', False):
286 default['name'] = proj.name + _(' (copy)')
287 res = super(project, self).copy(cr, uid, id, default, context)
291 def duplicate_template(self, cr, uid, ids, context=None):
294 project_obj = self.pool.get('project.project')
295 data_obj = self.pool.get('ir.model.data')
297 for proj in self.browse(cr, uid, ids, context=context):
298 parent_id = context.get('parent_id', False)
299 context.update({'analytic_project_copy': True})
300 new_date_start = time.strftime('%Y-%m-%d')
302 if proj.date_start and proj.date:
303 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
304 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
305 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
306 new_id = project_obj.copy(cr, uid, proj.id, default = {
307 'name': proj.name +_(' (copy)'),
309 'date_start':new_date_start,
311 'parent_id':parent_id}, context=context)
312 result.append(new_id)
314 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
315 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
317 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
319 if result and len(result):
321 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
322 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
323 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
324 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
325 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
326 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
328 'name': _('Projects'),
330 'view_mode': 'form,tree',
331 'res_model': 'project.project',
334 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
335 'type': 'ir.actions.act_window',
336 'search_view_id': search_view['res_id'],
340 # set active value for a project, its sub projects and its tasks
341 def setActive(self, cr, uid, ids, value=True, context=None):
342 task_obj = self.pool.get('project.task')
343 for proj in self.browse(cr, uid, ids, context=None):
344 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
345 cr.execute('select id from project_task where project_id=%s', (proj.id,))
346 tasks_id = [x[0] for x in cr.fetchall()]
348 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
349 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
351 self.setActive(cr, uid, child_ids, value, context=None)
356 class users(osv.osv):
357 _inherit = 'res.users'
359 'context_project_id': fields.many2one('project.project', 'Project')
364 _name = "project.task"
365 _description = "Task"
367 _date_name = "date_start"
369 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
370 obj_project = self.pool.get('project.project')
372 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
373 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
374 if id and isinstance(id, (long, int)):
375 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
376 args.append(('active', '=', False))
377 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
379 def _str_get(self, task, level=0, border='***', context=None):
380 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'+ \
381 border[0]+' '+(task.name or '')+'\n'+ \
382 (task.description or '')+'\n\n'
384 # Compute: effective_hours, total_hours, progress
385 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
386 project_obj = self.pool.get('project.project')
388 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
389 hours = dict(cr.fetchall())
391 uom_obj = self.pool.get('product.uom')
392 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
393 if user_uom != default_uom:
394 for task in self.browse(cr, uid, ids, context=context):
395 if hours.get(task.id, False):
396 dur_in_user_uom = uom_obj._compute_qty(cr, uid, default_uom, hours.get(task.id, 0.0), user_uom)
397 hours[task.id] = dur_in_user_uom
399 for task in self.browse(cr, uid, ids, context=context):
400 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
401 # res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
402 for ctimespent in task.work_ids:
403 timespent=timespent+ctimespent.hours
404 res[task.id]['delay_hours'] = task.planned_hours - timespent
405 res[task.id]['progress'] = 0.0
406 if (task.remaining_hours + hours.get(task.id, 0.0)):
407 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
408 if task.state in ('done','cancelled'):
409 res[task.id]['progress'] = 100.0
413 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
414 if remaining and not planned:
415 return {'value':{'planned_hours': remaining}}
418 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
419 return {'value':{'remaining_hours': planned - effective}}
421 def _default_project(self, cr, uid, context=None):
424 if 'project_id' in context and context['project_id']:
425 return int(context['project_id'])
428 def copy_data(self, cr, uid, id, default={}, context=None):
429 default = default or {}
430 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
431 if not default.get('remaining_hours', False):
432 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
433 default['active'] = True
434 default['type_id'] = False
435 if not default.get('name', False):
436 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
437 return super(task, self).copy_data(cr, uid, id, default, context)
439 def _check_dates(self, cr, uid, ids, context=None):
440 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
441 if task['date_start'] and task['date_end']:
442 if task['date_start'] > task['date_end']:
446 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
448 for task in self.browse(cr, uid, ids, context=context):
451 if task.project_id.active == False or task.project_id.state == 'template':
455 def _get_task(self, cr, uid, ids, context=None):
459 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
460 if work.task_id: result[work.task_id.id] = True
464 '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."),
465 'name': fields.char('Task Summary', size=128, required=True),
466 'description': fields.text('Description'),
467 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
468 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
469 'type_id': fields.many2one('project.task.type', 'Type',),
470 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
471 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.\
472 \n If the task is over, the states is set to \'Done\'.'),
473 'create_date': fields.datetime('Create Date', readonly=True),
474 'date_start': fields.datetime('Starting Date'),
475 'date_end': fields.datetime('Ending Date'),
476 'date_deadline': fields.date('Deadline'),
477 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
478 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
479 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
480 'notes': fields.text('Notes'),
481 '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.'),
482 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
484 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
485 'project.task.work': (_get_task, ['hours'], 10),
487 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
488 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
490 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
491 'project.task.work': (_get_task, ['hours'], 10),
493 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
495 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
496 'project.task.work': (_get_task, ['hours'], 10),
498 'delay_hours': fields.function(_hours_get, method=True, 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.",
500 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids'], 10),
501 'project.task.work': (_get_task, ['hours'], 10),
503 'user_id': fields.many2one('res.users', 'Assigned to'),
504 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
505 'partner_id': fields.many2one('res.partner', 'Partner'),
506 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
507 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
508 'company_id': fields.many2one('res.company', 'Company'),
509 'id': fields.integer('ID'),
518 'project_id': _default_project,
519 'user_id': lambda obj, cr, uid, context: uid,
520 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
523 _order = "sequence, priority, date_start, id"
525 def _check_recursion(self, cr, uid, ids):
526 obj_task = self.browse(cr, uid, ids[0])
527 parent_ids = [x.id for x in obj_task.parent_ids]
528 children_ids = [x.id for x in obj_task.child_ids]
530 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
534 cr.execute('SELECT DISTINCT task_id '\
535 'FROM project_task_parent_rel '\
536 'WHERE parent_id IN %s', (tuple(ids),))
537 child_ids = map(lambda x: x[0], cr.fetchall())
539 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
542 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
543 if (list(set(parent_ids).intersection(set(s_ids)))):
550 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
553 # Override view according to the company definition
556 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
557 users_obj = self.pool.get('res.users')
558 obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
559 tm = obj_tm and obj_tm.name or 'Hours'
561 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
563 if tm in ['Hours','Hour']:
566 eview = etree.fromstring(res['arch'])
568 def _check_rec(eview):
569 if eview.attrib.get('widget','') == 'float_time':
570 eview.set('widget','float')
577 res['arch'] = etree.tostring(eview)
579 for f in res['fields']:
580 if 'Hours' in res['fields'][f]['string']:
581 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
584 def action_close(self, cr, uid, ids, context=None):
585 # This action open wizard to send email to partner or project manager after close task.
586 project_id = len(ids) and ids[0] or False
587 if not project_id: return False
588 task = self.browse(cr, uid, project_id, context=context)
589 project = task.project_id
590 res = self.do_close(cr, uid, [project_id], context=context)
591 if project.warn_manager or project.warn_customer:
593 'name': _('Send Email after close task'),
596 'res_model': 'project.task.close',
597 'type': 'ir.actions.act_window',
600 'context': {'active_id': task.id}
604 def do_close(self, cr, uid, ids, context=None):
610 request = self.pool.get('res.request')
611 for task in self.browse(cr, uid, ids, context=context):
612 project = task.project_id
614 # Send request to project manager
615 if project.warn_manager and project.user_id and (project.user_id.id != uid):
616 request.create(cr, uid, {
617 'name': _("Task '%s' closed") % task.name,
620 'act_to': project.user_id.id,
621 'ref_partner_id': task.partner_id.id,
622 'ref_doc1': 'project.task,%d'% (task.id,),
623 'ref_doc2': 'project.project,%d'% (project.id,),
626 for parent_id in task.parent_ids:
627 if parent_id.state in ('pending','draft'):
629 for child in parent_id.child_ids:
630 if child.id != task.id and child.state not in ('done','cancelled'):
633 self.do_reopen(cr, uid, [parent_id.id])
634 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
635 message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
636 self.log(cr, uid, task.id, message)
639 def do_reopen(self, cr, uid, ids, context=None):
642 request = self.pool.get('res.request')
644 for task in self.browse(cr, uid, ids, context=context):
645 project = task.project_id
646 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
647 request.create(cr, uid, {
648 'name': _("Task '%s' set in progress") % task.name,
651 'act_to': project.user_id.id,
652 'ref_partner_id': task.partner_id.id,
653 'ref_doc1': 'project.task,%d' % task.id,
654 'ref_doc2': 'project.project,%d' % project.id,
657 self.write(cr, uid, [task.id], {'state': 'open'})
661 def do_cancel(self, cr, uid, ids, *args):
662 request = self.pool.get('res.request')
663 tasks = self.browse(cr, uid, ids)
665 project = task.project_id
666 if project.warn_manager and project.user_id and (project.user_id.id != uid):
667 request.create(cr, uid, {
668 'name': _("Task '%s' cancelled") % task.name,
671 'act_to': project.user_id.id,
672 'ref_partner_id': task.partner_id.id,
673 'ref_doc1': 'project.task,%d' % task.id,
674 'ref_doc2': 'project.project,%d' % project.id,
676 message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
677 self.log(cr, uid, task.id, message)
678 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
681 def do_open(self, cr, uid, ids, *args):
682 tasks= self.browse(cr,uid,ids)
684 data = {'state': 'open'}
686 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
687 self.write(cr, uid, [t.id], data)
688 message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
689 self.log(cr, uid, t.id, message)
692 def do_draft(self, cr, uid, ids, *args):
693 self.write(cr, uid, ids, {'state': 'draft'})
696 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
698 Delegate Task to another users.
702 task = self.browse(cr, uid, task_id, context=context)
703 new_task_id = self.copy(cr, uid, task.id, {
704 'name': delegate_data['name'],
705 'user_id': delegate_data['user_id'],
706 'planned_hours': delegate_data['planned_hours'],
707 'remaining_hours': delegate_data['planned_hours'],
708 'parent_ids': [(6, 0, [task.id])],
710 'description': delegate_data['new_task_description'] or '',
714 newname = delegate_data['prefix'] or ''
715 self.write(cr, uid, [task.id], {
716 'remaining_hours': delegate_data['planned_hours_me'],
717 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
720 if delegate_data['state'] == 'pending':
721 self.do_pending(cr, uid, [task.id], context)
723 self.do_close(cr, uid, [task.id], context)
724 user_pool = self.pool.get('res.users')
725 delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
726 message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_user.name +"' "
727 self.log(cr, uid, task.id, message)
730 def do_pending(self, cr, uid, ids, *args):
731 self.write(cr, uid, ids, {'state': 'pending'})
732 for (id, name) in self.name_get(cr, uid, ids):
733 message = _('Task ') + " '" + name + "' "+ _("is Pending.")
734 self.log(cr, uid, id, message)
737 def next_type(self, cr, uid, ids, *args):
738 for task in self.browse(cr, uid, ids):
739 typeid = task.type_id.id
740 types = map(lambda x:x.id, task.project_id.type_ids or [])
743 self.write(cr, uid, task.id, {'type_id': types[0]})
744 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
745 index = types.index(typeid)
746 self.write(cr, uid, task.id, {'type_id': types[index+1]})
749 def prev_type(self, cr, uid, ids, *args):
750 for task in self.browse(cr, uid, ids):
751 typeid = task.type_id.id
752 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
754 if typeid and typeid in types:
755 index = types.index(typeid)
756 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
761 class project_work(osv.osv):
762 _name = "project.task.work"
763 _description = "Project Task Work"
765 'name': fields.char('Work summary', size=128),
766 'date': fields.datetime('Date'),
767 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
768 'hours': fields.float('Time Spent'),
769 'user_id': fields.many2one('res.users', 'Done by', required=True),
770 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
774 'user_id': lambda obj, cr, uid, context: uid,
775 'date': time.strftime('%Y-%m-%d %H:%M:%S')
780 def create(self, cr, uid, vals, *args, **kwargs):
781 project_obj = self.pool.get('project.project')
782 uom_obj = self.pool.get('product.uom')
783 if vals.get('hours', False):
784 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
785 duration = vals['hours']
786 if user_uom != default_uom:
787 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
788 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
789 return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
791 def write(self, cr, uid, ids, vals, context=None):
792 project_obj = self.pool.get('project.project')
793 uom_obj = self.pool.get('product.uom')
794 if vals.get('hours', False):
795 old_hours = self.browse(cr, uid, ids, context=context)
796 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
797 duration = vals['hours']
798 for old in old_hours:
799 if vals.get('hours') != old.hours:
800 # this code is only needed when we update the hours of the project
801 # TODO: it may still a second calculation if the task.id is changed
803 if user_uom == default_uom:
804 for work in self.browse(cr, uid, ids, context=context):
805 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
807 for work in self.browse(cr, uid, ids, context=context):
808 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
809 del_work = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
810 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
811 return super(project_work,self).write(cr, uid, ids, vals, context=context)
813 def unlink(self, cr, uid, ids, *args, **kwargs):
814 context = kwargs.get('context', {})
815 project_obj = self.pool.get('project.project')
816 uom_obj = self.pool.get('product.uom')
817 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
818 if user_uom == default_uom:
819 for work in self.browse(cr, uid, ids, context):
820 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
822 for work in self.browse(cr, uid, ids, context):
823 duration = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
824 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
825 return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
829 class account_analytic_account(osv.osv):
831 _inherit = 'account.analytic.account'
832 _description = 'Analytic Account'
834 def create(self, cr, uid, vals, context=None):
837 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
838 vals['child_ids'] = []
839 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
841 account_analytic_account()
843 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: