Files
odoo_source/addons/scrum/scrum.py
T
Mustufa Rangwala 3b72b4276b [MERGE]
bzr revid: mra@mra-laptop-20100608123734-ou6c9570dp12u377
2010-06-08 18:07:34 +05:30

340 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
from mx import DateTime
import netsvc
from osv import fields, osv, orm
import re
import tools
from tools.translate import _
class scrum_project(osv.osv):
_inherit = 'project.project'
_columns = {
'product_owner_id': fields.many2one('res.users', 'Product Owner', help="The person who is responsible for the product"),
'sprint_size': fields.integer('Sprint Days', help="Number of days allocated for sprint"),
'scrum': fields.integer('Is a Scrum Project'),
}
_defaults = {
'product_owner_id': lambda self,cr,uid,context={}: uid,
'sprint_size': 15,
'scrum': 1
}
scrum_project()
class scrum_sprint(osv.osv):
_name = 'scrum.sprint'
_description = 'Scrum Sprint'
def _calc_progress(self, cr, uid, ids, name, args, context):
res = {}
for sprint in self.browse(cr, uid, ids):
tot = 0.0
prog = 0.0
for bl in sprint.backlog_ids:
tot += bl.expected_hours
prog += bl.expected_hours * bl.progress / 100.0
res.setdefault(sprint.id, 0.0)
if tot>0:
res[sprint.id] = round(prog/tot*100)
return res
def _calc_effective(self, cr, uid, ids, name, args, context):
res = {}
for sprint in self.browse(cr, uid, ids):
res.setdefault(sprint.id, 0.0)
for bl in sprint.backlog_ids:
res[sprint.id] += bl.effective_hours
return res
def _calc_planned(self, cr, uid, ids, name, args, context):
res = {}
for sprint in self.browse(cr, uid, ids):
res.setdefault(sprint.id, 0.0)
for bl in sprint.backlog_ids:
res[sprint.id] += bl.expected_hours
return res
def _calc_expected(self, cr, uid, ids, name, args, context):
res = {}
for sprint in self.browse(cr, uid, ids):
res.setdefault(sprint.id, 0.0)
for bl in sprint.backlog_ids:
res[sprint.id] += bl.expected_hours
return res
def button_cancel(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'cancel'}, context=context)
return True
def button_draft(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'draft'}, context=context)
return True
def button_open(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'open'}, context=context)
return True
def button_close(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'done'}, context=context)
return True
def button_pending(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'pending'}, context=context)
return True
_columns = {
'name' : fields.char('Sprint Name', required=True, size=64),
'date_start': fields.date('Starting Date', required=True),
'date_stop': fields.date('Ending Date', required=True),
'project_id': fields.many2one('project.project', 'Project', required=True, domain=[('scrum','=',1)], help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
'product_owner_id': fields.many2one('res.users', 'Product Owner', required=True,help="The person who is responsible for the product"),
'scrum_master_id': fields.many2one('res.users', 'Scrum Manager', required=True,help="The person who is maintains the processes for the product"),
'meeting_ids': fields.one2many('scrum.meeting', 'sprint_id', 'Daily Scrum'),
'review': fields.text('Sprint Review'),
'retrospective': fields.text('Sprint Retrospective'),
'backlog_ids': fields.one2many('scrum.product.backlog', 'sprint_id', 'Sprint Backlog'),
'progress': fields.function(_calc_progress, method=True, string='Progress (0-100)', help="Computed as: Time Spent / Total Time."),
'effective_hours': fields.function(_calc_effective, method=True, string='Effective hours', help="Computed using the sum of the task work done."),
'expected_hours': fields.function(_calc_expected, method=True, string='Planned Hours', help='Estimated time to do the task.'),
'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('cancel','Cancelled'),('done','Done')], 'State', required=True),
}
_defaults = {
'state': 'draft',
'date_start' : time.strftime('%Y-%m-%d'),
}
def copy(self, cr, uid, id, default=None, context=None):
"""Overrides orm copy method
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current user’s ID for security checks,
@param ids: List of case’s IDs
@param context: A standard dictionary for contextual values
"""
if context is None:
context = {}
if default is None:
default = {}
default.update({'backlog_ids': [], 'meeting_ids': []})
return super(scrum_sprint, self).copy(cr, uid, id, default=default, context=context)
def onchange_project_id(self, cr, uid, ids, project_id):
v = {}
if project_id:
proj = self.pool.get('project.project').browse(cr, uid, [project_id])[0]
v['product_owner_id']= proj.product_owner_id and proj.product_owner_id.id or False
v['scrum_master_id']= proj.user_id and proj.user_id.id or False
v['date_stop'] = (DateTime.now() + DateTime.RelativeDateTime(days=int(proj.sprint_size or 14))).strftime('%Y-%m-%d')
return {'value':v}
scrum_sprint()
class scrum_product_backlog(osv.osv):
_name = 'scrum.product.backlog'
_description = 'Product Backlog'
def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
if not args:
args=[]
if not context:
context={}
match = re.match('^S\(([0-9]+)\)$', name)
if match:
ids = self.search(cr, uid, [('sprint_id','=', int(match.group(1)))], limit=limit, context=context)
return self.name_get(cr, uid, ids, context=context)
return super(scrum_product_backlog, self).name_search(cr, uid, name, args, operator,context, limit=limit)
def _calc_progress(self, cr, uid, ids, name, args, context):
res = {}
for bl in self.browse(cr, uid, ids):
tot = 0.0
prog = 0.0
for task in bl.tasks_id:
tot += task.planned_hours
prog += task.planned_hours * task.progress / 100.0
res.setdefault(bl.id, 0.0)
if tot>0:
res[bl.id] = round(prog/tot*100)
return res
def _calc_effective(self, cr, uid, ids, name, args, context):
res = {}
for bl in self.browse(cr, uid, ids):
res.setdefault(bl.id, 0.0)
for task in bl.tasks_id:
res[bl.id] += task.effective_hours
return res
def _calc_task(self, cr, uid, ids, name, args, context):
res = {}
for bl in self.browse(cr, uid, ids):
res.setdefault(bl.id, 0.0)
for task in bl.tasks_id:
res[bl.id] += task.total_hours
return res
def button_cancel(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'cancel'}, context=context)
for backlog in self.browse(cr, uid, ids, context=context):
self.pool.get('project.task').write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'cancelled'})
return True
def button_draft(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'draft'}, context=context)
return True
def button_open(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'open'}, context=context)
return True
def button_close(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'done'}, context=context)
for backlog in self.browse(cr, uid, ids, context=context):
self.pool.get('project.task').write(cr, uid, [i.id for i in backlog.tasks_id], {'state': 'done'})
return True
def button_pending(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state':'pending'}, context=context)
return True
def button_postpone(self, cr, uid, ids, context=None):
for product in self.browse(cr, uid, ids, context=context):
tasks_id = []
for task in product.tasks_id:
if task.state != 'done':
tasks_id.append(task.id)
clone_id = self.copy(cr, uid, product.id, {
'name': 'PARTIAL:'+ product.name ,
'sprint_id':False,
'tasks_id':[(6, 0, tasks_id)],
})
self.write(cr, uid, ids, {'state':'cancel'}, context=context)
return True
_columns = {
'name' : fields.char('Feature', size=64, required=True),
'note' : fields.text('Note'),
'active' : fields.boolean('Active', help="If Active field is set to true, it will allow you to hide the product backlog without removing it."),
'project_id': fields.many2one('project.project', 'Project', required=True, domain=[('scrum','=',1)], help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
'user_id': fields.many2one('res.users', 'Responsible'),
'sprint_id': fields.many2one('scrum.sprint', 'Sprint'),
'sequence' : fields.integer('Sequence', help="Gives the sequence order when displaying a list of product backlog."),
'tasks_id': fields.one2many('project.task', 'product_backlog_id', 'Tasks Details'),
'state': fields.selection([('draft','Draft'),('open','Open'),('pending','Pending'),('done','Done'),('cancel','Cancelled')], 'State', required=True),
'progress': fields.function(_calc_progress, method=True, string='Progress', help="Computed as: Time Spent / Total Time."),
'effective_hours': fields.function(_calc_effective, method=True, string='Effective hours', help="Computed using the sum of the task work done (Time spent on tasks)"),
'expected_hours': fields.float('Planned Hours', help='Estimated total time to do the Backlog'),
'create_date': fields.datetime("Creation Date", readonly=True),
'task_hours': fields.function(_calc_task, method=True, string='Task Hours', help='Estimated time of the total hours of the tasks')
}
_defaults = {
'state': 'draft',
'active': 1,
'user_id': lambda self, cr, uid, context: uid,
}
_order = "sequence"
scrum_product_backlog()
class scrum_task(osv.osv):
_name = 'project.task'
_inherit = 'project.task'
def _get_task(self, cr, uid, ids, context={}):
result = {}
for line in self.pool.get('scrum.product.backlog').browse(cr, uid, ids, context=context):
for task in line.tasks_id:
result[task.id] = True
return result.keys()
_columns = {
'product_backlog_id': fields.many2one('scrum.product.backlog', 'Product Backlog',help="Related product backlog that contains this task. Used in SCRUM methodology"),
'sprint_id': fields.related('product_backlog_id','sprint_id', type='many2one', relation='scrum.sprint', string='Sprint',
store={
'project.task': (lambda self, cr, uid, ids, c={}: ids, ['product_backlog_id'], 10),
'scrum.product.backlog': (_get_task, ['sprint_id'], 10)
}),
}
def onchange_backlog_id(self, cr, uid, backlog_id):
if not backlog_id:
return {}
project_id = self.pool.get('scrum.product.backlog').browse(cr, uid, backlog_id).project_id.id
return {'value': {'project_id': project_id}}
scrum_task()
class scrum_meeting(osv.osv):
_name = 'scrum.meeting'
_description = 'Scrum Meeting'
_order = 'date desc'
_columns = {
'name' : fields.char('Meeting Name', size=64),
'date': fields.date('Meeting Date', required=True),
'sprint_id': fields.many2one('scrum.sprint', 'Sprint', required=True),
'project_id': fields.many2one('project.project', 'Project'),
'question_yesterday': fields.text('Tasks since yesterday'),
'question_today': fields.text('Tasks for today'),
'question_blocks': fields.text('Blocks encountered'),
'question_backlog': fields.text('Backlog Accurate'),
'task_ids': fields.many2many('project.task', 'meeting_task_rel', 'metting_id', 'task_id', 'Tasks')
}
#
# TODO: Find the right sprint thanks to users and date
#
_defaults = {
'date' : time.strftime('%Y-%m-%d'),
}
def button_send_to_master(self, cr, uid, ids, context=None):
meeting_id = self.browse(cr, uid, ids)[0]
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
if meeting_id and meeting_id.sprint_id.scrum_master_id.user_email:
self.email_send(cr, uid, ids, email)
else:
raise osv.except_osv(_('Error !'), _('Please provide email address for scrum master defined on sprint.'))
return True
def button_send_product_owner(self, cr, uid, ids, context=None):
meeting_id = self.browse(cr, uid, ids)[0]
if meeting_id.sprint_id.product_owner_id.user_email:
self.email_send(cr,uid,ids,email)
else:
raise osv.except_osv(_('Error !'), _('Please provide email address for product owner defined on sprint.'))
return True
def email_send(self, cr, uid, ids, email, context=None):
email_from = tools.config.get('email_from', False)
meeting_id = self.browse(cr,uid,ids)[0]
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
user_email = email_from or user.address_id.email or email_from
body = "Hello " + meeting_id.sprint_id.scrum_master_id.name+",\n" +" \nI am sending you Daily Meeting Details of date %s for the Sprint %s \n" % (meeting_id.date, meeting_id.sprint_id.name)
body += '\n*Tasks since yesterday: \n_______________________%s' % (meeting_id.question_yesterday) + '\n*Task for Today :\n_______________________ %s\n' % (meeting_id.question_today )+ '\n*Blocks encountered: \n_______________________ %s' % (meeting_id.question_blocks or 'No Blocks')
body += "\n\nThank you,\n"+ user.name
sub_name = meeting_id.name or 'Scrum Meeting of %s '%meeting_id.date
flag = tools.email_send(user_email , [email], sub_name, body, reply_to=None, openobject_id=str(meeting_id.id))
if not flag:
raise osv.except_osv(_('Error !'), _(' Email Not send to %s!' % meeting_id.sprint_id.product_owner_id.name))
return True
scrum_meeting()