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."),
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."),
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."),
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."),
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)]}),
193 'warn_manager': True,
196 def _check_dates(self, cr, uid, ids):
197 leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
198 if leave['date_start'] and leave['date']:
199 if leave['date_start'] > leave['date']:
204 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
207 def set_template(self, cr, uid, ids, context=None):
208 res = self.setActive(cr, uid, ids, value=False, context=context)
211 def set_done(self, cr, uid, ids, context=None):
212 task_obj = self.pool.get('project.task')
213 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
214 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
215 self.write(cr, uid, ids, {'state':'close'}, context=context)
216 for (id, name) in self.name_get(cr, uid, ids):
217 message = _('Project ') + " '" + name + "' "+ _("is Closed.")
218 self.log(cr, uid, id, message)
221 def set_cancel(self, cr, uid, ids, context=None):
222 task_obj = self.pool.get('project.task')
223 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
224 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
225 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
228 def set_pending(self, cr, uid, ids, context=None):
229 self.write(cr, uid, ids, {'state':'pending'}, context=context)
232 def set_open(self, cr, uid, ids, context=None):
233 self.write(cr, uid, ids, {'state':'open'}, context=context)
236 def reset_project(self, cr, uid, ids, context=None):
237 res = self.setActive(cr, uid, ids, value=True, context=context)
238 for (id, name) in self.name_get(cr, uid, ids):
239 message = _('Project ') + " '" + name + "' "+ _("is Open.")
240 self.log(cr, uid, id, message)
243 def copy(self, cr, uid, id, default={}, context=None):
247 proj = self.browse(cr, uid, id, context=context)
248 default = default or {}
249 context['active_test'] = False
250 default['state'] = 'open'
251 if not default.get('name', False):
252 default['name'] = proj.name + _(' (copy)')
253 res = super(project, self).copy(cr, uid, id, default, context)
257 def duplicate_template(self, cr, uid, ids, context=None):
260 project_obj = self.pool.get('project.project')
261 data_obj = self.pool.get('ir.model.data')
263 for proj in self.browse(cr, uid, ids, context=context):
264 parent_id = context.get('parent_id', False)
265 context.update({'analytic_project_copy': True})
266 new_date_start = time.strftime('%Y-%m-%d')
268 if proj.date_start and proj.date:
269 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
270 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
271 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
272 new_id = project_obj.copy(cr, uid, proj.id, default = {
273 'name': proj.name +_(' (copy)'),
275 'date_start':new_date_start,
277 'parent_id':parent_id}, context=context)
278 result.append(new_id)
280 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
281 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
283 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
285 if result and len(result):
287 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
288 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
289 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
290 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
291 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
292 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
294 'name': _('Projects'),
296 'view_mode': 'form,tree',
297 'res_model': 'project.project',
300 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
301 'type': 'ir.actions.act_window',
302 'search_view_id': search_view['res_id'],
306 # set active value for a project, its sub projects and its tasks
307 def setActive(self, cr, uid, ids, value=True, context=None):
308 task_obj = self.pool.get('project.task')
309 for proj in self.browse(cr, uid, ids, context=None):
310 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
311 cr.execute('select id from project_task where project_id=%s', (proj.id,))
312 tasks_id = [x[0] for x in cr.fetchall()]
314 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
315 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
317 self.setActive(cr, uid, child_ids, value, context=None)
322 class users(osv.osv):
323 _inherit = 'res.users'
325 'context_project_id': fields.many2one('project.project', 'Project')
330 _name = "project.task"
331 _description = "Task"
333 _date_name = "date_start"
335 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
336 obj_project = self.pool.get('project.project')
338 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
339 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
340 if id and isinstance(id, (long, int)):
341 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
342 args.append(('active', '=', False))
343 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
345 def _str_get(self, task, level=0, border='***', context=None):
346 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'+ \
347 border[0]+' '+(task.name or '')+'\n'+ \
348 (task.description or '')+'\n\n'
350 # Compute: effective_hours, total_hours, progress
351 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
352 project_obj = self.pool.get('project.project')
354 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
355 hours = dict(cr.fetchall())
357 uom_obj = self.pool.get('product.uom')
358 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
359 if user_uom != default_uom:
360 for task in self.browse(cr, uid, ids, context=context):
361 if hours.get(task.id, False):
362 dur_in_user_uom = uom_obj._compute_qty(cr, uid, default_uom, hours.get(task.id, 0.0), user_uom)
363 hours[task.id] = dur_in_user_uom
365 for task in self.browse(cr, uid, ids, context=context):
366 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
367 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
368 res[task.id]['progress'] = 0.0
369 if (task.remaining_hours + hours.get(task.id, 0.0)):
370 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
371 if task.state in ('done','cancelled'):
372 res[task.id]['progress'] = 100.0
376 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
377 return {'value':{'remaining_hours': planned - effective}}
379 def _default_project(self, cr, uid, context=None):
382 if 'project_id' in context and context['project_id']:
383 return int(context['project_id'])
386 def copy_data(self, cr, uid, id, default={}, context=None):
387 default = default or {}
388 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
389 if not default.get('remaining_hours', False):
390 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
391 default['active'] = True
392 default['type_id'] = False
393 if not default.get('name', False):
394 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
395 return super(task, self).copy_data(cr, uid, id, default, context)
397 def _check_dates(self, cr, uid, ids, context=None):
398 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
399 if task['date_start'] and task['date_end']:
400 if task['date_start'] > task['date_end']:
404 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
406 for task in self.browse(cr, uid, ids, context=context):
409 if task.project_id.active == False or task.project_id.state == 'template':
414 '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."),
415 'name': fields.char('Task Summary', size=128, required=True),
416 'description': fields.text('Description'),
417 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
418 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
419 'type_id': fields.many2one('project.task.type', 'Stage',),
420 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
421 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.\
422 \n If the task is over, the states is set to \'Done\'.'),
423 'create_date': fields.datetime('Create Date', readonly=True),
424 'date_start': fields.datetime('Starting Date'),
425 'date_end': fields.datetime('Ending Date'),
426 'date_deadline': fields.date('Deadline'),
427 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
428 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
429 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
430 'notes': fields.text('Notes'),
431 '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.'),
432 '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."),
433 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
434 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
435 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", store=True, help="Computed as: Time Spent / Total Time."),
436 '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."),
438 'user_id': fields.many2one('res.users', 'Assigned to'),
439 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
440 'partner_id': fields.many2one('res.partner', 'Partner'),
441 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
442 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
443 'company_id': fields.many2one('res.company', 'Company'),
452 'project_id': _default_project,
453 'user_id': lambda obj, cr, uid, context: uid,
454 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
457 _order = "sequence, priority, date_start, id"
459 def _check_recursion(self, cr, uid, ids):
460 obj_task = self.browse(cr, uid, ids[0])
461 parent_ids = [x.id for x in obj_task.parent_ids]
462 children_ids = [x.id for x in obj_task.child_ids]
464 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
468 cr.execute('SELECT DISTINCT task_id '\
469 'FROM project_task_parent_rel '\
470 'WHERE parent_id IN %s', (tuple(ids),))
471 child_ids = map(lambda x: x[0], cr.fetchall())
473 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
476 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
477 if (list(set(parent_ids).intersection(set(s_ids)))):
484 (_check_dates, 'Error! Task start-date must be lower then task end-date.', ['date_start', 'date_end']),
485 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
488 # Override view according to the company definition
491 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
492 users_obj = self.pool.get('res.users')
493 obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
494 tm = obj_tm and obj_tm.name or 'Hours'
496 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
498 if tm in ['Hours','Hour']:
501 eview = etree.fromstring(res['arch'])
503 def _check_rec(eview):
504 if eview.attrib.get('widget','') == 'float_time':
505 eview.set('widget','float')
512 res['arch'] = etree.tostring(eview)
514 for f in res['fields']:
515 if 'Hours' in res['fields'][f]['string']:
516 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
519 def action_close(self, cr, uid, ids, context=None):
520 # This action open wizard to send email to partner or project manager after close task.
521 project_id = len(ids) and ids[0] or False
522 if not project_id: return False
523 task = self.browse(cr, uid, project_id, context=context)
524 project = task.project_id
525 res = self.do_close(cr, uid, [project_id], context=context)
526 if project.warn_manager or project.warn_customer:
528 'name': _('Send Email after close task'),
531 'res_model': 'project.task.close',
532 'type': 'ir.actions.act_window',
535 'context': {'active_id': task.id}
539 def do_close(self, cr, uid, ids, context=None):
545 request = self.pool.get('res.request')
546 for task in self.browse(cr, uid, ids, context=context):
547 project = task.project_id
549 # Send request to project manager
550 if project.warn_manager and project.user_id and (project.user_id.id != uid):
551 request.create(cr, uid, {
552 'name': _("Task '%s' closed") % task.name,
555 'act_to': project.user_id.id,
556 'ref_partner_id': task.partner_id.id,
557 'ref_doc1': 'project.task,%d'% (task.id,),
558 'ref_doc2': 'project.project,%d'% (project.id,),
561 for parent_id in task.parent_ids:
562 if parent_id.state in ('pending','draft'):
564 for child in parent_id.child_ids:
565 if child.id != task.id and child.state not in ('done','cancelled'):
568 self.do_reopen(cr, uid, [parent_id.id])
569 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
570 message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
571 self.log(cr, uid, task.id, message)
574 def do_reopen(self, cr, uid, ids, context=None):
577 request = self.pool.get('res.request')
578 for task in self.browse(cr, uid, ids, context=context):
579 project = task.project_id
580 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
581 request.create(cr, uid, {
582 'name': _("Task '%s' set in progress") % task.name,
585 'act_to': project.user_id.id,
586 'ref_partner_id': task.partner_id.id,
587 'ref_doc1': 'project.task,%d' % task.id,
588 'ref_doc2': 'project.project,%d' % project.id,
591 self.write(cr, uid, [task.id], {'state': 'open'})
594 def do_cancel(self, cr, uid, ids, *args):
595 request = self.pool.get('res.request')
596 tasks = self.browse(cr, uid, ids)
598 project = task.project_id
599 if project.warn_manager and project.user_id and (project.user_id.id != uid):
600 request.create(cr, uid, {
601 'name': _("Task '%s' cancelled") % task.name,
604 'act_to': project.user_id.id,
605 'ref_partner_id': task.partner_id.id,
606 'ref_doc1': 'project.task,%d' % task.id,
607 'ref_doc2': 'project.project,%d' % project.id,
609 message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
610 self.log(cr, uid, task.id, message)
611 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
614 def do_open(self, cr, uid, ids, *args):
615 tasks= self.browse(cr,uid,ids)
617 self.write(cr, uid, [t.id], {'state': 'open', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S'),})
618 message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
619 self.log(cr, uid, t.id, message)
622 def do_draft(self, cr, uid, ids, *args):
623 self.write(cr, uid, ids, {'state': 'draft'})
626 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
628 Delegate Task to another users.
632 task = self.browse(cr, uid, task_id, context=context)
633 new_task_id = self.copy(cr, uid, task.id, {
634 'name': delegate_data['name'],
635 'user_id': delegate_data['user_id'],
636 'planned_hours': delegate_data['planned_hours'],
637 'remaining_hours': delegate_data['planned_hours'],
638 'parent_ids': [(6, 0, [task.id])],
640 'description': delegate_data['new_task_description'] or '',
644 newname = delegate_data['prefix'] or ''
645 self.write(cr, uid, [task.id], {
646 'remaining_hours': delegate_data['planned_hours_me'],
647 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
650 if delegate_data['state'] == 'pending':
651 self.do_pending(cr, uid, [task.id], context)
653 self.do_close(cr, uid, [task.id], context)
654 user_pool = self.pool.get('res.users')
655 delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
656 message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_user.name +"' "
657 self.log(cr, uid, task.id, message)
660 def do_pending(self, cr, uid, ids, *args):
661 self.write(cr, uid, ids, {'state': 'pending'})
662 for (id, name) in self.name_get(cr, uid, ids):
663 message = _('Task ') + " '" + name + "' "+ _("is Pending.")
664 self.log(cr, uid, id, message)
667 def next_type(self, cr, uid, ids, *args):
668 for task in self.browse(cr, uid, ids):
669 typeid = task.type_id.id
670 types = map(lambda x:x.id, task.project_id.type_ids or [])
673 self.write(cr, uid, task.id, {'type_id': types[0]})
674 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
675 index = types.index(typeid)
676 self.write(cr, uid, task.id, {'type_id': types[index+1]})
679 def prev_type(self, cr, uid, ids, *args):
680 for task in self.browse(cr, uid, ids):
681 typeid = task.type_id.id
682 types = map(lambda x:x.id, task.project_id.type_ids)
684 if typeid and typeid in types:
685 index = types.index(typeid)
686 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
691 class project_work(osv.osv):
692 _name = "project.task.work"
693 _description = "Project Task Work"
695 'name': fields.char('Work summary', size=128),
696 'date': fields.datetime('Date'),
697 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
698 'hours': fields.float('Time Spent'),
699 'user_id': fields.many2one('res.users', 'Done by', required=True),
700 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
704 'user_id': lambda obj, cr, uid, context: uid,
705 'date': time.strftime('%Y-%m-%d %H:%M:%S')
710 def create(self, cr, uid, vals, *args, **kwargs):
711 project_obj = self.pool.get('project.project')
712 uom_obj = self.pool.get('product.uom')
713 if vals.get('hours', False):
714 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
715 duration = vals['hours']
716 if user_uom != default_uom:
717 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
718 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
719 return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
721 def write(self, cr, uid, ids, vals, context=None):
722 project_obj = self.pool.get('project.project')
723 uom_obj = self.pool.get('product.uom')
724 if vals.get('hours', False):
725 old_hours = self.browse(cr, uid, ids, context=context)
726 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
727 duration = vals['hours']
728 for old in old_hours:
729 if vals.get('hours') != old.hours:
730 # this code is only needed when we update the hours of the project
731 # TODO: it may still a second calculation if the task.id is changed
733 if user_uom == default_uom:
734 for work in self.browse(cr, uid, ids, context=context):
735 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
737 for work in self.browse(cr, uid, ids, context=context):
738 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
739 del_work = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
740 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
741 return super(project_work,self).write(cr, uid, ids, vals, context=context)
743 def unlink(self, cr, uid, ids, *args, **kwargs):
744 context = kwargs.get('context', {})
745 project_obj = self.pool.get('project.project')
746 uom_obj = self.pool.get('product.uom')
747 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
748 if user_uom == default_uom:
749 for work in self.browse(cr, uid, ids, context):
750 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
752 for work in self.browse(cr, uid, ids, context):
753 duration = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
754 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
755 return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
759 class account_analytic_account(osv.osv):
761 _inherit = 'account.analytic.account'
762 _description = 'Analytic Account'
764 def create(self, cr, uid, vals, context=None):
767 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
768 vals['child_ids'] = []
769 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
771 account_analytic_account()
773 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: