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',
130 help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
131 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
132 '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.",
134 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
135 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
137 '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 '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.",
140 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
141 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
143 '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."),
144 '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)]}),
145 '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)]}),
146 '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)]}),
147 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
156 # TODO: Why not using a SQL contraints ?
157 def _check_dates(self, cr, uid, ids, context=None):
158 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
159 if leave['date_start'] and leave['date']:
160 if leave['date_start'] > leave['date']:
165 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
168 def set_template(self, cr, uid, ids, context=None):
169 res = self.setActive(cr, uid, ids, value=False, context=context)
172 def set_done(self, cr, uid, ids, context=None):
173 task_obj = self.pool.get('project.task')
174 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
175 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
176 self.write(cr, uid, ids, {'state':'close'}, context=context)
177 for (id, name) in self.name_get(cr, uid, ids):
178 message = _("The project '%s' has been closed.") % name
179 self.log(cr, uid, id, message)
182 def set_cancel(self, cr, uid, ids, context=None):
183 task_obj = self.pool.get('project.task')
184 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
185 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
189 def set_pending(self, cr, uid, ids, context=None):
190 self.write(cr, uid, ids, {'state':'pending'}, context=context)
193 def set_open(self, cr, uid, ids, context=None):
194 self.write(cr, uid, ids, {'state':'open'}, context=context)
197 def reset_project(self, cr, uid, ids, context=None):
198 res = self.setActive(cr, uid, ids, value=True, context=context)
199 for (id, name) in self.name_get(cr, uid, ids):
200 message = _("The project '%s' has been opened.") % name
201 self.log(cr, uid, id, message)
204 def copy(self, cr, uid, id, default={}, context=None):
208 proj = self.browse(cr, uid, id, context=context)
209 default = default or {}
210 context['active_test'] = False
211 default['state'] = 'open'
212 if not default.get('name', False):
213 default['name'] = proj.name + _(' (copy)')
214 res = super(project, self).copy(cr, uid, id, default, context)
218 def duplicate_template(self, cr, uid, ids, context=None):
221 project_obj = self.pool.get('project.project')
222 data_obj = self.pool.get('ir.model.data')
224 for proj in self.browse(cr, uid, ids, context=context):
225 parent_id = context.get('parent_id', False)
226 context.update({'analytic_project_copy': True})
227 new_date_start = time.strftime('%Y-%m-%d')
229 if proj.date_start and proj.date:
230 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
231 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
232 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
233 context.update({'copy':True})
234 new_id = project_obj.copy(cr, uid, proj.id, default = {
235 'name': proj.name +_(' (copy)'),
237 'date_start':new_date_start,
239 'parent_id':parent_id}, context=context)
240 result.append(new_id)
242 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
243 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
245 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
247 if result and len(result):
249 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
250 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
251 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
252 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
253 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
254 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
256 'name': _('Projects'),
258 'view_mode': 'form,tree',
259 'res_model': 'project.project',
262 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
263 'type': 'ir.actions.act_window',
264 'search_view_id': search_view['res_id'],
268 # set active value for a project, its sub projects and its tasks
269 def setActive(self, cr, uid, ids, value=True, context=None):
270 task_obj = self.pool.get('project.task')
271 for proj in self.browse(cr, uid, ids, context=None):
272 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
273 cr.execute('select id from project_task where project_id=%s', (proj.id,))
274 tasks_id = [x[0] for x in cr.fetchall()]
276 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
277 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
279 self.setActive(cr, uid, child_ids, value, context=None)
284 class users(osv.osv):
285 _inherit = 'res.users'
287 'context_project_id': fields.many2one('project.project', 'Project')
292 _name = "project.task"
293 _description = "Task"
295 _date_name = "date_start"
297 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
298 obj_project = self.pool.get('project.project')
300 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
301 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
302 if id and isinstance(id, (long, int)):
303 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
304 args.append(('active', '=', False))
305 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
307 def _str_get(self, task, level=0, border='***', context=None):
308 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'+ \
309 border[0]+' '+(task.name or '')+'\n'+ \
310 (task.description or '')+'\n\n'
312 # Compute: effective_hours, total_hours, progress
313 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
315 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
316 hours = dict(cr.fetchall())
317 for task in self.browse(cr, uid, ids, context=context):
318 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)}
319 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
320 res[task.id]['progress'] = 0.0
321 if (task.remaining_hours + hours.get(task.id, 0.0)):
322 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
323 if task.state in ('done','cancelled'):
324 res[task.id]['progress'] = 100.0
328 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
329 if remaining and not planned:
330 return {'value':{'planned_hours': remaining}}
333 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
334 return {'value':{'remaining_hours': planned - effective}}
336 def onchange_project(self, cr, uid, id, project_id):
339 data = self.pool.get('project.project').browse(cr, uid, [project_id])
340 partner_id=data and data[0].parent_id.partner_id
342 return {'value':{'partner_id':partner_id.id}}
345 def _default_project(self, cr, uid, context=None):
348 if 'project_id' in context and context['project_id']:
349 return int(context['project_id'])
352 def copy_data(self, cr, uid, id, default={}, context=None):
353 default = default or {}
354 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
355 if not default.get('remaining_hours', False):
356 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
357 default['active'] = True
358 default['type_id'] = False
359 if not default.get('name', False):
360 default['name'] = self.browse(cr, uid, id, context=context).name or ''
361 if not context.get('copy',False):
362 new_name = _("%s (copy)")%default.get('name','')
363 default.update({'name':new_name})
364 return super(task, self).copy_data(cr, uid, id, default, context)
366 def _check_dates(self, cr, uid, ids, context=None):
367 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
368 if task['date_start'] and task['date_end']:
369 if task['date_start'] > task['date_end']:
373 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
375 for task in self.browse(cr, uid, ids, context=context):
378 if task.project_id.active == False or task.project_id.state == 'template':
382 def _get_task(self, cr, uid, ids, context=None):
384 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
385 if work.task_id: result[work.task_id.id] = True
389 '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."),
390 'name': fields.char('Task Summary', size=128, required=True),
391 'description': fields.text('Description'),
392 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
393 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
394 'type_id': fields.many2one('project.task.type', 'Stage'),
395 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
396 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.\
397 \n If the task is over, the states is set to \'Done\'.'),
398 'create_date': fields.datetime('Create Date', readonly=True,select=True),
399 'date_start': fields.datetime('Starting Date',select=True),
400 'date_end': fields.datetime('Ending Date',select=True),
401 'date_deadline': fields.date('Deadline',select=True),
402 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
403 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
404 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
405 'notes': fields.text('Notes'),
406 '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.'),
407 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
409 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
410 'project.task.work': (_get_task, ['hours'], 10),
412 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
413 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
415 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
416 'project.task.work': (_get_task, ['hours'], 10),
418 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
420 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
421 'project.task.work': (_get_task, ['hours'], 10),
423 '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.",
425 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
426 'project.task.work': (_get_task, ['hours'], 10),
428 'user_id': fields.many2one('res.users', 'Assigned to'),
429 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
430 'partner_id': fields.many2one('res.partner', 'Partner'),
431 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
432 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
433 'company_id': fields.many2one('res.company', 'Company'),
434 'id': fields.integer('ID'),
443 'project_id': _default_project,
444 'user_id': lambda obj, cr, uid, context: uid,
445 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
448 _order = "sequence,priority, date_start, name, id"
450 def _check_recursion(self, cr, uid, ids, context=None):
451 obj_task = self.browse(cr, uid, ids[0], context=context)
452 parent_ids = [x.id for x in obj_task.parent_ids]
453 children_ids = [x.id for x in obj_task.child_ids]
455 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
459 cr.execute('SELECT DISTINCT task_id '\
460 'FROM project_task_parent_rel '\
461 'WHERE parent_id IN %s', (tuple(ids),))
462 child_ids = map(lambda x: x[0], cr.fetchall())
464 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
467 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
468 if (list(set(parent_ids).intersection(set(s_ids)))):
474 def _check_dates(self, cr, uid, ids, context=None):
477 obj_task = self.browse(cr, uid, ids[0], context=context)
478 start = obj_task.date_start or False
479 end = obj_task.date_end or False
486 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
487 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
490 # Override view according to the company definition
494 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
495 users_obj = self.pool.get('res.users')
497 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
498 # this should be safe (no context passed to avoid side-effects)
499 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
500 tm = obj_tm and obj_tm.name or 'Hours'
502 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
504 if tm in ['Hours','Hour']:
507 eview = etree.fromstring(res['arch'])
509 def _check_rec(eview):
510 if eview.attrib.get('widget','') == 'float_time':
511 eview.set('widget','float')
518 res['arch'] = etree.tostring(eview)
520 for f in res['fields']:
521 if 'Hours' in res['fields'][f]['string']:
522 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
525 def action_close(self, cr, uid, ids, context=None):
526 # This action open wizard to send email to partner or project manager after close task.
527 project_id = len(ids) and ids[0] or False
528 if not project_id: return False
529 task = self.browse(cr, uid, project_id, context=context)
530 project = task.project_id
531 res = self.do_close(cr, uid, [project_id], context=context)
532 if project.warn_manager or project.warn_customer:
534 'name': _('Send Email after close task'),
537 'res_model': 'project.task.close',
538 'type': 'ir.actions.act_window',
541 'context': {'active_id': task.id}
545 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):
552 project = task.project_id
554 # Send request to project manager
555 if project.warn_manager and project.user_id and (project.user_id.id != uid):
556 request.create(cr, uid, {
557 'name': _("Task '%s' closed") % task.name,
560 'act_to': project.user_id.id,
561 'ref_partner_id': task.partner_id.id,
562 'ref_doc1': 'project.task,%d'% (task.id,),
563 'ref_doc2': 'project.project,%d'% (project.id,),
566 for parent_id in task.parent_ids:
567 if parent_id.state in ('pending','draft'):
569 for child in parent_id.child_ids:
570 if child.id != task.id and child.state not in ('done','cancelled'):
573 self.do_reopen(cr, uid, [parent_id.id])
574 vals.update({'state': 'done'})
575 vals.update({'remaining_hours': 0.0})
576 if not task.date_end:
577 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
578 self.write(cr, uid, [task.id],vals)
579 message = _("The task '%s' is done") % (task.name,)
580 self.log(cr, uid, task.id, message)
583 def do_reopen(self, cr, uid, ids, context=None):
584 request = self.pool.get('res.request')
586 for task in self.browse(cr, uid, ids, context=context):
587 project = task.project_id
588 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
589 request.create(cr, uid, {
590 'name': _("Task '%s' set in progress") % task.name,
593 'act_to': project.user_id.id,
594 'ref_partner_id': task.partner_id.id,
595 'ref_doc1': 'project.task,%d' % task.id,
596 'ref_doc2': 'project.project,%d' % project.id,
599 self.write(cr, uid, [task.id], {'state': 'open'})
603 def do_cancel(self, cr, uid, ids, *args):
604 request = self.pool.get('res.request')
605 tasks = self.browse(cr, uid, ids)
607 project = task.project_id
608 if project.warn_manager and project.user_id and (project.user_id.id != uid):
609 request.create(cr, uid, {
610 'name': _("Task '%s' cancelled") % task.name,
613 'act_to': project.user_id.id,
614 'ref_partner_id': task.partner_id.id,
615 'ref_doc1': 'project.task,%d' % task.id,
616 'ref_doc2': 'project.project,%d' % project.id,
618 message = _("The task '%s' is cancelled.") % (task.name,)
619 self.log(cr, uid, task.id, message)
620 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
623 def do_open(self, cr, uid, ids, *args):
624 tasks= self.browse(cr,uid,ids)
626 data = {'state': 'open'}
628 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
629 self.write(cr, uid, [t.id], data)
630 message = _("The task '%s' is opened.") % (t.name,)
631 self.log(cr, uid, t.id, message)
634 def do_draft(self, cr, uid, ids, *args):
635 self.write(cr, uid, ids, {'state': 'draft'})
638 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
640 Delegate Task to another users.
642 task = self.browse(cr, uid, task_id, context=context)
643 self.copy(cr, uid, task.id, {
644 'name': delegate_data['name'],
645 'user_id': delegate_data['user_id'],
646 'planned_hours': delegate_data['planned_hours'],
647 'remaining_hours': delegate_data['planned_hours'],
648 'parent_ids': [(6, 0, [task.id])],
650 'description': delegate_data['new_task_description'] or '',
654 newname = delegate_data['prefix'] or ''
655 self.write(cr, uid, [task.id], {
656 'remaining_hours': delegate_data['planned_hours_me'],
657 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
660 if delegate_data['state'] == 'pending':
661 self.do_pending(cr, uid, [task.id], context)
663 self.do_close(cr, uid, [task.id], context=context)
664 user_pool = self.pool.get('res.users')
665 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
666 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
667 self.log(cr, uid, task.id, message)
670 def do_pending(self, cr, uid, ids, *args):
671 self.write(cr, uid, ids, {'state': 'pending'})
672 for (id, name) in self.name_get(cr, uid, ids):
673 message = _("The task '%s' is pending.") % name
674 self.log(cr, uid, id, message)
677 def next_type(self, cr, uid, ids, *args):
678 for task in self.browse(cr, uid, ids):
679 typeid = task.type_id.id
680 types = map(lambda x:x.id, task.project_id.type_ids or [])
683 self.write(cr, uid, task.id, {'type_id': types[0]})
684 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
685 index = types.index(typeid)
686 self.write(cr, uid, task.id, {'type_id': types[index+1]})
689 def prev_type(self, cr, uid, ids, *args):
690 for task in self.browse(cr, uid, ids):
691 typeid = task.type_id.id
692 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
694 if typeid and typeid in types:
695 index = types.index(typeid)
696 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
701 class project_work(osv.osv):
702 _name = "project.task.work"
703 _description = "Project Task Work"
705 'name': fields.char('Work summary', size=128),
706 'date': fields.datetime('Date'),
707 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
708 'hours': fields.float('Time Spent'),
709 'user_id': fields.many2one('res.users', 'Done by', required=True),
710 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
714 'user_id': lambda obj, cr, uid, context: uid,
715 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
719 def create(self, cr, uid, vals, *args, **kwargs):
720 if 'hours' in vals and (not vals['hours']):
722 if 'task_id' in vals:
723 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
724 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
726 def write(self, cr, uid, ids, vals, context=None):
727 if 'hours' in vals and (not vals['hours']):
730 for work in self.browse(cr, uid, ids, context=context):
731 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))
732 return super(project_work,self).write(cr, uid, ids, vals, context)
734 def unlink(self, cr, uid, ids, *args, **kwargs):
735 for work in self.browse(cr, uid, ids):
736 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
737 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
740 class account_analytic_account(osv.osv):
742 _inherit = 'account.analytic.account'
743 _description = 'Analytic Account'
745 def create(self, cr, uid, vals, context=None):
748 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
749 vals['child_ids'] = []
750 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
752 account_analytic_account()
754 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: