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 class project_project(osv.osv):
30 _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_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.has_key('user_prefence') and context['user_prefence']:
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 can not delete a project with tasks. I suggest you to deactivate it.'))
123 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
126 'complete_name': fields.function(_complete_name, method=True, 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", method=True, 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", method=True, 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", 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."),
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)]}),
160 # TODO: Why not using a SQL contraints ?
161 def _check_dates(self, cr, uid, ids, context=None):
162 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
163 if leave['date_start'] and leave['date']:
164 if leave['date_start'] > leave['date']:
169 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
172 def set_template(self, cr, uid, ids, context=None):
173 res = self.setActive(cr, uid, ids, value=False, context=context)
176 def set_done(self, cr, uid, ids, context=None):
177 task_obj = self.pool.get('project.task')
178 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
179 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
180 self.write(cr, uid, ids, {'state':'close'}, context=context)
181 for (id, name) in self.name_get(cr, uid, ids):
182 message = _("The project '%s' has been closed.") % name
183 self.log(cr, uid, id, message)
186 def set_cancel(self, cr, uid, ids, context=None):
187 task_obj = self.pool.get('project.task')
188 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
189 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
193 def set_pending(self, cr, uid, ids, context=None):
194 self.write(cr, uid, ids, {'state':'pending'}, context=context)
197 def set_open(self, cr, uid, ids, context=None):
198 self.write(cr, uid, ids, {'state':'open'}, context=context)
201 def reset_project(self, cr, uid, ids, context=None):
202 res = self.setActive(cr, uid, ids, value=True, context=context)
203 for (id, name) in self.name_get(cr, uid, ids):
204 message = _("The project '%s' has been opened.") % name
205 self.log(cr, uid, id, message)
208 def copy(self, cr, uid, id, default={}, context=None):
212 default = default or {}
213 context['active_test'] = False
214 default['state'] = 'open'
215 proj = self.browse(cr, uid, id, context=context)
216 if not default.get('name', False):
217 default['name'] = proj.name + _(' (copy)')
219 res = super(project, self).copy(cr, uid, id, default, context)
223 def template_copy(self, cr, uid, id, default={}, context=None):
224 task_obj = self.pool.get('project.task')
225 proj = self.browse(cr, uid, id, context=context)
227 default['tasks'] = [] #avoid to copy all the task automaticly
228 res = self.copy(cr, uid, id, default=default, context=context)
230 #copy all the task manually
232 for task in proj.tasks:
233 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
235 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
236 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
240 def duplicate_template(self, cr, uid, ids, context=None):
243 data_obj = self.pool.get('ir.model.data')
245 for proj in self.browse(cr, uid, ids, context=context):
246 parent_id = context.get('parent_id', False)
247 context.update({'analytic_project_copy': True})
248 new_date_start = time.strftime('%Y-%m-%d')
250 if proj.date_start and proj.date:
251 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
252 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
253 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
254 context.update({'copy':True})
255 new_id = self.template_copy(cr, uid, proj.id, default = {
256 'name': proj.name +_(' (copy)'),
258 'date_start':new_date_start,
260 'parent_id':parent_id}, context=context)
261 result.append(new_id)
263 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
264 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
266 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
268 if result and len(result):
270 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
271 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
272 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
273 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
274 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
275 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
277 'name': _('Projects'),
279 'view_mode': 'form,tree',
280 'res_model': 'project.project',
283 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
284 'type': 'ir.actions.act_window',
285 'search_view_id': search_view['res_id'],
289 # set active value for a project, its sub projects and its tasks
290 def setActive(self, cr, uid, ids, value=True, context=None):
291 task_obj = self.pool.get('project.task')
292 for proj in self.browse(cr, uid, ids, context=None):
293 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
294 cr.execute('select id from project_task where project_id=%s', (proj.id,))
295 tasks_id = [x[0] for x in cr.fetchall()]
297 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
298 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
300 self.setActive(cr, uid, child_ids, value, context=None)
305 class users(osv.osv):
306 _inherit = 'res.users'
308 'context_project_id': fields.many2one('project.project', 'Project')
313 _name = "project.task"
314 _description = "Task"
316 _date_name = "date_start"
318 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
319 obj_project = self.pool.get('project.project')
321 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
322 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
323 if id and isinstance(id, (long, int)):
324 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
325 args.append(('active', '=', False))
326 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
328 def _str_get(self, task, level=0, border='***', context=None):
329 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'+ \
330 border[0]+' '+(task.name or '')+'\n'+ \
331 (task.description or '')+'\n\n'
333 # Compute: effective_hours, total_hours, progress
334 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
336 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
337 hours = dict(cr.fetchall())
338 for task in self.browse(cr, uid, ids, context=context):
339 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)}
340 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
341 res[task.id]['progress'] = 0.0
342 if (task.remaining_hours + hours.get(task.id, 0.0)):
343 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
344 if task.state in ('done','cancelled'):
345 res[task.id]['progress'] = 100.0
349 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
350 if remaining and not planned:
351 return {'value':{'planned_hours': remaining}}
354 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
355 return {'value':{'remaining_hours': planned - effective}}
357 def onchange_project(self, cr, uid, id, project_id):
360 data = self.pool.get('project.project').browse(cr, uid, [project_id])
361 partner_id=data and data[0].parent_id.partner_id
363 return {'value':{'partner_id':partner_id.id}}
366 def _default_project(self, cr, uid, context=None):
369 if 'project_id' in context and context['project_id']:
370 return int(context['project_id'])
373 def duplicate_task(self, cr, uid, map_ids, context=None):
374 for new in map_ids.values():
375 task = self.browse(cr, uid, new, context)
376 child_ids = [ ch.id for ch in task.child_ids]
378 for child in task.child_ids:
379 if child.id in map_ids.keys():
380 child_ids.remove(child.id)
381 child_ids.append(map_ids[child.id])
383 parent_ids = [ ch.id for ch in task.parent_ids]
385 for parent in task.parent_ids:
386 if parent.id in map_ids.keys():
387 parent_ids.remove(parent.id)
388 parent_ids.append(map_ids[parent.id])
389 #FIXME why there is already the copy and the old one
390 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
392 def copy_data(self, cr, uid, id, default={}, context=None):
393 default = default or {}
394 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
395 if not default.get('remaining_hours', False):
396 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
397 default['active'] = True
398 default['type_id'] = False
399 if not default.get('name', False):
400 default['name'] = self.browse(cr, uid, id, context=context).name or ''
401 if not context.get('copy',False):
402 new_name = _("%s (copy)")%default.get('name','')
403 default.update({'name':new_name})
404 return super(task, self).copy_data(cr, uid, id, default, context)
407 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
409 for task in self.browse(cr, uid, ids, context=context):
412 if task.project_id.active == False or task.project_id.state == 'template':
416 def _get_task(self, cr, uid, ids, context=None):
418 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
419 if work.task_id: result[work.task_id.id] = True
423 '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."),
424 'name': fields.char('Task Summary', size=128, required=True),
425 'description': fields.text('Description'),
426 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
427 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
428 'type_id': fields.many2one('project.task.type', 'Stage'),
429 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
430 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.\
431 \n If the task is over, the states is set to \'Done\'.'),
432 'create_date': fields.datetime('Create Date', readonly=True,select=True),
433 'date_start': fields.datetime('Starting Date',select=True),
434 'date_end': fields.datetime('Ending Date',select=True),
435 'date_deadline': fields.date('Deadline',select=True),
436 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
437 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
438 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
439 'notes': fields.text('Notes'),
440 '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.'),
441 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
443 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
444 'project.task.work': (_get_task, ['hours'], 10),
446 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
447 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
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 'progress': fields.function(_hours_get, method=True, 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",
454 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
455 'project.task.work': (_get_task, ['hours'], 10),
457 '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.",
459 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
460 'project.task.work': (_get_task, ['hours'], 10),
462 'user_id': fields.many2one('res.users', 'Assigned to'),
463 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
464 'partner_id': fields.many2one('res.partner', 'Partner'),
465 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
466 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
467 'company_id': fields.many2one('res.company', 'Company'),
468 'id': fields.integer('ID'),
477 'project_id': _default_project,
478 'user_id': lambda obj, cr, uid, context: uid,
479 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
482 _order = "sequence,priority, date_start, name, id"
484 def _check_recursion(self, cr, uid, ids, context=None):
486 visited_branch = set()
488 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
494 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
495 if id in visited_branch: #Cycle
498 if id in visited_node: #Already tested don't work one more time for nothing
501 visited_branch.add(id)
504 #visit child using DFS
505 task = self.browse(cr, uid, id, context=context)
506 for child in task.child_ids:
507 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
511 visited_branch.remove(id)
514 def _check_dates(self, cr, uid, ids, context=None):
517 obj_task = self.browse(cr, uid, ids[0], context=context)
518 start = obj_task.date_start or False
519 end = obj_task.date_end or False
526 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
527 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
530 # Override view according to the company definition
534 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
535 users_obj = self.pool.get('res.users')
537 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
538 # this should be safe (no context passed to avoid side-effects)
539 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
540 tm = obj_tm and obj_tm.name or 'Hours'
542 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
544 if tm in ['Hours','Hour']:
547 eview = etree.fromstring(res['arch'])
549 def _check_rec(eview):
550 if eview.attrib.get('widget','') == 'float_time':
551 eview.set('widget','float')
558 res['arch'] = etree.tostring(eview)
560 for f in res['fields']:
561 if 'Hours' in res['fields'][f]['string']:
562 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
565 def action_close(self, cr, uid, ids, context=None):
566 # This action open wizard to send email to partner or project manager after close task.
567 project_id = len(ids) and ids[0] or False
568 if not project_id: return False
569 task = self.browse(cr, uid, project_id, context=context)
570 project = task.project_id
571 res = self.do_close(cr, uid, [project_id], context=context)
572 if project.warn_manager or project.warn_customer:
574 'name': _('Send Email after close task'),
577 'res_model': 'project.task.close',
578 'type': 'ir.actions.act_window',
581 'context': {'active_id': task.id}
585 def do_close(self, cr, uid, ids, context=None):
591 request = self.pool.get('res.request')
592 for task in self.browse(cr, uid, ids, context=context):
594 project = task.project_id
596 # Send request to project manager
597 if project.warn_manager and project.user_id and (project.user_id.id != uid):
598 request.create(cr, uid, {
599 'name': _("Task '%s' closed") % task.name,
602 'act_to': project.user_id.id,
603 'ref_partner_id': task.partner_id.id,
604 'ref_doc1': 'project.task,%d'% (task.id,),
605 'ref_doc2': 'project.project,%d'% (project.id,),
608 for parent_id in task.parent_ids:
609 if parent_id.state in ('pending','draft'):
611 for child in parent_id.child_ids:
612 if child.id != task.id and child.state not in ('done','cancelled'):
615 self.do_reopen(cr, uid, [parent_id.id])
616 vals.update({'state': 'done'})
617 vals.update({'remaining_hours': 0.0})
618 if not task.date_end:
619 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
620 self.write(cr, uid, [task.id],vals)
621 message = _("The task '%s' is done") % (task.name,)
622 self.log(cr, uid, task.id, message)
625 def do_reopen(self, cr, uid, ids, context=None):
626 request = self.pool.get('res.request')
628 for task in self.browse(cr, uid, ids, context=context):
629 project = task.project_id
630 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
631 request.create(cr, uid, {
632 'name': _("Task '%s' set in progress") % task.name,
635 'act_to': project.user_id.id,
636 'ref_partner_id': task.partner_id.id,
637 'ref_doc1': 'project.task,%d' % task.id,
638 'ref_doc2': 'project.project,%d' % project.id,
641 self.write(cr, uid, [task.id], {'state': 'open'})
645 def do_cancel(self, cr, uid, ids, context=None):
648 request = self.pool.get('res.request')
649 tasks = self.browse(cr, uid, ids)
651 project = task.project_id
652 if project.warn_manager and project.user_id and (project.user_id.id != uid):
653 request.create(cr, uid, {
654 'name': _("Task '%s' cancelled") % task.name,
657 'act_to': project.user_id.id,
658 'ref_partner_id': task.partner_id.id,
659 'ref_doc1': 'project.task,%d' % task.id,
660 'ref_doc2': 'project.project,%d' % project.id,
662 message = _("The task '%s' is cancelled.") % (task.name,)
663 self.log(cr, uid, task.id, message)
664 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
667 def do_open(self, cr, uid, ids, context=None):
670 tasks= self.browse(cr,uid,ids)
672 data = {'state': 'open'}
674 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
675 self.write(cr, uid, [t.id], data)
676 message = _("The task '%s' is opened.") % (t.name,)
677 self.log(cr, uid, t.id, message)
680 def do_draft(self, cr, uid, ids, context=None):
683 self.write(cr, uid, ids, {'state': 'draft'})
686 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
688 Delegate Task to another users.
690 task = self.browse(cr, uid, task_id, context=context)
691 self.copy(cr, uid, task.id, {
692 'name': delegate_data['name'],
693 'user_id': delegate_data['user_id'],
694 'planned_hours': delegate_data['planned_hours'],
695 'remaining_hours': delegate_data['planned_hours'],
696 'parent_ids': [(6, 0, [task.id])],
698 'description': delegate_data['new_task_description'] or '',
702 newname = delegate_data['prefix'] or ''
703 self.write(cr, uid, [task.id], {
704 'remaining_hours': delegate_data['planned_hours_me'],
705 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
708 if delegate_data['state'] == 'pending':
709 self.do_pending(cr, uid, [task.id], context)
711 self.do_close(cr, uid, [task.id], context=context)
712 user_pool = self.pool.get('res.users')
713 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
714 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
715 self.log(cr, uid, task.id, message)
718 def do_pending(self, cr, uid, ids, context=None):
721 self.write(cr, uid, ids, {'state': 'pending'})
722 for (id, name) in self.name_get(cr, uid, ids):
723 message = _("The task '%s' is pending.") % name
724 self.log(cr, uid, id, message)
727 def _change_type(self, cr, uid, ids, next, *args):
730 if next is False, go to previous stage
732 for task in self.browse(cr, uid, ids):
733 if task.project_id.type_ids:
734 typeid = task.type_id.id
736 for type in task.project_id.type_ids :
737 types_seq[type.id] = type.sequence
739 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
741 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
742 sorted_types = [x[0] for x in types]
744 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
745 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
746 index = sorted_types.index(typeid)
747 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
750 def next_type(self, cr, uid, ids, *args):
751 return self._change_type(cr, uid, ids, True, *args)
753 def prev_type(self, cr, uid, ids, *args):
754 return self._change_type(cr, uid, ids, False, *args)
759 class project_work(osv.osv):
760 _name = "project.task.work"
761 _description = "Project Task Work"
763 'name': fields.char('Work summary', size=128),
764 'date': fields.datetime('Date', select="1"),
765 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
766 'hours': fields.float('Time Spent'),
767 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
768 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
772 'user_id': lambda obj, cr, uid, context: uid,
773 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
777 def create(self, cr, uid, vals, *args, **kwargs):
778 if 'hours' in vals and (not vals['hours']):
780 if 'task_id' in vals:
781 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
782 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
784 def write(self, cr, uid, ids, vals, context=None):
785 if 'hours' in vals and (not vals['hours']):
788 for work in self.browse(cr, uid, ids, context=context):
789 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))
790 return super(project_work,self).write(cr, uid, ids, vals, context)
792 def unlink(self, cr, uid, ids, *args, **kwargs):
793 for work in self.browse(cr, uid, ids):
794 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
795 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
798 class account_analytic_account(osv.osv):
800 _inherit = 'account.analytic.account'
801 _description = 'Analytic Account'
803 def create(self, cr, uid, vals, context=None):
806 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
807 vals['child_ids'] = []
808 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
810 def unlink(self, cr, uid, ids, *args, **kwargs):
811 project_obj = self.pool.get('project.project')
812 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
814 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
815 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
817 account_analytic_account()
819 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: