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
29 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
30 #class project_project(osv.osv):
31 # _name = 'project.project'
34 class project_task_type(osv.osv):
35 _name = 'project.task.type'
36 _description = 'Task Stage'
39 'name': fields.char('Stage Name', required=True, size=64, translate=True),
40 'description': fields.text('Description'),
41 'sequence': fields.integer('Sequence'),
42 'project_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
43 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
51 class project(osv.osv):
52 _name = "project.project"
53 _description = "Project"
54 _inherits = {'account.analytic.account': "analytic_account_id"}
56 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
58 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
59 if context and context.get('user_preference'):
60 cr.execute("""SELECT project.id FROM project_project project
61 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
62 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
63 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
64 return [(r[0]) for r in cr.fetchall()]
65 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
66 context=context, count=count)
68 def _complete_name(self, cr, uid, ids, name, args, context=None):
70 for m in self.browse(cr, uid, ids, context=context):
71 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
74 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
75 partner_obj = self.pool.get('res.partner')
77 return {'value':{'contact_id': False, 'pricelist_id': False}}
78 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
79 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
80 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
81 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
83 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
84 res = {}.fromkeys(ids, 0.0)
88 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
95 project_id''', (tuple(ids),))
96 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
97 for project in self.browse(cr, uid, ids, context=context):
98 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
100 'planned_hours': s[0],
101 'effective_hours': s[2],
103 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
107 def _get_project_task(self, cr, uid, ids, context=None):
109 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
110 if task.project_id: result[task.project_id.id] = True
113 def _get_project_work(self, cr, uid, ids, context=None):
115 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
116 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
119 def unlink(self, cr, uid, ids, *args, **kwargs):
120 for proj in self.browse(cr, uid, ids):
122 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
123 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
126 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
127 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
128 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
129 '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),
130 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
131 '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)]}),
133 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
134 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)]}),
135 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
136 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned 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 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
142 'total_hours': fields.function(_progress_rate, multi="progress", 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", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
148 '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)]}),
149 '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)]}),
150 '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)]}),
151 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
153 def _get_type_common(self, cr, uid, context):
154 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
162 'type_ids': _get_type_common
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 default = default or {}
218 context['active_test'] = False
219 default['state'] = 'open'
220 proj = self.browse(cr, uid, id, context=context)
221 if not default.get('name', False):
222 default['name'] = proj.name + _(' (copy)')
224 res = super(project, self).copy(cr, uid, id, default, context)
228 def template_copy(self, cr, uid, id, default={}, context=None):
229 task_obj = self.pool.get('project.task')
230 proj = self.browse(cr, uid, id, context=context)
232 default['tasks'] = [] #avoid to copy all the task automaticly
233 res = self.copy(cr, uid, id, default=default, context=context)
235 #copy all the task manually
237 for task in proj.tasks:
238 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
240 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
241 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
245 def duplicate_template(self, cr, uid, ids, context=None):
248 data_obj = self.pool.get('ir.model.data')
250 for proj in self.browse(cr, uid, ids, context=context):
251 parent_id = context.get('parent_id', False)
252 context.update({'analytic_project_copy': True})
253 new_date_start = time.strftime('%Y-%m-%d')
255 if proj.date_start and proj.date:
256 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
257 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
258 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
259 context.update({'copy':True})
260 new_id = self.template_copy(cr, uid, proj.id, default = {
261 'name': proj.name +_(' (copy)'),
263 'date_start':new_date_start,
265 'parent_id':parent_id}, context=context)
266 result.append(new_id)
268 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
269 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
271 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
273 if result and len(result):
275 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
276 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
277 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
278 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
279 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
280 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
282 'name': _('Projects'),
284 'view_mode': 'form,tree',
285 'res_model': 'project.project',
288 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
289 'type': 'ir.actions.act_window',
290 'search_view_id': search_view['res_id'],
294 # set active value for a project, its sub projects and its tasks
295 def setActive(self, cr, uid, ids, value=True, context=None):
296 task_obj = self.pool.get('project.task')
297 for proj in self.browse(cr, uid, ids, context=None):
298 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
299 cr.execute('select id from project_task where project_id=%s', (proj.id,))
300 tasks_id = [x[0] for x in cr.fetchall()]
302 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
303 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
305 self.setActive(cr, uid, child_ids, value, context=None)
310 class users(osv.osv):
311 _inherit = 'res.users'
313 'context_project_id': fields.many2one('project.project', 'Project')
318 _name = "project.task"
319 _description = "Task"
321 _date_name = "date_start"
323 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
324 obj_project = self.pool.get('project.project')
326 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
327 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
328 if id and isinstance(id, (long, int)):
329 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
330 args.append(('active', '=', False))
331 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
333 def _str_get(self, task, level=0, border='***', context=None):
334 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'+ \
335 border[0]+' '+(task.name or '')+'\n'+ \
336 (task.description or '')+'\n\n'
338 # Compute: effective_hours, total_hours, progress
339 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
341 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
342 hours = dict(cr.fetchall())
343 for task in self.browse(cr, uid, ids, context=context):
344 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)}
345 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
346 res[task.id]['progress'] = 0.0
347 if (task.remaining_hours + hours.get(task.id, 0.0)):
348 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
349 if task.state in ('done','cancelled'):
350 res[task.id]['progress'] = 100.0
354 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
355 if remaining and not planned:
356 return {'value':{'planned_hours': remaining}}
359 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
360 return {'value':{'remaining_hours': planned - effective}}
362 def onchange_project(self, cr, uid, id, project_id):
365 data = self.pool.get('project.project').browse(cr, uid, [project_id])
366 partner_id=data and data[0].parent_id.partner_id
368 return {'value':{'partner_id':partner_id.id}}
371 def _default_project(self, cr, uid, context=None):
374 if 'project_id' in context and context['project_id']:
375 return int(context['project_id'])
378 def duplicate_task(self, cr, uid, map_ids, context=None):
379 for new in map_ids.values():
380 task = self.browse(cr, uid, new, context)
381 child_ids = [ ch.id for ch in task.child_ids]
383 for child in task.child_ids:
384 if child.id in map_ids.keys():
385 child_ids.remove(child.id)
386 child_ids.append(map_ids[child.id])
388 parent_ids = [ ch.id for ch in task.parent_ids]
390 for parent in task.parent_ids:
391 if parent.id in map_ids.keys():
392 parent_ids.remove(parent.id)
393 parent_ids.append(map_ids[parent.id])
394 #FIXME why there is already the copy and the old one
395 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
397 def copy_data(self, cr, uid, id, default={}, context=None):
398 default = default or {}
399 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
400 if not default.get('remaining_hours', False):
401 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
402 default['active'] = True
403 default['type_id'] = False
404 if not default.get('name', False):
405 default['name'] = self.browse(cr, uid, id, context=context).name or ''
406 if not context.get('copy',False):
407 new_name = _("%s (copy)")%default.get('name','')
408 default.update({'name':new_name})
409 return super(task, self).copy_data(cr, uid, id, default, context)
412 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
414 for task in self.browse(cr, uid, ids, context=context):
417 if task.project_id.active == False or task.project_id.state == 'template':
421 def _get_task(self, cr, uid, ids, context=None):
423 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
424 if work.task_id: result[work.task_id.id] = True
428 'active': fields.function(_is_template, 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."),
429 'name': fields.char('Task Summary', size=128, required=True),
430 'description': fields.text('Description'),
431 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
432 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
433 'type_id': fields.many2one('project.task.type', 'Stage'),
434 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
435 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.\
436 \n If the task is over, the states is set to \'Done\'.'),
437 'kanban_state': fields.selection([('blocked', 'Blocked'),('normal', 'Normal'),('done', 'Done')], 'Kanban State', readonly=True, required=False),
438 'create_date': fields.datetime('Create Date', readonly=True,select=True),
439 'date_start': fields.datetime('Starting Date',select=True),
440 'date_end': fields.datetime('Ending Date',select=True),
441 'date_deadline': fields.date('Deadline',select=True),
442 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
443 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
444 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
445 'notes': fields.text('Notes'),
446 '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.'),
447 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
449 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
450 'project.task.work': (_get_task, ['hours'], 10),
452 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
453 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
455 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
456 'project.task.work': (_get_task, ['hours'], 10),
458 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
460 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
461 'project.task.work': (_get_task, ['hours'], 10),
463 'delay_hours': fields.function(_hours_get, 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.",
465 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
466 'project.task.work': (_get_task, ['hours'], 10),
468 'user_id': fields.many2one('res.users', 'Assigned to'),
469 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
470 'partner_id': fields.many2one('res.partner', 'Partner'),
471 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
472 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
473 'company_id': fields.many2one('res.company', 'Company'),
474 'id': fields.integer('ID', readonly=True),
475 'color': fields.integer('Color Index'),
476 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
481 'kanban_state': 'normal',
486 'project_id': _default_project,
487 'user_id': lambda obj, cr, uid, context: uid,
488 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
491 _order = "priority, sequence, date_start, name, id"
493 def set_priority(self, cr, uid, ids, priority):
496 return self.write(cr, uid, ids, {'priority' : priority})
498 def set_high_priority(self, cr, uid, ids, *args):
499 """Set task priority to high
501 return self.set_priority(cr, uid, ids, '1')
503 def set_normal_priority(self, cr, uid, ids, *args):
504 """Set task priority to normal
506 return self.set_priority(cr, uid, ids, '3')
508 def _check_recursion(self, cr, uid, ids, context=None):
510 visited_branch = set()
512 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
518 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
519 if id in visited_branch: #Cycle
522 if id in visited_node: #Already tested don't work one more time for nothing
525 visited_branch.add(id)
528 #visit child using DFS
529 task = self.browse(cr, uid, id, context=context)
530 for child in task.child_ids:
531 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
535 visited_branch.remove(id)
538 def _check_dates(self, cr, uid, ids, context=None):
541 obj_task = self.browse(cr, uid, ids[0], context=context)
542 start = obj_task.date_start or False
543 end = obj_task.date_end or False
550 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
551 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
554 # Override view according to the company definition
556 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
557 users_obj = self.pool.get('res.users')
559 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
560 # this should be safe (no context passed to avoid side-effects)
561 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
562 tm = obj_tm and obj_tm.name or 'Hours'
564 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
566 if tm in ['Hours','Hour']:
569 eview = etree.fromstring(res['arch'])
571 def _check_rec(eview):
572 if eview.attrib.get('widget','') == 'float_time':
573 eview.set('widget','float')
580 res['arch'] = etree.tostring(eview)
582 for f in res['fields']:
583 if 'Hours' in res['fields'][f]['string']:
584 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
587 def _check_child_task(self, cr, uid, ids, context=None):
590 tasks = self.browse(cr, uid, ids, context=context)
593 for child in task.child_ids:
594 if child.state in ['draft', 'open', 'pending']:
595 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
598 def action_close(self, cr, uid, ids, context=None):
599 # This action open wizard to send email to partner or project manager after close task.
602 task_id = len(ids) and ids[0] or False
603 self._check_child_task(cr, uid, ids, context=context)
604 if not task_id: return False
605 task = self.browse(cr, uid, task_id, context=context)
606 project = task.project_id
607 res = self.do_close(cr, uid, [task_id], context=context)
608 if project.warn_manager or project.warn_customer:
610 'name': _('Send Email after close task'),
613 'res_model': 'mail.compose.message',
614 'type': 'ir.actions.act_window',
617 'context': {'active_id': task.id,
618 'active_model': 'project.task'}
622 def do_close(self, cr, uid, ids, context={}):
626 request = self.pool.get('res.request')
627 # calling do_close from demo data it returns id in string therefor need to convert in list
628 if not isinstance(ids,list): ids = [ids]
629 task = self.browse(cr, uid, ids, context=context)[0]
631 project = task.project_id
633 # Send request to project manager
634 if project.warn_manager and project.user_id and (project.user_id.id != uid):
635 request.create(cr, uid, {
636 'name': _("Task '%s' closed") % task.name,
639 'act_to': project.user_id.id,
640 'ref_partner_id': task.partner_id.id,
641 'ref_doc1': 'project.task,%d'% (task.id,),
642 'ref_doc2': 'project.project,%d'% (project.id,),
645 for parent_id in task.parent_ids:
646 if parent_id.state in ('pending','draft'):
648 for child in parent_id.child_ids:
649 if child.id != task.id and child.state not in ('done','cancelled'):
652 self.do_reopen(cr, uid, [parent_id.id], context=context)
653 vals.update({'state': 'done'})
654 vals.update({'remaining_hours': 0.0})
655 if not task.date_end:
656 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
657 self.write(cr, uid, [task.id],vals, context=context)
658 message = _("The task '%s' is done") % (task.name,)
659 self.log(cr, uid, task.id, message)
662 def do_reopen(self, cr, uid, ids, context=None):
663 request = self.pool.get('res.request')
665 for task in self.browse(cr, uid, ids, context=context):
666 project = task.project_id
667 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
668 request.create(cr, uid, {
669 'name': _("Task '%s' set in progress") % task.name,
672 'act_to': project.user_id.id,
673 'ref_partner_id': task.partner_id.id,
674 'ref_doc1': 'project.task,%d' % task.id,
675 'ref_doc2': 'project.project,%d' % project.id,
678 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
681 def do_cancel(self, cr, uid, ids, context={}):
682 request = self.pool.get('res.request')
683 tasks = self.browse(cr, uid, ids, context=context)
684 self._check_child_task(cr, uid, ids, context=context)
686 project = task.project_id
687 if project.warn_manager and project.user_id and (project.user_id.id != uid):
688 request.create(cr, uid, {
689 'name': _("Task '%s' cancelled") % task.name,
692 'act_to': project.user_id.id,
693 'ref_partner_id': task.partner_id.id,
694 'ref_doc1': 'project.task,%d' % task.id,
695 'ref_doc2': 'project.project,%d' % project.id,
697 message = _("The task '%s' is cancelled.") % (task.name,)
698 self.log(cr, uid, task.id, message)
699 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
702 def do_open(self, cr, uid, ids, context={}):
703 # calling do_open from demo data it returns id in string therefor need to convert in list
704 if not isinstance(ids,list): ids = [ids]
705 task = self.browse(cr, uid, ids, context=context)[0]
706 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
707 message = _("The task '%s' is opened.") % (task.name)
708 self.log(cr, uid, task.id, message)
711 def do_draft(self, cr, uid, ids, context={}):
712 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
715 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
717 Delegate Task to another users.
719 task = self.browse(cr, uid, task_id, context=context)
720 self.copy(cr, uid, task.id, {
721 'name': delegate_data['name'],
722 'user_id': delegate_data['user_id'],
723 'planned_hours': delegate_data['planned_hours'],
724 'remaining_hours': delegate_data['planned_hours'],
725 'parent_ids': [(6, 0, [task.id])],
727 'description': delegate_data['new_task_description'] or '',
731 newname = delegate_data['prefix'] or ''
732 self.write(cr, uid, [task.id], {
733 'remaining_hours': delegate_data['planned_hours_me'],
734 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
737 if delegate_data['state'] == 'pending':
738 self.do_pending(cr, uid, [task.id], context)
740 self.do_close(cr, uid, [task.id], context=context)
741 user_pool = self.pool.get('res.users')
742 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
743 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
744 self.log(cr, uid, task.id, message)
747 def do_pending(self, cr, uid, ids, context={}):
748 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
749 for (id, name) in self.name_get(cr, uid, ids):
750 message = _("The task '%s' is pending.") % name
751 self.log(cr, uid, id, message)
754 def set_remaining_time_1(self, cr, uid, ids, context=None):
755 self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
758 def set_remaining_time_2(self, cr, uid, ids, context=None):
759 self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
762 def set_remaining_time_5(self, cr, uid, ids, context=None):
763 self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
766 def set_remaining_time_10(self, cr, uid, ids, context=None):
767 self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
770 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
771 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
773 def set_kanban_state_normal(self, cr, uid, ids, context=None):
774 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
776 def set_kanban_state_done(self, cr, uid, ids, context=None):
777 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
779 def _change_type(self, cr, uid, ids, next, *args):
782 if next is False, go to previous stage
784 for task in self.browse(cr, uid, ids):
785 if task.project_id.type_ids:
786 typeid = task.type_id.id
788 for type in task.project_id.type_ids :
789 types_seq[type.id] = type.sequence
791 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
793 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
794 sorted_types = [x[0] for x in types]
796 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
797 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
798 index = sorted_types.index(typeid)
799 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
802 def next_type(self, cr, uid, ids, *args):
803 return self._change_type(cr, uid, ids, True, *args)
805 def prev_type(self, cr, uid, ids, *args):
806 return self._change_type(cr, uid, ids, False, *args)
808 def unlink(self, cr, uid, ids, context=None):
811 self._check_child_task(cr, uid, ids, context=context)
812 res = super(task, self).unlink(cr, uid, ids, context)
817 class project_work(osv.osv):
818 _name = "project.task.work"
819 _description = "Project Task Work"
821 'name': fields.char('Work summary', size=128),
822 'date': fields.datetime('Date', select="1"),
823 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
824 'hours': fields.float('Time Spent'),
825 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
826 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
830 'user_id': lambda obj, cr, uid, context: uid,
831 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
835 def create(self, cr, uid, vals, *args, **kwargs):
836 if 'hours' in vals and (not vals['hours']):
838 if 'task_id' in vals:
839 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
840 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
842 def write(self, cr, uid, ids, vals, context=None):
843 if 'hours' in vals and (not vals['hours']):
846 for work in self.browse(cr, uid, ids, context=context):
847 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))
848 return super(project_work,self).write(cr, uid, ids, vals, context)
850 def unlink(self, cr, uid, ids, *args, **kwargs):
851 for work in self.browse(cr, uid, ids):
852 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
853 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
856 class account_analytic_account(osv.osv):
858 _inherit = 'account.analytic.account'
859 _description = 'Analytic Account'
861 def create(self, cr, uid, vals, context=None):
864 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
865 vals['child_ids'] = []
866 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
868 def unlink(self, cr, uid, ids, *args, **kwargs):
869 project_obj = self.pool.get('project.project')
870 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
872 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
873 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
875 account_analytic_account()
877 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: