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
26 from tools.translate import _
27 from osv import fields, osv
30 class project_task_type(osv.osv):
31 _name = 'project.task.type'
32 _description = 'Task Stage'
35 'name': fields.char('Stage Name', required=True, size=64, translate=True),
36 'description': fields.text('Description'),
37 'sequence': fields.integer('Sequence'),
46 class project(osv.osv):
47 _name = "project.project"
48 _description = "Project"
49 _inherits = {'account.analytic.account': "analytic_account_id"}
51 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
53 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
54 if context and context.has_key('user_prefence') and context['user_prefence']:
55 cr.execute("""SELECT project.id FROM project_project project
56 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
57 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
58 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
59 return [(r[0]) for r in cr.fetchall()]
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
61 context=context, count=count)
63 def _complete_name(self, cr, uid, ids, name, args, context=None):
65 for m in self.browse(cr, uid, ids, context=context):
66 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
69 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
70 partner_obj = self.pool.get('res.partner')
72 return {'value':{'contact_id': False, 'pricelist_id': False}}
73 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
74 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
75 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
76 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
78 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
79 res = {}.fromkeys(ids, 0.0)
84 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
91 project_id''', (tuple(ids),))
92 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
93 for project in self.browse(cr, uid, ids, context=context):
94 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
96 'planned_hours': s[0],
97 'effective_hours': s[2],
99 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
103 def _get_project_task(self, cr, uid, ids, context=None):
105 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
106 if task.project_id: result[task.project_id.id] = True
109 def _get_project_work(self, cr, uid, ids, context=None):
111 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
112 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
115 def unlink(self, cr, uid, ids, *args, **kwargs):
116 for proj in self.browse(cr, uid, ids):
118 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
119 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
122 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
123 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
124 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
125 '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),
126 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
127 '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)]}),
129 '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, but a user has to be member of a project to add a the to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
130 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
131 '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.",
133 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
134 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
136 '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.",
138 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
139 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
141 '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.",
143 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
144 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
146 '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.",
148 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
149 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
150 'project.task.work': (_get_project_work, ['hours'], 10),
152 '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)]}),
153 '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)]}),
154 '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)]}),
155 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
164 # TODO: Why not using a SQL contraints ?
165 def _check_dates(self, cr, uid, ids, context=None):
166 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
167 if leave['date_start'] and leave['date']:
168 if leave['date_start'] > leave['date']:
173 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
176 def set_template(self, cr, uid, ids, context=None):
177 res = self.setActive(cr, uid, ids, value=False, context=context)
180 def set_done(self, cr, uid, ids, context=None):
181 task_obj = self.pool.get('project.task')
182 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
183 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
184 self.write(cr, uid, ids, {'state':'close'}, context=context)
185 for (id, name) in self.name_get(cr, uid, ids):
186 message = _("The project '%s' has been closed.") % name
187 self.log(cr, uid, id, message)
190 def set_cancel(self, cr, uid, ids, context=None):
191 task_obj = self.pool.get('project.task')
192 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
193 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
194 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
197 def set_pending(self, cr, uid, ids, context=None):
198 self.write(cr, uid, ids, {'state':'pending'}, context=context)
201 def set_open(self, cr, uid, ids, context=None):
202 self.write(cr, uid, ids, {'state':'open'}, context=context)
205 def reset_project(self, cr, uid, ids, context=None):
206 res = self.setActive(cr, uid, ids, value=True, context=context)
207 for (id, name) in self.name_get(cr, uid, ids):
208 message = _("The project '%s' has been opened.") % name
209 self.log(cr, uid, id, message)
212 def copy(self, cr, uid, id, default={}, context=None):
216 proj = self.browse(cr, uid, id, context=context)
217 default = default or {}
218 context['active_test'] = False
219 default['state'] = 'open'
220 if not default.get('name', False):
221 default['name'] = proj.name + _(' (copy)')
222 res = super(project, self).copy(cr, uid, id, default, context)
226 def duplicate_template(self, cr, uid, ids, context=None):
229 project_obj = self.pool.get('project.project')
230 data_obj = self.pool.get('ir.model.data')
232 for proj in self.browse(cr, uid, ids, context=context):
233 parent_id = context.get('parent_id', False)
234 context.update({'analytic_project_copy': True})
235 new_date_start = time.strftime('%Y-%m-%d')
237 if proj.date_start and proj.date:
238 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
239 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
240 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
241 new_id = project_obj.copy(cr, uid, proj.id, default = {
242 'name': proj.name +_(' (copy)'),
244 'date_start':new_date_start,
246 'parent_id':parent_id}, context=context)
247 result.append(new_id)
249 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
250 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
252 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
254 if result and len(result):
256 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
257 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
258 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
259 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
260 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
261 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
263 'name': _('Projects'),
265 'view_mode': 'form,tree',
266 'res_model': 'project.project',
269 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
270 'type': 'ir.actions.act_window',
271 'search_view_id': search_view['res_id'],
275 # set active value for a project, its sub projects and its tasks
276 def setActive(self, cr, uid, ids, value=True, context=None):
277 task_obj = self.pool.get('project.task')
278 for proj in self.browse(cr, uid, ids, context=None):
279 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
280 cr.execute('select id from project_task where project_id=%s', (proj.id,))
281 tasks_id = [x[0] for x in cr.fetchall()]
283 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
284 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
286 self.setActive(cr, uid, child_ids, value, context=None)
291 class users(osv.osv):
292 _inherit = 'res.users'
294 'context_project_id': fields.many2one('project.project', 'Project')
299 _name = "project.task"
300 _description = "Task"
302 _date_name = "date_start"
304 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
305 obj_project = self.pool.get('project.project')
307 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
308 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
309 if id and isinstance(id, (long, int)):
310 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
311 args.append(('active', '=', False))
312 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
314 def _str_get(self, task, level=0, border='***', context=None):
315 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'+ \
316 border[0]+' '+(task.name or '')+'\n'+ \
317 (task.description or '')+'\n\n'
319 # Compute: effective_hours, total_hours, progress
320 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
322 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
323 hours = dict(cr.fetchall())
324 for task in self.browse(cr, uid, ids, context=context):
325 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
326 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
327 res[task.id]['progress'] = 0.0
328 if (task.remaining_hours + hours.get(task.id, 0.0)):
329 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
330 if task.state in ('done','cancelled'):
331 res[task.id]['progress'] = 100.0
335 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
336 if remaining and not planned:
337 return {'value':{'planned_hours': remaining}}
340 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
341 return {'value':{'remaining_hours': planned - effective}}
343 def onchange_project(self, cr, uid, id, project_id):
346 data = self.pool.get('project.project').browse(cr, uid, [project_id])
347 partner_id=data and data[0].parent_id.partner_id
349 return {'value':{'partner_id':partner_id.id}}
352 def _default_project(self, cr, uid, context=None):
355 if 'project_id' in context and context['project_id']:
356 return int(context['project_id'])
359 def copy_data(self, cr, uid, id, default={}, context=None):
360 default = default or {}
361 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
362 if not default.get('remaining_hours', False):
363 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
364 default['active'] = True
365 default['type_id'] = False
366 if not default.get('name', False):
367 default['name'] = self.browse(cr, uid, id, context=context).name
368 return super(task, self).copy_data(cr, uid, id, default, context)
370 def _check_dates(self, cr, uid, ids, context=None):
371 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
372 if task['date_start'] and task['date_end']:
373 if task['date_start'] > task['date_end']:
377 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
379 for task in self.browse(cr, uid, ids, context=context):
382 if task.project_id.active == False or task.project_id.state == 'template':
386 def _get_task(self, cr, uid, ids, context=None):
388 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
389 if work.task_id: result[work.task_id.id] = True
393 '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."),
394 'name': fields.char('Task Summary', size=128, required=True),
395 'description': fields.text('Description'),
396 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
397 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
398 'type_id': fields.many2one('project.task.type', 'Stage'),
399 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
400 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.\
401 \n If the task is over, the states is set to \'Done\'.'),
402 'create_date': fields.datetime('Create Date', readonly=True,select=True),
403 'date_start': fields.datetime('Starting Date',select=True),
404 'date_end': fields.datetime('Ending Date',select=True),
405 'date_deadline': fields.date('Deadline',select=True),
406 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
407 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
408 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
409 'notes': fields.text('Notes'),
410 '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.'),
411 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
413 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
414 'project.task.work': (_get_task, ['hours'], 10),
416 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
417 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
419 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
420 'project.task.work': (_get_task, ['hours'], 10),
422 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
424 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
425 'project.task.work': (_get_task, ['hours'], 10),
427 '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.",
429 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
430 'project.task.work': (_get_task, ['hours'], 10),
432 'user_id': fields.many2one('res.users', 'Assigned to'),
433 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
434 'partner_id': fields.many2one('res.partner', 'Partner'),
435 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
436 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
437 'company_id': fields.many2one('res.company', 'Company'),
438 'id': fields.integer('ID'),
447 'project_id': _default_project,
448 'user_id': lambda obj, cr, uid, context: uid,
449 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
452 _order = "sequence,priority, date_start, name, id"
454 def _check_recursion(self, cr, uid, ids, context=None):
455 obj_task = self.browse(cr, uid, ids[0], context=context)
456 parent_ids = [x.id for x in obj_task.parent_ids]
457 children_ids = [x.id for x in obj_task.child_ids]
459 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
463 cr.execute('SELECT DISTINCT task_id '\
464 'FROM project_task_parent_rel '\
465 'WHERE parent_id IN %s', (tuple(ids),))
466 child_ids = map(lambda x: x[0], cr.fetchall())
468 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
471 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
472 if (list(set(parent_ids).intersection(set(s_ids)))):
479 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids'])
482 # Override view according to the company definition
486 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
487 users_obj = self.pool.get('res.users')
489 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
490 # this should be safe (no context passed to avoid side-effects)
491 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
492 tm = obj_tm and obj_tm.name or 'Hours'
494 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
496 if tm in ['Hours','Hour']:
499 eview = etree.fromstring(res['arch'])
501 def _check_rec(eview):
502 if eview.attrib.get('widget','') == 'float_time':
503 eview.set('widget','float')
510 res['arch'] = etree.tostring(eview)
512 for f in res['fields']:
513 if 'Hours' in res['fields'][f]['string']:
514 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
517 def action_close(self, cr, uid, ids, context=None):
518 # This action open wizard to send email to partner or project manager after close task.
519 project_id = len(ids) and ids[0] or False
520 if not project_id: return False
521 task = self.browse(cr, uid, project_id, context=context)
522 project = task.project_id
523 res = self.do_close(cr, uid, [project_id], context=context)
524 if project.warn_manager or project.warn_customer:
526 'name': _('Send Email after close task'),
529 'res_model': 'project.task.close',
530 'type': 'ir.actions.act_window',
533 'context': {'active_id': task.id}
537 def do_close(self, cr, uid, ids, context=None):
541 request = self.pool.get('res.request')
542 for task in self.browse(cr, uid, ids, context=context):
543 project = task.project_id
545 # Send request to project manager
546 if project.warn_manager and project.user_id and (project.user_id.id != uid):
547 request.create(cr, uid, {
548 'name': _("Task '%s' closed") % task.name,
551 'act_to': project.user_id.id,
552 'ref_partner_id': task.partner_id.id,
553 'ref_doc1': 'project.task,%d'% (task.id,),
554 'ref_doc2': 'project.project,%d'% (project.id,),
557 for parent_id in task.parent_ids:
558 if parent_id.state in ('pending','draft'):
560 for child in parent_id.child_ids:
561 if child.id != task.id and child.state not in ('done','cancelled'):
564 self.do_reopen(cr, uid, [parent_id.id])
565 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
566 message = _("The task '%s' is done") % (task.name,)
567 self.log(cr, uid, task.id, message)
570 def do_reopen(self, cr, uid, ids, context=None):
571 request = self.pool.get('res.request')
573 for task in self.browse(cr, uid, ids, context=context):
574 project = task.project_id
575 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
576 request.create(cr, uid, {
577 'name': _("Task '%s' set in progress") % task.name,
580 'act_to': project.user_id.id,
581 'ref_partner_id': task.partner_id.id,
582 'ref_doc1': 'project.task,%d' % task.id,
583 'ref_doc2': 'project.project,%d' % project.id,
586 self.write(cr, uid, [task.id], {'state': 'open'})
590 def do_cancel(self, cr, uid, ids, *args):
591 request = self.pool.get('res.request')
592 tasks = self.browse(cr, uid, ids)
594 project = task.project_id
595 if project.warn_manager and project.user_id and (project.user_id.id != uid):
596 request.create(cr, uid, {
597 'name': _("Task '%s' cancelled") % task.name,
600 'act_to': project.user_id.id,
601 'ref_partner_id': task.partner_id.id,
602 'ref_doc1': 'project.task,%d' % task.id,
603 'ref_doc2': 'project.project,%d' % project.id,
605 message = _("The task '%s' is cancelled.") % (task.name,)
606 self.log(cr, uid, task.id, message)
607 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
610 def do_open(self, cr, uid, ids, *args):
611 tasks= self.browse(cr,uid,ids)
613 data = {'state': 'open'}
615 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
616 self.write(cr, uid, [t.id], data)
617 message = _("The task '%s' is opened.") % (t.name,)
618 self.log(cr, uid, t.id, message)
621 def do_draft(self, cr, uid, ids, *args):
622 self.write(cr, uid, ids, {'state': 'draft'})
625 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
627 Delegate Task to another users.
629 task = self.browse(cr, uid, task_id, context=context)
630 self.copy(cr, uid, task.id, {
631 'name': delegate_data['name'],
632 'user_id': delegate_data['user_id'],
633 'planned_hours': delegate_data['planned_hours'],
634 'remaining_hours': delegate_data['planned_hours'],
635 'parent_ids': [(6, 0, [task.id])],
637 'description': delegate_data['new_task_description'] or '',
641 newname = delegate_data['prefix'] or ''
642 self.write(cr, uid, [task.id], {
643 'remaining_hours': delegate_data['planned_hours_me'],
644 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
647 if delegate_data['state'] == 'pending':
648 self.do_pending(cr, uid, [task.id], context)
650 self.do_close(cr, uid, [task.id], context=context)
651 user_pool = self.pool.get('res.users')
652 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
653 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
654 self.log(cr, uid, task.id, message)
657 def do_pending(self, cr, uid, ids, *args):
658 self.write(cr, uid, ids, {'state': 'pending'})
659 for (id, name) in self.name_get(cr, uid, ids):
660 message = _("The task '%s' is pending.") % name
661 self.log(cr, uid, id, message)
664 def next_type(self, cr, uid, ids, *args):
665 for task in self.browse(cr, uid, ids):
666 typeid = task.type_id.id
667 types = map(lambda x:x.id, task.project_id.type_ids or [])
670 self.write(cr, uid, task.id, {'type_id': types[0]})
671 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
672 index = types.index(typeid)
673 self.write(cr, uid, task.id, {'type_id': types[index+1]})
676 def prev_type(self, cr, uid, ids, *args):
677 for task in self.browse(cr, uid, ids):
678 typeid = task.type_id.id
679 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
681 if typeid and typeid in types:
682 index = types.index(typeid)
683 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
688 class project_work(osv.osv):
689 _name = "project.task.work"
690 _description = "Project Task Work"
692 'name': fields.char('Work summary', size=128),
693 'date': fields.datetime('Date'),
694 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
695 'hours': fields.float('Time Spent'),
696 'user_id': fields.many2one('res.users', 'Done by', required=True),
697 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
701 'user_id': lambda obj, cr, uid, context: uid,
702 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
706 def create(self, cr, uid, vals, *args, **kwargs):
707 if 'hours' in vals and (not vals['hours']):
709 if 'task_id' in vals:
710 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
711 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
713 def write(self, cr, uid, ids, vals, context=None):
714 if 'hours' in vals and (not vals['hours']):
717 for work in self.browse(cr, uid, ids, context=context):
718 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
719 return super(project_work,self).write(cr, uid, ids, vals, context)
721 def unlink(self, cr, uid, ids, *args, **kwargs):
722 for work in self.browse(cr, uid, ids):
723 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
724 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
727 class account_analytic_account(osv.osv):
729 _inherit = 'account.analytic.account'
730 _description = 'Analytic Account'
732 def create(self, cr, uid, vals, context=None):
735 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
736 vals['child_ids'] = []
737 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
739 account_analytic_account()
741 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: