1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from lxml import etree
24 from datetime import datetime, date
26 from tools.translate import _
27 from osv import fields, osv
30 class project_task_type(osv.osv):
31 _name = 'project.task.type'
32 _description = 'Task Stage'
35 'name': fields.char('Stage Name', required=True, size=64, translate=True),
36 'description': fields.text('Description'),
37 'sequence': fields.integer('Sequence'),
46 class project(osv.osv):
47 _name = "project.project"
48 _description = "Project"
49 _inherits = {'account.analytic.account': "analytic_account_id"}
51 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
53 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
54 if context and context.has_key('user_prefence') and context['user_prefence']:
55 cr.execute("""SELECT project.id FROM project_project project
56 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
57 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
58 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
59 return [(r[0]) for r in cr.fetchall()]
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
61 context=context, count=count)
63 def _complete_name(self, cr, uid, ids, name, args, context=None):
65 for m in self.browse(cr, uid, ids, context=context):
66 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
69 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
70 partner_obj = self.pool.get('res.partner')
72 return {'value':{'contact_id': False, 'pricelist_id': False}}
73 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
74 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
75 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
76 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
78 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
79 res = {}.fromkeys(ids, 0.0)
84 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
91 project_id''', (tuple(ids),))
92 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
93 for project in self.browse(cr, uid, ids, context=context):
94 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
96 'planned_hours': s[0],
97 'effective_hours': s[2],
99 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
103 def _get_project_task(self, cr, uid, ids, context=None):
105 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
106 if task.project_id: result[task.project_id.id] = True
109 def _get_project_work(self, cr, uid, ids, context=None):
111 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
112 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
115 def unlink(self, cr, uid, ids, *args, **kwargs):
116 for proj in self.browse(cr, uid, ids):
118 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
119 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
122 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
123 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
124 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
125 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True),
126 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
127 'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
129 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
130 help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
131 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
132 'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
134 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
135 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 15),
137 'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects.",
139 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
140 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 15),
141 'project.task.work': (_get_project_work, ['hours'], 20),
143 '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.",
145 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
146 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 15),
148 '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.",
150 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
151 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 15),
152 'project.task.work': (_get_project_work, ['hours'], 20),
154 '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)]}),
155 '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)]}),
156 '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)]}),
157 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
166 # TODO: Why not using a SQL contraints ?
167 def _check_dates(self, cr, uid, ids, context=None):
168 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
169 if leave['date_start'] and leave['date']:
170 if leave['date_start'] > leave['date']:
175 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
178 def set_template(self, cr, uid, ids, context=None):
179 res = self.setActive(cr, uid, ids, value=False, context=context)
182 def set_done(self, cr, uid, ids, context=None):
183 task_obj = self.pool.get('project.task')
184 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
185 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186 self.write(cr, uid, ids, {'state':'close'}, context=context)
187 for (id, name) in self.name_get(cr, uid, ids):
188 message = _("The project '%s' has been closed.") % name
189 self.log(cr, uid, id, message)
192 def set_cancel(self, cr, uid, ids, context=None):
193 task_obj = self.pool.get('project.task')
194 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
195 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
196 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
199 def set_pending(self, cr, uid, ids, context=None):
200 self.write(cr, uid, ids, {'state':'pending'}, context=context)
203 def set_open(self, cr, uid, ids, context=None):
204 self.write(cr, uid, ids, {'state':'open'}, context=context)
207 def reset_project(self, cr, uid, ids, context=None):
208 res = self.setActive(cr, uid, ids, value=True, context=context)
209 for (id, name) in self.name_get(cr, uid, ids):
210 message = _("The project '%s' has been opened.") % name
211 self.log(cr, uid, id, message)
214 def copy(self, cr, uid, id, default={}, context=None):
218 proj = self.browse(cr, uid, id, context=context)
219 default = default or {}
220 context['active_test'] = False
221 default['state'] = 'open'
222 default['code'] = False
223 default['line_ids'] = []
224 if not default.get('name', False):
225 default['name'] = proj.name + _(' (copy)')
226 res = super(project, self).copy(cr, uid, id, default, context)
230 def duplicate_template(self, cr, uid, ids, context=None):
233 project_obj = self.pool.get('project.project')
234 data_obj = self.pool.get('ir.model.data')
236 for proj in self.browse(cr, uid, ids, context=context):
237 parent_id = context.get('parent_id', False)
238 context.update({'analytic_project_copy': True})
239 new_date_start = time.strftime('%Y-%m-%d')
241 if proj.date_start and proj.date:
242 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
243 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
244 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
245 new_id = project_obj.copy(cr, uid, proj.id, default = {
246 'name': proj.name +_(' (copy)'),
248 'date_start':new_date_start,
250 'parent_id':parent_id}, context=context)
251 result.append(new_id)
253 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
254 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
256 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
258 if result and len(result):
260 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
261 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
262 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
263 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
264 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
265 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
267 'name': _('Projects'),
269 'view_mode': 'form,tree',
270 'res_model': 'project.project',
273 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
274 'type': 'ir.actions.act_window',
275 'search_view_id': search_view['res_id'],
279 # set active value for a project, its sub projects and its tasks
280 def setActive(self, cr, uid, ids, value=True, context=None):
281 task_obj = self.pool.get('project.task')
282 for proj in self.browse(cr, uid, ids, context=None):
283 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
284 cr.execute('select id from project_task where project_id=%s', (proj.id,))
285 tasks_id = [x[0] for x in cr.fetchall()]
287 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
288 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
290 self.setActive(cr, uid, child_ids, value, context=None)
295 class users(osv.osv):
296 _inherit = 'res.users'
298 'context_project_id': fields.many2one('project.project', 'Project')
303 _name = "project.task"
304 _description = "Task"
306 _date_name = "date_start"
308 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
309 obj_project = self.pool.get('project.project')
311 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
312 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
313 if id and isinstance(id, (long, int)):
314 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
315 args.append(('active', '=', False))
316 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
318 def _str_get(self, task, level=0, border='***', context=None):
319 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'+ \
320 border[0]+' '+(task.name or '')+'\n'+ \
321 (task.description or '')+'\n\n'
323 # Compute: effective_hours, total_hours, progress
324 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
326 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
327 hours = dict(cr.fetchall())
328 for task in self.browse(cr, uid, ids, context=context):
329 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)}
330 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
331 res[task.id]['progress'] = 0.0
332 if (task.remaining_hours + hours.get(task.id, 0.0)):
333 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
334 if task.state in ('done','cancelled'):
335 res[task.id]['progress'] = 100.0
339 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
340 if remaining and not planned:
341 return {'value':{'planned_hours': remaining}}
344 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
345 return {'value':{'remaining_hours': planned - effective}}
347 def onchange_project(self, cr, uid, id, project_id):
350 data = self.pool.get('project.project').browse(cr, uid, [project_id])
351 partner_id=data and data[0].parent_id.partner_id
353 return {'value':{'partner_id':partner_id.id}}
356 def _default_project(self, cr, uid, context=None):
359 if 'project_id' in context and context['project_id']:
360 return int(context['project_id'])
363 def copy_data(self, cr, uid, id, default={}, context=None):
364 default = default or {}
365 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
366 if not default.get('remaining_hours', False):
367 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
368 default['active'] = True
369 default['type_id'] = False
370 if not default.get('name', False):
371 default['name'] = self.browse(cr, uid, id, context=context).name
372 return super(task, self).copy_data(cr, uid, id, default, context)
374 def _check_dates(self, cr, uid, ids, context=None):
375 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
376 if task['date_start'] and task['date_end']:
377 if task['date_start'] > task['date_end']:
381 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
383 for task in self.browse(cr, uid, ids, context=context):
386 if task.project_id.active == False or task.project_id.state == 'template':
390 def _get_task(self, cr, uid, ids, context=None):
392 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
393 if work.task_id: result[work.task_id.id] = True
397 '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."),
398 'name': fields.char('Task Summary', size=128, required=True),
399 'description': fields.text('Description'),
400 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
401 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
402 'type_id': fields.many2one('project.task.type', 'Stage'),
403 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
404 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.\
405 \n If the task is over, the states is set to \'Done\'.'),
406 'create_date': fields.datetime('Create Date', readonly=True,select=True),
407 'date_start': fields.datetime('Starting Date',select=True),
408 'date_end': fields.datetime('Ending Date',select=True),
409 'date_deadline': fields.date('Deadline',select=True),
410 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
411 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
412 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
413 'notes': fields.text('Notes'),
414 '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.'),
415 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
417 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
418 'project.task.work': (_get_task, ['hours'], 15),
420 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
421 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
423 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
424 'project.task.work': (_get_task, ['hours'], 15),
426 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
428 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
429 'project.task.work': (_get_task, ['hours'], 15),
431 '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.",
433 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
434 'project.task.work': (_get_task, ['hours'], 15),
436 'user_id': fields.many2one('res.users', 'Assigned to'),
437 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
438 'partner_id': fields.many2one('res.partner', 'Partner'),
439 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
440 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
441 'company_id': fields.many2one('res.company', 'Company'),
442 'id': fields.integer('ID'),
451 'project_id': _default_project,
452 'user_id': lambda obj, cr, uid, context: uid,
453 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
456 _order = "sequence,priority, date_start, name, id"
458 def _check_recursion(self, cr, uid, ids, context=None):
459 obj_task = self.browse(cr, uid, ids[0], context=context)
460 parent_ids = [x.id for x in obj_task.parent_ids]
461 children_ids = [x.id for x in obj_task.child_ids]
463 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
467 cr.execute('SELECT DISTINCT task_id '\
468 'FROM project_task_parent_rel '\
469 'WHERE parent_id IN %s', (tuple(ids),))
470 child_ids = map(lambda x: x[0], cr.fetchall())
472 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
475 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
476 if (list(set(parent_ids).intersection(set(s_ids)))):
482 def _check_dates(self, cr, uid, ids, context=None):
485 obj_task = self.browse(cr, uid, ids[0], context=context)
486 start = obj_task.date_start or False
487 end = obj_task.date_end or False
494 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
495 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
498 # Override view according to the company definition
502 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
503 users_obj = self.pool.get('res.users')
504 translation_obj = self.pool.get('ir.translation')
506 untranslate = ['Hour','Hours']
507 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
508 # this should be safe (no context passed to avoid side-effects)
509 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
510 tm = obj_tm and obj_tm.name or 'Hours'
512 is_hour = translation_obj.search(cr, uid, [('name', '=', 'product.uom,name'), ('src', 'in', untranslate)])
513 translated = [i.value for i in translation_obj.browse(cr, uid, is_hour, context=context)]
514 translated.extend(untranslate)
516 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
521 eview = etree.fromstring(res['arch'])
523 def _check_rec(eview):
524 if eview.attrib.get('widget','') == 'float_time':
525 eview.set('widget','float')
532 res['arch'] = etree.tostring(eview)
534 for f in res['fields']:
535 for trans in translated:
536 if trans in res['fields'][f]['string']:
537 res['fields'][f]['string'] = res['fields'][f]['string'].replace(trans, tm)
540 def action_close(self, cr, uid, ids, context=None):
541 # This action open wizard to send email to partner or project manager after close task.
542 project_id = len(ids) and ids[0] or False
543 if not project_id: return False
544 task = self.browse(cr, uid, project_id, context=context)
545 project = task.project_id
546 res = self.do_close(cr, uid, [project_id], context=context)
547 if project.warn_manager or project.warn_customer:
549 'name': _('Send Email after close task'),
552 'res_model': 'project.task.close',
553 'type': 'ir.actions.act_window',
556 'context': {'active_id': task.id}
560 def do_close(self, cr, uid, ids, context=None):
564 request = self.pool.get('res.request')
565 for task in self.browse(cr, uid, ids, context=context):
567 project = task.project_id
569 # Send request to project manager
570 if project.warn_manager and project.user_id and (project.user_id.id != uid):
571 request.create(cr, uid, {
572 'name': _("Task '%s' closed") % task.name,
575 'act_to': project.user_id.id,
576 'ref_partner_id': task.partner_id.id,
577 'ref_doc1': 'project.task,%d'% (task.id,),
578 'ref_doc2': 'project.project,%d'% (project.id,),
581 for parent_id in task.parent_ids:
582 if parent_id.state in ('pending','draft'):
584 for child in parent_id.child_ids:
585 if child.id != task.id and child.state not in ('done','cancelled'):
588 self.do_reopen(cr, uid, [parent_id.id])
589 vals.update({'state': 'done'})
590 vals.update({'remaining_hours': 0.0})
591 if not task.date_end:
592 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
593 self.write(cr, uid, [task.id],vals)
594 message = _("The task '%s' is done") % (task.name,)
595 self.log(cr, uid, task.id, message)
598 def do_reopen(self, cr, uid, ids, context=None):
599 request = self.pool.get('res.request')
601 for task in self.browse(cr, uid, ids, context=context):
602 project = task.project_id
603 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
604 request.create(cr, uid, {
605 'name': _("Task '%s' set in progress") % task.name,
608 'act_to': project.user_id.id,
609 'ref_partner_id': task.partner_id.id,
610 'ref_doc1': 'project.task,%d' % task.id,
611 'ref_doc2': 'project.project,%d' % project.id,
614 self.write(cr, uid, [task.id], {'state': 'open'})
618 def do_cancel(self, cr, uid, ids, *args):
619 request = self.pool.get('res.request')
620 tasks = self.browse(cr, uid, ids)
622 project = task.project_id
623 if project.warn_manager and project.user_id and (project.user_id.id != uid):
624 request.create(cr, uid, {
625 'name': _("Task '%s' cancelled") % task.name,
628 'act_to': project.user_id.id,
629 'ref_partner_id': task.partner_id.id,
630 'ref_doc1': 'project.task,%d' % task.id,
631 'ref_doc2': 'project.project,%d' % project.id,
633 message = _("The task '%s' is cancelled.") % (task.name,)
634 self.log(cr, uid, task.id, message)
635 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
638 def do_open(self, cr, uid, ids, *args):
639 tasks= self.browse(cr,uid,ids)
641 data = {'state': 'open'}
643 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
644 self.write(cr, uid, [t.id], data)
645 message = _("The task '%s' is opened.") % (t.name,)
646 self.log(cr, uid, t.id, message)
649 def do_draft(self, cr, uid, ids, *args):
650 self.write(cr, uid, ids, {'state': 'draft'})
653 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
655 Delegate Task to another users.
657 task = self.browse(cr, uid, task_id, context=context)
658 self.copy(cr, uid, task.id, {
659 'name': delegate_data['name'],
660 'user_id': delegate_data['user_id'],
661 'planned_hours': delegate_data['planned_hours'],
662 'remaining_hours': delegate_data['planned_hours'],
663 'parent_ids': [(6, 0, [task.id])],
665 'description': delegate_data['new_task_description'] or '',
669 newname = delegate_data['prefix'] or ''
670 self.write(cr, uid, [task.id], {
671 'remaining_hours': delegate_data['planned_hours_me'],
672 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
675 if delegate_data['state'] == 'pending':
676 self.do_pending(cr, uid, [task.id], context)
678 self.do_close(cr, uid, [task.id], context=context)
679 user_pool = self.pool.get('res.users')
680 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
681 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
682 self.log(cr, uid, task.id, message)
685 def do_pending(self, cr, uid, ids, *args):
686 self.write(cr, uid, ids, {'state': 'pending'})
687 for (id, name) in self.name_get(cr, uid, ids):
688 message = _("The task '%s' is pending.") % name
689 self.log(cr, uid, id, message)
692 def next_type(self, cr, uid, ids, *args):
693 for task in self.browse(cr, uid, ids):
694 typeid = task.type_id.id
695 types = map(lambda x:x.id, task.project_id.type_ids or [])
698 self.write(cr, uid, task.id, {'type_id': types[0]})
699 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
700 index = types.index(typeid)
701 self.write(cr, uid, task.id, {'type_id': types[index+1]})
704 def prev_type(self, cr, uid, ids, *args):
705 for task in self.browse(cr, uid, ids):
706 typeid = task.type_id.id
707 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
709 if typeid and typeid in types:
710 index = types.index(typeid)
711 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
716 class project_work(osv.osv):
717 _name = "project.task.work"
718 _description = "Project Task Work"
720 'name': fields.char('Work summary', size=128),
721 'date': fields.datetime('Date'),
722 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
723 'hours': fields.float('Time Spent'),
724 'user_id': fields.many2one('res.users', 'Done by', required=True),
725 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
729 'user_id': lambda obj, cr, uid, context: uid,
730 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
734 def create(self, cr, uid, vals, *args, **kwargs):
735 if 'hours' in vals and (not vals['hours']):
737 if 'task_id' in vals:
738 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
739 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
741 def write(self, cr, uid, ids, vals, context=None):
742 if 'hours' in vals and (not vals['hours']):
745 for work in self.browse(cr, uid, ids, context=context):
746 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))
747 return super(project_work,self).write(cr, uid, ids, vals, context)
749 def unlink(self, cr, uid, ids, *args, **kwargs):
750 for work in self.browse(cr, uid, ids):
751 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
752 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
755 class account_analytic_account(osv.osv):
757 _inherit = 'account.analytic.account'
758 _description = 'Analytic Account'
760 def create(self, cr, uid, vals, context=None):
763 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
764 vals['child_ids'] = []
765 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
767 def unlink(self, cr, uid, ids, context=None):
768 project_obj = self.pool.get('project.project')
769 project_ids = project_obj.search(cr, 1, [('analytic_account_id','in',ids)])
771 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
772 return super(account_analytic_account, self).unlink(cr, uid, ids, context=context)
774 account_analytic_account()
776 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: