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.",
139 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
140 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
142 '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.",
144 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
145 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
147 '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.",
149 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
150 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
151 'project.task.work': (_get_project_work, ['hours'], 10),
153 '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)]}),
154 '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)]}),
155 '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)]}),
156 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
165 # TODO: Why not using a SQL contraints ?
166 def _check_dates(self, cr, uid, ids, context=None):
167 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
168 if leave['date_start'] and leave['date']:
169 if leave['date_start'] > leave['date']:
174 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
177 def set_template(self, cr, uid, ids, context=None):
178 res = self.setActive(cr, uid, ids, value=False, context=context)
181 def set_done(self, cr, uid, ids, context=None):
182 task_obj = self.pool.get('project.task')
183 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
184 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
185 self.write(cr, uid, ids, {'state':'close'}, context=context)
186 for (id, name) in self.name_get(cr, uid, ids):
187 message = _("The project '%s' has been closed.") % name
188 self.log(cr, uid, id, message)
191 def set_cancel(self, cr, uid, ids, context=None):
192 task_obj = self.pool.get('project.task')
193 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
194 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
195 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
198 def set_pending(self, cr, uid, ids, context=None):
199 self.write(cr, uid, ids, {'state':'pending'}, context=context)
202 def set_open(self, cr, uid, ids, context=None):
203 self.write(cr, uid, ids, {'state':'open'}, context=context)
206 def reset_project(self, cr, uid, ids, context=None):
207 res = self.setActive(cr, uid, ids, value=True, context=context)
208 for (id, name) in self.name_get(cr, uid, ids):
209 message = _("The project '%s' has been opened.") % name
210 self.log(cr, uid, id, message)
213 def copy(self, cr, uid, id, default={}, context=None):
217 proj = self.browse(cr, uid, id, context=context)
218 default = default or {}
219 context['active_test'] = False
220 default['state'] = 'open'
221 if not default.get('name', False):
222 default['name'] = proj.name + _(' (copy)')
223 res = super(project, self).copy(cr, uid, id, default, context)
227 def duplicate_template(self, cr, uid, ids, context=None):
230 project_obj = self.pool.get('project.project')
231 data_obj = self.pool.get('ir.model.data')
233 for proj in self.browse(cr, uid, ids, context=context):
234 parent_id = context.get('parent_id', False)
235 context.update({'analytic_project_copy': True})
236 new_date_start = time.strftime('%Y-%m-%d')
238 if proj.date_start and proj.date:
239 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
240 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
241 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
242 new_id = project_obj.copy(cr, uid, proj.id, default = {
243 'name': proj.name +_(' (copy)'),
245 'date_start':new_date_start,
247 'parent_id':parent_id}, context=context)
248 result.append(new_id)
250 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
251 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
253 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
255 if result and len(result):
257 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
258 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
259 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
260 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
261 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
262 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
264 'name': _('Projects'),
266 'view_mode': 'form,tree',
267 'res_model': 'project.project',
270 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
271 'type': 'ir.actions.act_window',
272 'search_view_id': search_view['res_id'],
276 # set active value for a project, its sub projects and its tasks
277 def setActive(self, cr, uid, ids, value=True, context=None):
278 task_obj = self.pool.get('project.task')
279 for proj in self.browse(cr, uid, ids, context=None):
280 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
281 cr.execute('select id from project_task where project_id=%s', (proj.id,))
282 tasks_id = [x[0] for x in cr.fetchall()]
284 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
285 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
287 self.setActive(cr, uid, child_ids, value, context=None)
292 class users(osv.osv):
293 _inherit = 'res.users'
295 'context_project_id': fields.many2one('project.project', 'Project')
300 _name = "project.task"
301 _description = "Task"
303 _date_name = "date_start"
305 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
306 obj_project = self.pool.get('project.project')
308 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
309 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
310 if id and isinstance(id, (long, int)):
311 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
312 args.append(('active', '=', False))
313 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
315 def _str_get(self, task, level=0, border='***', context=None):
316 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'+ \
317 border[0]+' '+(task.name or '')+'\n'+ \
318 (task.description or '')+'\n\n'
320 # Compute: effective_hours, total_hours, progress
321 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
323 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
324 hours = dict(cr.fetchall())
325 for task in self.browse(cr, uid, ids, context=context):
326 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)}
327 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
328 res[task.id]['progress'] = 0.0
329 if (task.remaining_hours + hours.get(task.id, 0.0)):
330 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
331 if task.state in ('done','cancelled'):
332 res[task.id]['progress'] = 100.0
336 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
337 if remaining and not planned:
338 return {'value':{'planned_hours': remaining}}
341 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
342 return {'value':{'remaining_hours': planned - effective}}
344 def onchange_project(self, cr, uid, id, project_id):
347 data = self.pool.get('project.project').browse(cr, uid, [project_id])
348 partner_id=data and data[0].parent_id.partner_id
350 return {'value':{'partner_id':partner_id.id}}
353 def _default_project(self, cr, uid, context=None):
356 if 'project_id' in context and context['project_id']:
357 return int(context['project_id'])
360 def copy_data(self, cr, uid, id, default={}, context=None):
361 default = default or {}
362 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
363 if not default.get('remaining_hours', False):
364 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
365 default['active'] = True
366 default['type_id'] = False
367 if not default.get('name', False):
368 default['name'] = self.browse(cr, uid, id, context=context).name
369 return super(task, self).copy_data(cr, uid, id, default, context)
371 def _check_dates(self, cr, uid, ids, context=None):
372 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
373 if task['date_start'] and task['date_end']:
374 if task['date_start'] > task['date_end']:
378 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
380 for task in self.browse(cr, uid, ids, context=context):
383 if task.project_id.active == False or task.project_id.state == 'template':
387 def _get_task(self, cr, uid, ids, context=None):
389 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
390 if work.task_id: result[work.task_id.id] = True
394 '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."),
395 'name': fields.char('Task Summary', size=128, required=True),
396 'description': fields.text('Description'),
397 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
398 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
399 'type_id': fields.many2one('project.task.type', 'Stage'),
400 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
401 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.\
402 \n If the task is over, the states is set to \'Done\'.'),
403 'create_date': fields.datetime('Create Date', readonly=True,select=True),
404 'date_start': fields.datetime('Starting Date',select=True),
405 'date_end': fields.datetime('Ending Date',select=True),
406 'date_deadline': fields.date('Deadline',select=True),
407 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
408 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
409 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
410 'notes': fields.text('Notes'),
411 '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.'),
412 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
414 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
415 'project.task.work': (_get_task, ['hours'], 10),
417 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
418 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
420 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
421 'project.task.work': (_get_task, ['hours'], 10),
423 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
425 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
426 'project.task.work': (_get_task, ['hours'], 10),
428 '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.",
430 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
431 'project.task.work': (_get_task, ['hours'], 10),
433 'user_id': fields.many2one('res.users', 'Assigned to'),
434 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
435 'partner_id': fields.many2one('res.partner', 'Partner'),
436 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
437 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
438 'company_id': fields.many2one('res.company', 'Company'),
439 'id': fields.integer('ID'),
448 'project_id': _default_project,
449 'user_id': lambda obj, cr, uid, context: uid,
450 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
453 _order = "sequence,priority, date_start, name, id"
455 def _check_recursion(self, cr, uid, ids, context=None):
456 obj_task = self.browse(cr, uid, ids[0], context=context)
457 parent_ids = [x.id for x in obj_task.parent_ids]
458 children_ids = [x.id for x in obj_task.child_ids]
460 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
464 cr.execute('SELECT DISTINCT task_id '\
465 'FROM project_task_parent_rel '\
466 'WHERE parent_id IN %s', (tuple(ids),))
467 child_ids = map(lambda x: x[0], cr.fetchall())
469 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
472 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
473 if (list(set(parent_ids).intersection(set(s_ids)))):
479 def _check_dates(self, cr, uid, ids, context=None):
482 obj_task = self.browse(cr, uid, ids[0], context=context)
483 start = obj_task.date_start or False
484 end = obj_task.date_end or False
491 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
492 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
495 # Override view according to the company definition
499 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
500 users_obj = self.pool.get('res.users')
502 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
503 # this should be safe (no context passed to avoid side-effects)
504 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
505 tm = obj_tm and obj_tm.name or _('Hours')
507 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
509 if tm in [_('Hours'),_('Hour')]:
512 eview = etree.fromstring(res['arch'])
514 def _check_rec(eview):
515 if eview.attrib.get('widget','') == 'float_time':
516 eview.set('widget','float')
523 res['arch'] = etree.tostring(eview)
525 for f in res['fields']:
526 if _('Hours') in res['fields'][f]['string']:
527 res['fields'][f]['string'] = res['fields'][f]['string'].replace(_('Hours'), tm)
530 def action_close(self, cr, uid, ids, context=None):
531 # This action open wizard to send email to partner or project manager after close task.
532 project_id = len(ids) and ids[0] or False
533 if not project_id: return False
534 task = self.browse(cr, uid, project_id, context=context)
535 project = task.project_id
536 res = self.do_close(cr, uid, [project_id], context=context)
537 if project.warn_manager or project.warn_customer:
539 'name': _('Send Email after close task'),
542 'res_model': 'project.task.close',
543 'type': 'ir.actions.act_window',
546 'context': {'active_id': task.id}
550 def do_close(self, cr, uid, ids, context=None):
554 request = self.pool.get('res.request')
555 for task in self.browse(cr, uid, ids, context=context):
557 project = task.project_id
559 # Send request to project manager
560 if project.warn_manager and project.user_id and (project.user_id.id != uid):
561 request.create(cr, uid, {
562 'name': _("Task '%s' closed") % task.name,
565 'act_to': project.user_id.id,
566 'ref_partner_id': task.partner_id.id,
567 'ref_doc1': 'project.task,%d'% (task.id,),
568 'ref_doc2': 'project.project,%d'% (project.id,),
571 for parent_id in task.parent_ids:
572 if parent_id.state in ('pending','draft'):
574 for child in parent_id.child_ids:
575 if child.id != task.id and child.state not in ('done','cancelled'):
578 self.do_reopen(cr, uid, [parent_id.id])
579 vals.update({'state': 'done'})
580 vals.update({'remaining_hours': 0.0})
581 if not task.date_end:
582 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
583 self.write(cr, uid, [task.id],vals)
584 message = _("The task '%s' is done") % (task.name,)
585 self.log(cr, uid, task.id, message)
588 def do_reopen(self, cr, uid, ids, context=None):
589 request = self.pool.get('res.request')
591 for task in self.browse(cr, uid, ids, context=context):
592 project = task.project_id
593 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
594 request.create(cr, uid, {
595 'name': _("Task '%s' set in progress") % task.name,
598 'act_to': project.user_id.id,
599 'ref_partner_id': task.partner_id.id,
600 'ref_doc1': 'project.task,%d' % task.id,
601 'ref_doc2': 'project.project,%d' % project.id,
604 self.write(cr, uid, [task.id], {'state': 'open'})
608 def do_cancel(self, cr, uid, ids, *args):
609 request = self.pool.get('res.request')
610 tasks = self.browse(cr, uid, ids)
612 project = task.project_id
613 if project.warn_manager and project.user_id and (project.user_id.id != uid):
614 request.create(cr, uid, {
615 'name': _("Task '%s' cancelled") % task.name,
618 'act_to': project.user_id.id,
619 'ref_partner_id': task.partner_id.id,
620 'ref_doc1': 'project.task,%d' % task.id,
621 'ref_doc2': 'project.project,%d' % project.id,
623 message = _("The task '%s' is cancelled.") % (task.name,)
624 self.log(cr, uid, task.id, message)
625 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
628 def do_open(self, cr, uid, ids, *args):
629 tasks= self.browse(cr,uid,ids)
631 data = {'state': 'open'}
633 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
634 self.write(cr, uid, [t.id], data)
635 message = _("The task '%s' is opened.") % (t.name,)
636 self.log(cr, uid, t.id, message)
639 def do_draft(self, cr, uid, ids, *args):
640 self.write(cr, uid, ids, {'state': 'draft'})
643 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
645 Delegate Task to another users.
647 task = self.browse(cr, uid, task_id, context=context)
648 self.copy(cr, uid, task.id, {
649 'name': delegate_data['name'],
650 'user_id': delegate_data['user_id'],
651 'planned_hours': delegate_data['planned_hours'],
652 'remaining_hours': delegate_data['planned_hours'],
653 'parent_ids': [(6, 0, [task.id])],
655 'description': delegate_data['new_task_description'] or '',
659 newname = delegate_data['prefix'] or ''
660 self.write(cr, uid, [task.id], {
661 'remaining_hours': delegate_data['planned_hours_me'],
662 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
665 if delegate_data['state'] == 'pending':
666 self.do_pending(cr, uid, [task.id], context)
668 self.do_close(cr, uid, [task.id], context=context)
669 user_pool = self.pool.get('res.users')
670 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
671 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
672 self.log(cr, uid, task.id, message)
675 def do_pending(self, cr, uid, ids, *args):
676 self.write(cr, uid, ids, {'state': 'pending'})
677 for (id, name) in self.name_get(cr, uid, ids):
678 message = _("The task '%s' is pending.") % name
679 self.log(cr, uid, id, message)
682 def next_type(self, cr, uid, ids, *args):
683 for task in self.browse(cr, uid, ids):
684 typeid = task.type_id.id
685 types = map(lambda x:x.id, task.project_id.type_ids or [])
688 self.write(cr, uid, task.id, {'type_id': types[0]})
689 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
690 index = types.index(typeid)
691 self.write(cr, uid, task.id, {'type_id': types[index+1]})
694 def prev_type(self, cr, uid, ids, *args):
695 for task in self.browse(cr, uid, ids):
696 typeid = task.type_id.id
697 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
699 if typeid and typeid in types:
700 index = types.index(typeid)
701 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
706 class project_work(osv.osv):
707 _name = "project.task.work"
708 _description = "Project Task Work"
710 'name': fields.char('Work summary', size=128),
711 'date': fields.datetime('Date'),
712 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
713 'hours': fields.float('Time Spent'),
714 'user_id': fields.many2one('res.users', 'Done by', required=True),
715 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
719 'user_id': lambda obj, cr, uid, context: uid,
720 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
724 def create(self, cr, uid, vals, *args, **kwargs):
725 if 'hours' in vals and (not vals['hours']):
727 if 'task_id' in vals:
728 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
729 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
731 def write(self, cr, uid, ids, vals, context=None):
732 if 'hours' in vals and (not vals['hours']):
735 for work in self.browse(cr, uid, ids, context=context):
736 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))
737 return super(project_work,self).write(cr, uid, ids, vals, context)
739 def unlink(self, cr, uid, ids, *args, **kwargs):
740 for work in self.browse(cr, uid, ids):
741 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
742 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
745 class account_analytic_account(osv.osv):
747 _inherit = 'account.analytic.account'
748 _description = 'Analytic Account'
750 def create(self, cr, uid, vals, context=None):
753 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
754 vals['child_ids'] = []
755 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
757 account_analytic_account()
759 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: