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'),
50 class project(osv.osv):
51 _name = "project.project"
52 _description = "Project"
53 _inherits = {'account.analytic.account': "analytic_account_id"}
55 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
57 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
58 if context and context.get('user_preference'):
59 cr.execute("""SELECT project.id FROM project_project project
60 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
61 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
62 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
63 return [(r[0]) for r in cr.fetchall()]
64 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
65 context=context, count=count)
67 def _complete_name(self, cr, uid, ids, name, args, context=None):
69 for m in self.browse(cr, uid, ids, context=context):
70 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
73 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
74 partner_obj = self.pool.get('res.partner')
76 return {'value':{'contact_id': False, 'pricelist_id': False}}
77 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
78 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
79 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
80 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
82 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
83 res = {}.fromkeys(ids, 0.0)
87 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
94 project_id''', (tuple(ids),))
95 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
96 for project in self.browse(cr, uid, ids, context=context):
97 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
99 'planned_hours': s[0],
100 'effective_hours': s[2],
102 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
106 def _get_project_task(self, cr, uid, ids, context=None):
108 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
109 if task.project_id: result[task.project_id.id] = True
112 def _get_project_work(self, cr, uid, ids, context=None):
114 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
115 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
118 def unlink(self, cr, uid, ids, *args, **kwargs):
119 for proj in self.browse(cr, uid, ids):
121 raise osv.except_osv(_('Operation Not Permitted !'), _('You cannot delete a project containing tasks. I suggest you to desactivate it.'))
122 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
125 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
126 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
127 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
128 '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),
129 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
130 '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)]}),
132 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
133 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)]}),
134 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
135 '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.",
137 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
138 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
140 '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."),
141 '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.",
143 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
144 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
146 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
147 '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)]}),
148 '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)]}),
149 '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)]}),
150 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
152 def _get_type_common(self, cr, uid, context):
153 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
161 'type_ids': _get_type_common
164 # TODO: Why not using a SQL contraints ?
165 def _check_dates(self, cr, uid, ids, context=None):
166 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
167 if leave['date_start'] and leave['date']:
168 if leave['date_start'] > leave['date']:
173 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
176 def set_template(self, cr, uid, ids, context=None):
177 res = self.setActive(cr, uid, ids, value=False, context=context)
180 def set_done(self, cr, uid, ids, context=None):
181 task_obj = self.pool.get('project.task')
182 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
183 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
184 self.write(cr, uid, ids, {'state':'close'}, context=context)
185 for (id, name) in self.name_get(cr, uid, ids):
186 message = _("The project '%s' has been closed.") % name
187 self.log(cr, uid, id, message)
190 def set_cancel(self, cr, uid, ids, context=None):
191 task_obj = self.pool.get('project.task')
192 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
193 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
194 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
197 def set_pending(self, cr, uid, ids, context=None):
198 self.write(cr, uid, ids, {'state':'pending'}, context=context)
201 def set_open(self, cr, uid, ids, context=None):
202 self.write(cr, uid, ids, {'state':'open'}, context=context)
205 def reset_project(self, cr, uid, ids, context=None):
206 res = self.setActive(cr, uid, ids, value=True, context=context)
207 for (id, name) in self.name_get(cr, uid, ids):
208 message = _("The project '%s' has been opened.") % name
209 self.log(cr, uid, id, message)
212 def copy(self, cr, uid, id, default={}, context=None):
216 default = default or {}
217 context['active_test'] = False
218 default['state'] = 'open'
219 proj = self.browse(cr, uid, id, context=context)
220 if not default.get('name', False):
221 default['name'] = proj.name + _(' (copy)')
223 res = super(project, self).copy(cr, uid, id, default, context)
227 def template_copy(self, cr, uid, id, default={}, context=None):
228 task_obj = self.pool.get('project.task')
229 proj = self.browse(cr, uid, id, context=context)
231 default['tasks'] = [] #avoid to copy all the task automaticly
232 res = self.copy(cr, uid, id, default=default, context=context)
234 #copy all the task manually
236 for task in proj.tasks:
237 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
239 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
240 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
244 def duplicate_template(self, cr, uid, ids, context=None):
247 data_obj = self.pool.get('ir.model.data')
249 for proj in self.browse(cr, uid, ids, context=context):
250 parent_id = context.get('parent_id', False)
251 context.update({'analytic_project_copy': True})
252 new_date_start = time.strftime('%Y-%m-%d')
254 if proj.date_start and proj.date:
255 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
256 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
257 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
258 context.update({'copy':True})
259 new_id = self.template_copy(cr, uid, proj.id, default = {
260 'name': proj.name +_(' (copy)'),
262 'date_start':new_date_start,
264 'parent_id':parent_id}, context=context)
265 result.append(new_id)
267 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
268 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
270 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
272 if result and len(result):
274 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
275 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
276 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
277 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
278 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
279 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
281 'name': _('Projects'),
283 'view_mode': 'form,tree',
284 'res_model': 'project.project',
287 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
288 'type': 'ir.actions.act_window',
289 'search_view_id': search_view['res_id'],
293 # set active value for a project, its sub projects and its tasks
294 def setActive(self, cr, uid, ids, value=True, context=None):
295 task_obj = self.pool.get('project.task')
296 for proj in self.browse(cr, uid, ids, context=None):
297 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
298 cr.execute('select id from project_task where project_id=%s', (proj.id,))
299 tasks_id = [x[0] for x in cr.fetchall()]
301 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
302 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
304 self.setActive(cr, uid, child_ids, value, context=None)
309 class users(osv.osv):
310 _inherit = 'res.users'
312 'context_project_id': fields.many2one('project.project', 'Project')
317 _name = "project.task"
318 _description = "Task"
320 _date_name = "date_start"
322 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
323 obj_project = self.pool.get('project.project')
325 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
326 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
327 if id and isinstance(id, (long, int)):
328 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
329 args.append(('active', '=', False))
330 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
332 def _str_get(self, task, level=0, border='***', context=None):
333 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'+ \
334 border[0]+' '+(task.name or '')+'\n'+ \
335 (task.description or '')+'\n\n'
337 # Compute: effective_hours, total_hours, progress
338 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
340 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
341 hours = dict(cr.fetchall())
342 for task in self.browse(cr, uid, ids, context=context):
343 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)}
344 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
345 res[task.id]['progress'] = 0.0
346 if (task.remaining_hours + hours.get(task.id, 0.0)):
347 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
348 if task.state in ('done','cancelled'):
349 res[task.id]['progress'] = 100.0
353 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
354 if remaining and not planned:
355 return {'value':{'planned_hours': remaining}}
358 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
359 return {'value':{'remaining_hours': planned - effective}}
361 def onchange_project(self, cr, uid, id, project_id):
364 data = self.pool.get('project.project').browse(cr, uid, [project_id])
365 partner_id=data and data[0].parent_id.partner_id
367 return {'value':{'partner_id':partner_id.id}}
370 def _default_project(self, cr, uid, context=None):
373 if 'project_id' in context and context['project_id']:
374 return int(context['project_id'])
377 def duplicate_task(self, cr, uid, map_ids, context=None):
378 for new in map_ids.values():
379 task = self.browse(cr, uid, new, context)
380 child_ids = [ ch.id for ch in task.child_ids]
382 for child in task.child_ids:
383 if child.id in map_ids.keys():
384 child_ids.remove(child.id)
385 child_ids.append(map_ids[child.id])
387 parent_ids = [ ch.id for ch in task.parent_ids]
389 for parent in task.parent_ids:
390 if parent.id in map_ids.keys():
391 parent_ids.remove(parent.id)
392 parent_ids.append(map_ids[parent.id])
393 #FIXME why there is already the copy and the old one
394 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
396 def copy_data(self, cr, uid, id, default={}, context=None):
397 default = default or {}
398 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
399 if not default.get('remaining_hours', False):
400 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
401 default['active'] = True
402 default['type_id'] = False
403 if not default.get('name', False):
404 default['name'] = self.browse(cr, uid, id, context=context).name or ''
405 if not context.get('copy',False):
406 new_name = _("%s (copy)")%default.get('name','')
407 default.update({'name':new_name})
408 return super(task, self).copy_data(cr, uid, id, default, context)
411 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
413 for task in self.browse(cr, uid, ids, context=context):
416 if task.project_id.active == False or task.project_id.state == 'template':
420 def _get_task(self, cr, uid, ids, context=None):
422 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
423 if work.task_id: result[work.task_id.id] = True
427 '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."),
428 'name': fields.char('Task Summary', size=128, required=True),
429 'description': fields.text('Description'),
430 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
431 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
432 'type_id': fields.many2one('project.task.type', 'Stage'),
433 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
434 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.\
435 \n If the task is over, the states is set to \'Done\'.'),
436 'create_date': fields.datetime('Create Date', readonly=True,select=True),
437 'date_start': fields.datetime('Starting Date',select=True),
438 'date_end': fields.datetime('Ending Date',select=True),
439 'date_deadline': fields.date('Deadline',select=True),
440 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
441 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
442 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
443 'notes': fields.text('Notes'),
444 '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.'),
445 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
447 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
448 'project.task.work': (_get_task, ['hours'], 10),
450 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
451 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
453 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
454 'project.task.work': (_get_task, ['hours'], 10),
456 '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",
458 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
459 'project.task.work': (_get_task, ['hours'], 10),
461 '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.",
463 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
464 'project.task.work': (_get_task, ['hours'], 10),
466 'user_id': fields.many2one('res.users', 'Assigned to'),
467 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
468 'partner_id': fields.many2one('res.partner', 'Partner'),
469 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
470 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
471 'company_id': fields.many2one('res.company', 'Company'),
472 'id': fields.integer('ID'),
481 'project_id': _default_project,
482 'user_id': lambda obj, cr, uid, context: uid,
483 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
486 _order = "sequence,priority, date_start, name, id"
488 def _check_recursion(self, cr, uid, ids, context=None):
490 visited_branch = set()
492 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
498 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
499 if id in visited_branch: #Cycle
502 if id in visited_node: #Already tested don't work one more time for nothing
505 visited_branch.add(id)
508 #visit child using DFS
509 task = self.browse(cr, uid, id, context=context)
510 for child in task.child_ids:
511 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
515 visited_branch.remove(id)
518 def _check_dates(self, cr, uid, ids, context=None):
521 obj_task = self.browse(cr, uid, ids[0], context=context)
522 start = obj_task.date_start or False
523 end = obj_task.date_end or False
530 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
531 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
534 # Override view according to the company definition
536 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
537 users_obj = self.pool.get('res.users')
539 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
540 # this should be safe (no context passed to avoid side-effects)
541 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
542 tm = obj_tm and obj_tm.name or 'Hours'
544 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
546 if tm in ['Hours','Hour']:
549 eview = etree.fromstring(res['arch'])
551 def _check_rec(eview):
552 if eview.attrib.get('widget','') == 'float_time':
553 eview.set('widget','float')
560 res['arch'] = etree.tostring(eview)
562 for f in res['fields']:
563 if 'Hours' in res['fields'][f]['string']:
564 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
567 def _check_child_task(self, cr, uid, ids, context=None):
570 tasks = self.browse(cr, uid, ids, context=context)
573 for child in task.child_ids:
574 if child.state in ['draft', 'open', 'pending']:
575 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
578 def action_close(self, cr, uid, ids, context=None):
579 # This action open wizard to send email to partner or project manager after close task.
582 task_id = len(ids) and ids[0] or False
583 self._check_child_task(cr, uid, ids, context=context)
584 if not task_id: return False
585 task = self.browse(cr, uid, task_id, context=context)
586 project = task.project_id
587 res = self.do_close(cr, uid, [task_id], context=context)
588 if project.warn_manager or project.warn_customer:
590 'name': _('Send Email after close task'),
593 'res_model': 'mail.compose.message',
594 'type': 'ir.actions.act_window',
597 'context': {'active_id': task.id,
598 'active_model': 'project.task'}
602 def do_close(self, cr, uid, ids, context={}):
606 request = self.pool.get('res.request')
607 for task in self.browse(cr, uid, ids, context=context):
609 project = task.project_id
611 # Send request to project manager
612 if project.warn_manager and project.user_id and (project.user_id.id != uid):
613 request.create(cr, uid, {
614 'name': _("Task '%s' closed") % task.name,
617 'act_to': project.user_id.id,
618 'ref_partner_id': task.partner_id.id,
619 'ref_doc1': 'project.task,%d'% (task.id,),
620 'ref_doc2': 'project.project,%d'% (project.id,),
623 for parent_id in task.parent_ids:
624 if parent_id.state in ('pending','draft'):
626 for child in parent_id.child_ids:
627 if child.id != task.id and child.state not in ('done','cancelled'):
630 self.do_reopen(cr, uid, [parent_id.id], context=context)
631 vals.update({'state': 'done'})
632 vals.update({'remaining_hours': 0.0})
633 if not task.date_end:
634 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
635 self.write(cr, uid, [task.id],vals, context=context)
636 message = _("The task '%s' is done") % (task.name,)
637 self.log(cr, uid, task.id, message)
640 def do_reopen(self, cr, uid, ids, context=None):
641 request = self.pool.get('res.request')
643 for task in self.browse(cr, uid, ids, context=context):
644 project = task.project_id
645 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
646 request.create(cr, uid, {
647 'name': _("Task '%s' set in progress") % task.name,
650 'act_to': project.user_id.id,
651 'ref_partner_id': task.partner_id.id,
652 'ref_doc1': 'project.task,%d' % task.id,
653 'ref_doc2': 'project.project,%d' % project.id,
656 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
659 def do_cancel(self, cr, uid, ids, context={}):
660 request = self.pool.get('res.request')
661 tasks = self.browse(cr, uid, ids, context=context)
662 self._check_child_task(cr, uid, ids, context=context)
664 project = task.project_id
665 if project.warn_manager and project.user_id and (project.user_id.id != uid):
666 request.create(cr, uid, {
667 'name': _("Task '%s' cancelled") % task.name,
670 'act_to': project.user_id.id,
671 'ref_partner_id': task.partner_id.id,
672 'ref_doc1': 'project.task,%d' % task.id,
673 'ref_doc2': 'project.project,%d' % project.id,
675 message = _("The task '%s' is cancelled.") % (task.name,)
676 self.log(cr, uid, task.id, message)
677 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
680 def do_open(self, cr, uid, ids, context={}):
681 tasks= self.browse(cr, uid, ids, context=context)
683 data = {'state': 'open'}
685 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
686 self.write(cr, uid, [t.id], data, context=context)
687 message = _("The task '%s' is opened.") % (t.name,)
688 self.log(cr, uid, t.id, message)
691 def do_draft(self, cr, uid, ids, context={}):
692 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
695 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
697 Delegate Task to another users.
699 task = self.browse(cr, uid, task_id, context=context)
700 self.copy(cr, uid, task.id, {
701 'name': delegate_data['name'],
702 'user_id': delegate_data['user_id'],
703 'planned_hours': delegate_data['planned_hours'],
704 'remaining_hours': delegate_data['planned_hours'],
705 'parent_ids': [(6, 0, [task.id])],
707 'description': delegate_data['new_task_description'] or '',
711 newname = delegate_data['prefix'] or ''
712 self.write(cr, uid, [task.id], {
713 'remaining_hours': delegate_data['planned_hours_me'],
714 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
717 if delegate_data['state'] == 'pending':
718 self.do_pending(cr, uid, [task.id], context)
720 self.do_close(cr, uid, [task.id], context=context)
721 user_pool = self.pool.get('res.users')
722 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
723 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
724 self.log(cr, uid, task.id, message)
727 def do_pending(self, cr, uid, ids, context={}):
728 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
729 for (id, name) in self.name_get(cr, uid, ids):
730 message = _("The task '%s' is pending.") % name
731 self.log(cr, uid, id, message)
734 def _change_type(self, cr, uid, ids, next, *args):
737 if next is False, go to previous stage
739 for task in self.browse(cr, uid, ids):
740 if task.project_id.type_ids:
741 typeid = task.type_id.id
743 for type in task.project_id.type_ids :
744 types_seq[type.id] = type.sequence
746 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
748 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
749 sorted_types = [x[0] for x in types]
751 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
752 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
753 index = sorted_types.index(typeid)
754 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
757 def next_type(self, cr, uid, ids, *args):
758 return self._change_type(cr, uid, ids, True, *args)
760 def prev_type(self, cr, uid, ids, *args):
761 return self._change_type(cr, uid, ids, False, *args)
763 def unlink(self, cr, uid, ids, context=None):
766 self._check_child_task(cr, uid, ids, context=context)
767 res = super(task, self).unlink(cr, uid, ids, context)
772 class project_work(osv.osv):
773 _name = "project.task.work"
774 _description = "Project Task Work"
776 'name': fields.char('Work summary', size=128),
777 'date': fields.datetime('Date', select="1"),
778 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
779 'hours': fields.float('Time Spent'),
780 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
781 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
785 'user_id': lambda obj, cr, uid, context: uid,
786 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
790 def create(self, cr, uid, vals, *args, **kwargs):
791 if 'hours' in vals and (not vals['hours']):
793 if 'task_id' in vals:
794 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
795 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
797 def write(self, cr, uid, ids, vals, context=None):
798 if 'hours' in vals and (not vals['hours']):
801 for work in self.browse(cr, uid, ids, context=context):
802 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))
803 return super(project_work,self).write(cr, uid, ids, vals, context)
805 def unlink(self, cr, uid, ids, *args, **kwargs):
806 for work in self.browse(cr, uid, ids):
807 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
808 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
811 class account_analytic_account(osv.osv):
813 _inherit = 'account.analytic.account'
814 _description = 'Analytic Account'
816 def create(self, cr, uid, vals, context=None):
819 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
820 vals['child_ids'] = []
821 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
823 def unlink(self, cr, uid, ids, *args, **kwargs):
824 project_obj = self.pool.get('project.project')
825 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
827 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
828 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
830 account_analytic_account()
832 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: