flectra/addons/project_scrum/models/project_sprint.py
2018-01-18 15:28:29 +05:30

671 lines
25 KiB
Python
Executable File

# Part of Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models, fields, api, _
from flectra.exceptions import ValidationError
from datetime import datetime, timedelta
class ProjectSprint(models.Model):
_name = "project.sprint"
_inherit = ['ir.branch.company.mixin', 'mail.thread']
_description = "Sprint of the Project"
_rec_name = 'sprint_seq'
sprint_seq = fields.Char(
string="Reference", readonly=True)
name = fields.Char("Sprint Name", required=True,
track_visibility="onchange")
goal_of_sprint = fields.Char("Goal of Sprint", track_visibility="onchange")
meeting_date = fields.Datetime("Planning Meeting Date", required=True,
track_visibility="onchange")
hour = fields.Float(string="Hour", track_visibility="onchange")
time_zone = fields.Selection([
('am', 'AM'),
('pm', 'PM'),
], track_visibility="onchange")
estimated_velocity = fields.Integer(
compute="calculate_estimated_velocity", string="Estimated Velocity",
store=True, track_visibility="onchange")
actual_velocity = fields.Integer(
compute="calculate_actual_velocity", string="Actual Velocity",
store=True, track_visibility="onchange")
sprint_planning_line = fields.One2many(
'sprint.planning.line', 'sprint_id', string="Sprint Planning Lines")
project_id = fields.Many2one('project.project', string="Project",
track_visibility="onchange")
start_date = fields.Date(string="Start Date", track_visibility="onchange")
end_date = fields.Date(string="End Date", track_visibility="onchange")
working_days = fields.Integer(
compute="calculate_business_days", string="Business Days",
store=True, track_visibility="onchange")
productivity_hours = fields.Float(string="Productivity Hours",
track_visibility="onchange")
productivity_per = fields.Float(
compute="calculate_productivity_per", string="Productivity (%)",
store=True, track_visibility="onchange")
holiday_type = fields.Selection(
[('hours', 'Hours'), ('days', 'Days')],
string="Holiday (Hours / Days)", default='hours',
track_visibility="onchange")
holiday_count = fields.Float(string="Holiday Count",
track_visibility="onchange")
holiday_days = fields.Float(
compute="calculate_holiday_days", string="Holiday Days", store=True,
track_visibility="onchange")
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In Progress'),
('pending', 'Pending'),
('done', 'Done'),
('cancel', 'Cancel')], string="State", default='draft',
track_visibility="onchange")
team_id = fields.Many2one('project.team', string="Team",
track_visibility="onchange", required=True)
task_line = fields.One2many('project.task', 'sprint_id', string='Tasks')
color = fields.Integer('Color Index')
@api.depends('start_date', 'end_date')
def days_calculate(self):
if self.start_date and self.end_date:
diff = datetime.strptime(
self.end_date, '%Y-%m-%d').date() - datetime.strptime(
self.start_date, '%Y-%m-%d').date()
self.duration = diff.days
duration = fields.Integer(
"Duration (In Days)", compute='days_calculate', store=True,
track_visibility="onchange")
@api.multi
def _get_task_count(self):
for record in self:
record.number_of_tasks = record.env['project.task'].search_count([
('sprint_id', '=', record.id)])
number_of_tasks = fields.Integer(
string="# of tasks", compute="_get_task_count")
@api.multi
def _get_story_count(self):
for record in self:
count = record.env['project.story'].search_count([
('sprint_id', '=', record.id)])
record.number_of_stories = count
number_of_stories = fields.Integer(
string="# of stories", compute="_get_story_count")
@api.multi
def _get_retrospective_count(self):
for record in self:
count = record.env['retrospective'].search_count([
('sprint_id', '=', record.id)])
record.number_of_retrospectives = count
number_of_retrospectives = fields.Integer(
string="# of Retrospectives", compute="_get_retrospective_count")
@api.multi
@api.depends('task_line', 'task_line.stage_id', 'task_line.sprint_id',
'estimated_velocity', 'start_date', 'end_date', 'project_id',
'team_id')
def calculate_tasks_json(self):
data = []
for record in self:
task_ids = self.env['project.task'].search([
('sprint_id', '=', record.id)])
velocity = record.estimated_velocity or 1.0
for task in task_ids:
data.append({
'task': task.task_seq or '/',
'velocity': task.velocity or 0,
'per': round(((float(task.velocity) * 100) / float(
velocity)), 2)
})
record.tasks_json = data
tasks_json = fields.Char(
string="Tasks", compute="calculate_tasks_json", store=True)
@api.multi
def get_data(self):
task_dict_list = []
for record in self:
task_pool = self.env['project.task'].search([
('sprint_id', '=', record.id)])
for task in task_pool:
task_dict = {
'reference': task.task_seq,
'name': task.name,
'velocity': task.velocity,
'start_date': task.start_date,
'end_date': task.end_date,
'actual_end_date': task.actual_end_date,
'assigned_to': task.user_id.name,
'state': task.stage_id.name,
}
task_dict_list.append(task_dict)
return task_dict_list
@api.multi
def set_state_open(self):
self.state = 'in_progress'
@api.multi
def set_state_cancel(self):
self.state = 'cancel'
@api.multi
def set_state_pending(self):
self.state = 'pending'
@api.multi
def redirect_to_view(self, model, caption):
return {
'name': (_(caption)),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': model,
'domain': [('sprint_id', '=', self.id)],
'context': {
'default_sprint_id': self.id,
'default_project_id': self.project_id.id
}
}
@api.multi
def action_view_tasks(self):
return self.redirect_to_view("project.task", "Tasks")
@api.multi
def action_view_stories(self):
return self.redirect_to_view("project.story", "Stories")
@api.multi
def action_view_release_planning(self):
return self.redirect_to_view("release.planning", "Release Planning")
@api.multi
def action_view_retrospective(self):
return self.redirect_to_view("retrospective", "Retrospective")
@api.constrains('start_date', 'end_date')
def check_dates(self):
if self.start_date and self.end_date and (
self.start_date > self.end_date):
raise ValidationError(
"Start Date can not be greater than End date, Dude!")
@api.onchange('holiday_type')
def onchange_holiday_type(self):
self.holiday_count = 0.0
self.holiday_days = 0.0
@api.multi
@api.depends('project_id', 'project_id.no_of_days', 'start_date',
'end_date')
def calculate_business_days(self):
for record in self:
if record.start_date and record.end_date:
days_dict = {
0: (1, 2, 3, 4, 5, 6, 7),
1: (2, 3, 4, 5, 6, 7),
2: (3, 4, 5, 6, 7),
3: (4, 5, 6, 7),
4: (5, 6, 7),
5: (6, 7),
6: (7,),
7: (),
}
start = datetime.strptime(record.start_date, "%Y-%m-%d").date()
end = datetime.strptime(record.end_date, "%Y-%m-%d").date()
delta = timedelta(days=1)
days = 0
if record.project_id and end > start:
working_days = record.project_id.no_of_days
non_working_days = days_dict[working_days]
while end != start:
end -= delta
if end.isoweekday() not in non_working_days:
days += 1
record.working_days = days
@api.multi
@api.depends('project_id', 'project_id.no_of_hours', 'productivity_hours')
def calculate_productivity_per(self):
for record in self:
project_id = record.project_id
no_of_hours = project_id.no_of_hours if project_id else 0
prod_hours = record.productivity_hours
if project_id and no_of_hours > 0 and prod_hours:
record.productivity_per = (prod_hours / no_of_hours) * 100
@api.multi
@api.depends('project_id', 'project_id.no_of_hours', 'holiday_count')
def calculate_holiday_days(self):
for record in self:
if record.holiday_type == 'days' and record.project_id:
hours = record.holiday_count * record.project_id.no_of_hours
record.holiday_days = hours
@api.multi
@api.depends('project_id', 'task_line', 'task_line.velocity')
def calculate_estimated_velocity(self):
for record in self:
task_ids = record.env['project.task'].search([
('sprint_id', '=', record.id)
])
total_velocity = sum([
task.velocity for task in task_ids if task.velocity])
record.estimated_velocity = total_velocity
@api.multi
@api.depends('project_id', 'end_date', 'task_line', 'task_line.velocity',
'task_line.stage_id')
def calculate_actual_velocity(self):
for record in self:
task_ids = record.env['project.task'].search([
('sprint_id', '=', record.id),
('actual_end_date', '<=', record.end_date),
])
total_velocity = sum([
task.velocity for task in task_ids if task.velocity])
record.actual_velocity = total_velocity
@api.onchange('duration')
def onchange_start_date(self):
if self.start_date:
end_date = datetime.strptime(
self.start_date, '%Y-%m-%d') + timedelta(days=self.duration)
self.end_date = end_date
@api.onchange('project_id')
def onchange_project(self):
if self.project_id and self.project_id.branch_id:
self.branch_id = self.project_id.branch_id
@api.multi
def check_sprint_state(self):
next_call = datetime.today()
for record in self.search([('state', '!=', 'done')]):
if record.end_date:
end_date = datetime.strptime(
record.end_date, '%Y-%m-%d').date()
if end_date < next_call:
record.state = 'done'
@api.constrains('sprint_planning_line')
def check_users_in_planning_line(self):
user_list = []
for user in self.sprint_planning_line:
if user.user_id.id not in user_list:
user_list.append(user.user_id.id)
else:
raise ValidationError(
"You can't add the same user twice in Sprint Planning!")
@api.model
def create(self, vals):
vals['sprint_seq'] = self.env[
'ir.sequence'].next_by_code('project.sprint')
res = super(ProjectSprint, self).create(vals)
partner_list = []
mail_channel_id = self.env['mail.channel'].sudo().search([
('name', '=', 'Project Sprint')
])
if mail_channel_id:
mail_channel_ids = self.env['mail.followers'].sudo().search([
('channel_id', '=', mail_channel_id.id),
('res_model', '=', res._name),
('res_id', '=', res.id),
])
if not mail_channel_ids:
self.env['mail.followers'].sudo().create({
'channel_id': mail_channel_id.id,
'res_model': res._name,
'res_id': res.id,
})
if 'team_id' in vals:
team_id = self.env['project.team'].browse(vals['team_id'])
partner_list += [member.partner_id.id
for member in team_id.member_ids]
for follower in partner_list:
if follower:
mail_follower_ids = self.env['mail.followers'].sudo().search([
('res_id', '=', res.id),
('partner_id', '=', follower),
('res_model', '=', res._name),
])
if not mail_follower_ids:
self.env['mail.followers'].sudo().create({
'res_id': res.id,
'res_model': res._name,
'partner_id': follower,
'team_id': team_id.id,
})
return res
@api.multi
def write(self, vals):
res = super(ProjectSprint, self).write(vals)
if 'team_id' in vals:
team_id = self.env['project.team'].browse(vals['team_id'])
else:
team_id = self.team_id
delete_team_id = self.env['mail.followers'].sudo().search([
('team_id', '!=', team_id.id),
('res_id', '=', self.id),
])
delete_team_id.unlink()
partner_list = [member.partner_id.id for member in team_id.member_ids]
for follower in partner_list:
if follower:
mail_follower_ids = self.env['mail.followers'].sudo().search([
('res_id', '=', self.id),
('partner_id', '=', follower),
('res_model', '=', self._name),
])
if not mail_follower_ids:
self.env['mail.followers'].sudo().create({
'res_id': self.id,
'res_model': self._name,
'partner_id': follower,
'team_id': team_id.id,
})
return res
@api.multi
def _track_subtype(self, init_values):
self.ensure_one()
if 'state' in init_values and self.state == 'draft':
return 'project_scrum.state_sprint_draft'
elif 'state' in init_values and self.state == 'in_progress':
return 'project_scrum.state_sprint_in_progress'
elif 'state' in init_values and self.state == 'pending':
return 'project_scrum.state_sprint_pending'
elif 'state' in init_values and self.state == 'done':
return 'project_scrum.state_sprint_done'
elif 'state' in init_values and self.state == 'cancel':
return 'project_scrum.state_sprint_cancel'
return super(ProjectSprint, self)._track_subtype(init_values)
class Project(models.Model):
_inherit = "project.project"
@api.multi
def _sprint_count(self):
for project in self:
count = project.env['project.sprint'].search_count([
('project_id', '=', project.id)
])
project.sprint_count = count
sprint_count = fields.Integer(
compute="_sprint_count", string="No. of sprints related to")
@api.multi
def show_sprints(self):
self.ensure_one()
return {
'name': ("Sprints"),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'project.sprint',
'domain': [('project_id', '=', self.id)]
}
no_of_hours = fields.Integer(
related="resource_calendar_id.no_of_hours",
string="Working Hour(s) per Day", store=True)
no_of_days = fields.Integer(
related="resource_calendar_id.no_of_days",
string="Working Day(s) per Week", store=True)
task_type_ids = fields.Many2many(
'project.task.type', 'project_task_type_rel', 'project_id', 'type_id',
string='Project(s)')
class SprintPlanningLine(models.Model):
_name = "sprint.planning.line"
@api.multi
@api.depends('available_per', 'sprint_id.project_id',
'sprint_id.project_id.no_of_hours',
'sprint_id', 'sprint_id.working_days')
def calculate_hours(self):
for record in self:
project_id = record.sprint_id.project_id
no_of_hours = project_id and project_id.no_of_hours or 0.0
calc = (
record.available_per *
record.sprint_id.working_days * no_of_hours) / 100
record.productivity_hours = float(calc)
@api.multi
@api.depends('sprint_id', 'available_per', 'sprint_id.working_days',
'sprint_id.productivity_hours')
def calculate_sprint_hours(self):
for record in self:
hours = (record.available_per * record.sprint_id.working_days *
record.sprint_id.productivity_hours) / 100
record.sprint_hours = hours
@api.multi
@api.depends('sprint_id', 'sprint_id.holiday_count',
'sprint_id.holiday_days', 'sprint_hours', 'off_hours')
def calculate_total_hours(self):
for record in self:
if record.sprint_id.holiday_type == 'hours':
hours = (
record.sprint_hours - record.sprint_id.holiday_count -
record.off_hours)
else:
hours = (
record.sprint_hours - record.sprint_id.holiday_days -
record.off_hours)
record.total_hours = hours
sprint_id = fields.Many2one('project.sprint', string="Sprint")
user_id = fields.Many2one('res.users', string="User")
role_id = fields.Many2one(
related="user_id.role_id", string="Role", store=True)
available_per = fields.Integer(string="Available (%)")
productivity_hours = fields.Float(
compute="calculate_hours", string="Productivity Hour(s)", store=True)
sprint_hours = fields.Float(
compute="calculate_sprint_hours", string="Sprint Hour(s)", store=True)
off_hours = fields.Float(string="Off Hour(s)")
total_hours = fields.Float(
compute="calculate_total_hours", string="Total Hour(s)", store=True)
class UserRole(models.Model):
_name = "user.role"
name = fields.Char(string="Role")
code = fields.Char(string="Code")
class ResUsers(models.Model):
_inherit = "res.users"
role_id = fields.Many2one("user.role", string="User Role")
class ProjectTask(models.Model):
_inherit = "project.task"
sprint_id = fields.Many2one(
'project.sprint', string="Sprint", track_visibility="onchange")
velocity = fields.Integer(string="Velocity", track_visibility="onchange")
story_id = fields.Many2one(
'project.story', string="Story", track_visibility="onchange")
release_planning_id = fields.Many2one(
"release.planning", string="Release Planning",
track_visibility="onchange")
@api.onchange('story_id')
def onchange_story(self):
if self.story_id:
self.description = self.story_id.description
@api.constrains('start_date', 'end_date')
def check_dates(self):
if self.sprint_id:
start_date = self.sprint_id.start_date
end_date = self.sprint_id.end_date
if self.start_date and start_date and (
self.start_date < start_date):
raise ValidationError(
"Start date is not valid according to the Sprint.")
if self.end_date and end_date and (self.end_date > end_date):
raise ValidationError(
"End date is not valid according to the Sprint.")
@api.model
def create(self, vals):
res = super(ProjectTask, self).create(vals)
partner_list = [
member.partner_id.id
for member in res.sprint_id.team_id.member_ids
]
for follower in partner_list:
if follower:
mail_follower_ids = self.env['mail.followers'].sudo().search([
('res_id', '=', res.id),
('partner_id', '=', follower),
('res_model', '=', self._name),
])
if not mail_follower_ids:
self.env['mail.followers'].sudo().create({
'res_id': res.id,
'res_model': self._name,
'partner_id': follower,
'team_id': res.sprint_id.team_id.id,
})
return res
@api.multi
def write(self, vals):
if self.task_seq == '/':
vals['task_seq'] = self.env['ir.sequence'].next_by_code(
'project.task')
res = super(ProjectTask, self).write(vals)
data = []
for record in self:
task_ids = self.search([
('sprint_id', '=', record.sprint_id.id)])
for task in task_ids:
data.append({
'task': task.task_seq or '/',
'velocity': task.velocity or 0,
'per': round(((float(task.velocity) * 100) / float(
record.sprint_id.estimated_velocity)), 2)
if record.sprint_id.estimated_velocity > 0 else 0
})
record.sprint_id.write({'tasks_json': data})
if 'sprint_id' in vals:
sprint_id = self.env['project.sprint'].browse(vals['sprint_id'])
team_id = sprint_id.team_id
else:
team_id = self.sprint_id.team_id
delete_team_id = self.env['mail.followers'].sudo().search([
('team_id', '!=', team_id.id),
('res_id', '=', self.id),
])
delete_team_id.unlink()
partner_list = [member.partner_id.id for member in team_id.member_ids]
for follower in partner_list:
if follower:
mail_follower_ids = self.env['mail.followers'].sudo().search([
('res_id', '=', self.id),
('partner_id', '=', follower),
('res_model', '=', self._name),
])
if not mail_follower_ids:
self.env['mail.followers'].sudo().create({
'res_id': self.id,
'res_model': self._name,
'partner_id': follower,
'team_id': team_id.id,
})
return res
class MailFollowers(models.Model):
_inherit = "mail.followers"
team_id = fields.Many2one("project.team", string="Project Team")
class ResourceCalendar(models.Model):
_inherit = "resource.calendar"
no_of_hours = fields.Integer(string="No. of Hour(s) a Day", default=8)
no_of_days = fields.Integer(string="No. of Day(s) a Week", default=5)
@api.multi
@api.constrains('no_of_hours', 'no_of_days', 'attendance_ids')
def _check_days_hours(self):
week_days = {
0: 'Monday',
1: 'Tuesday',
2: 'Wednesday',
3: 'Thursday',
4: 'Friday',
5: 'Saturday',
6: 'Sunday'
}
for resource in self:
res = {}
current_hours = resource.no_of_hours
current_days = resource.no_of_days
if current_days > 7 or current_days < 1:
raise ValidationError(_(
"No. of Days should be in between 1 - 7."))
for attendance in resource.attendance_ids:
day = attendance.dayofweek
diff = abs(attendance.hour_from - attendance.hour_to)
if day not in res:
res[day] = diff
else:
res[day] += diff
if current_days and len(res) > current_days:
raise ValidationError(_(
"You can not Add Working Hour(s) for more than %s\
different days.") % current_days)
for day, hours in sorted(list(res.items())):
if current_hours and hours > current_hours:
raise ValidationError(_(
"Invalid hours for %s!\n\nWorking hours per day should\
not be greater than %s.") % (
week_days[int(day)], current_hours))