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], x[2], x[3])), 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)
149 if project.state == 'close':
150 progress_rate = 100.0
152 progress_rate = s[1] and round(min(100.0 * s[2] / s[1], 99.99), 2)
155 'planned_hours': s[0],
156 'effective_hours': s[2],
158 'progress_rate': progress_rate
162 def unlink(self, cr, uid, ids, *args, **kwargs):
163 for proj in self.browse(cr, uid, ids):
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)
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)]}),
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']:
203 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
206 def set_template(self, cr, uid, ids, context=None):
207 res = self.setActive(cr, uid, ids, value=False, context=context)
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)
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)
227 def set_pending(self, cr, uid, ids, context=None):
228 self.write(cr, uid, ids, {'state':'pending'}, context=context)
231 def set_open(self, cr, uid, ids, context=None):
232 self.write(cr, uid, ids, {'state':'open'}, context=context)
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)
242 def copy(self, cr, uid, id, default={}, context=None):
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)
256 def duplicate_template(self, cr, uid, ids, context=None):
259 project_obj = self.pool.get('project.project')
260 data_obj = self.pool.get('ir.model.data')
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')
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)'),
274 'date_start':new_date_start,
276 'parent_id':parent_id}, context=context)
277 result.append(new_id)
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]
282 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
284 if result and len(result):
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'])
293 'name': _('Projects'),
295 'view_mode': 'form,tree',
296 'res_model': 'project.project',
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'],
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()]
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)])
316 self.setActive(cr, uid, child_ids, value, context=None)
321 class users(osv.osv):
322 _inherit = 'res.users'
324 'context_project_id': fields.many2one('project.project', 'Project')
329 _name = "project.task"
330 _description = "Task"
332 _date_name = "date_start"
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')
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)
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'
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')
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())
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
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
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}}
380 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
381 return {'value':{'remaining_hours': planned - effective}}
383 def _default_project(self, cr, uid, context=None):
386 if 'project_id' in context and context['project_id']:
387 return int(context['project_id'])
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)
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']:
408 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
410 for task in self.browse(cr, uid, ids, context=context):
413 if task.project_id.active == False or task.project_id.state == 'template':
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."),
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'),
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)
462 _order = "sequence, priority, date_start, id"
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]
469 if (obj_task.id in children_ids) or (obj_task.id in parent_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())
478 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
481 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
482 if (list(set(parent_ids).intersection(set(s_ids)))):
489 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
492 # Override view according to the company definition
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'
500 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
502 if tm in ['Hours','Hour']:
505 eview = etree.fromstring(res['arch'])
507 def _check_rec(eview):
508 if eview.attrib.get('widget','') == 'float_time':
509 eview.set('widget','float')
516 res['arch'] = etree.tostring(eview)
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)
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:
532 'name': _('Send Email after close task'),
535 'res_model': 'project.task.close',
536 'type': 'ir.actions.act_window',
539 'context': {'active_id': task.id}
543 def do_close(self, cr, uid, ids, context=None):
549 request = self.pool.get('res.request')
550 for task in self.browse(cr, uid, ids, context=context):
551 project = task.project_id
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,
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,),
565 for parent_id in task.parent_ids:
566 if parent_id.state in ('pending','draft'):
568 for child in parent_id.child_ids:
569 if child.id != task.id and child.state not in ('done','cancelled'):
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)
578 def do_reopen(self, cr, uid, ids, context=None):
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,
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,
595 self.write(cr, uid, [task.id], {'state': 'open'})
598 def do_cancel(self, cr, uid, ids, *args):
599 request = self.pool.get('res.request')
600 tasks = self.browse(cr, uid, ids)
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,
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,
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})
618 def do_open(self, cr, uid, ids, *args):
619 tasks= self.browse(cr,uid,ids)
621 data = {'state': 'open'}
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)
629 def do_draft(self, cr, uid, ids, *args):
630 self.write(cr, uid, ids, {'state': 'draft'})
633 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
635 Delegate Task to another users.
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])],
647 'description': delegate_data['new_task_description'] or '',
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),
657 if delegate_data['state'] == 'pending':
658 self.do_pending(cr, uid, [task.id], context)
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)
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)
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 [])
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]})
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 [])
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})
698 class project_work(osv.osv):
699 _name = "project.task.work"
700 _description = "Project Task Work"
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)
711 'user_id': lambda obj, cr, uid, context: uid,
712 'date': time.strftime('%Y-%m-%d %H:%M:%S')
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)
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
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))
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)
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))
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)
766 class account_analytic_account(osv.osv):
768 _inherit = 'account.analytic.account'
769 _description = 'Analytic Account'
771 def create(self, cr, uid, vals, context=None):
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)
778 account_analytic_account()
780 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: