from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from markupsafe import Markup
from datetime import datetime, timedelta
import pytz
class ProjectProject(models.Model):
_inherit = 'project.project'
sequence_name = fields.Char("Project Number", copy=False, readonly=True)
task_sequence_id = fields.Many2one(
'ir.sequence',
string="Task Sequence",
readonly=True,
copy=False,
help="Sequence for tasks of this project"
)
discuss_channel_id = fields.Many2one(
'discuss.channel',
string="Channel",
domain="[('parent_channel_id', '=', default_projects_channel_id)]",
help="Select a channel for project communications. Channels must be sub-channels of the main Projects Channel."
)
default_projects_channel_id = fields.Many2one(
'discuss.channel',
default=lambda self: self._get_default_projects_channel(),
string="Default Projects Channel"
)
project_stages = fields.One2many('project.stages.approval.flow', 'project_id')
assign_approval_flow = fields.Boolean(default=False)
project_sponsor = fields.Many2one('res.users')
show_project_chatter = fields.Boolean(default=False)
project_vision = fields.Text(
string="Project Vision",
help="Concise statement describing the project's ultimate goal and purpose"
)
# Requirement Documentation
description = fields.Html("Requirement Description")
requirement_file = fields.Binary("Requirement Document")
requirement_file_name = fields.Char("Requirement File Name")
# Feasibility Assessment
feasibility_html = fields.Html("Feasibility Assessment")
feasibility_file = fields.Binary("Feasibility Document")
feasibility_file_name = fields.Char("Feasibility File Name")
manager_level_edit_access = fields.Boolean(compute="_compute_has_manager_level_edit_access")
approval_status = fields.Selection([
('submitted', 'Submitted'),
('reject', 'Rejected')
])
show_submission_button = fields.Boolean(compute="_compute_access_check")
show_approval_button = fields.Boolean(compute="_compute_access_check")
show_refuse_button = fields.Boolean(compute="_compute_access_check")
show_back_button = fields.Boolean(compute="_compute_access_check")
project_activity_log = fields.Html(string="Project Activity Log")
project_scope = fields.Html(string="Scope", default=lambda self: """
Scope Description
1. In Scope Items?
2. Out Scope Items?
""")
risk_ids = fields.One2many(
"project.risk",
"project_id",
string="Project Risks"
)
# stage_id = fields.Many2one(domain="[('id','in',showable_stage_ids or [])]")
showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages')
# fields:
estimated_amount = fields.Float(string="Estimated Amount")
total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True)
# Manpower
resource_cost_ids = fields.One2many(
"project.resource.cost",
"project_id",
string="Resource Costs"
)
# Material
material_cost_ids = fields.One2many(
"project.material.cost",
"project_id",
string="Material Costs"
)
# Equipment
equipment_cost_ids = fields.One2many(
"project.equipment.cost",
"project_id",
string="Equipment Costs"
)
architecture_design_ids = fields.One2many(
"project.architecture.design",
"project_id",
string="Architecture & Design"
)
require_sprint = fields.Boolean(
string="Require Sprints?",
default=False,
help="Enable sprint-based planning for this project."
)
sprint_ids = fields.One2many(
"project.sprint",
"project_id",
string="Project Sprints"
)
commit_step_ids = fields.One2many(
'project.commit.step',
'project_id',
string="Commit Steps"
)
development_document_ids = fields.One2many(
"task.development.document",
"project_id",
string="Development Documents"
)
testing_document_ids = fields.One2many(
"task.testing.document",
"project_id",
string="Testing Documents"
)
development_notes = fields.Html()
testing_notes = fields.Html()
deployment_log_ids = fields.One2many(
'project.deployment.log',
'project_id',
string="Deployment Logs"
)
maintenance_support_ids = fields.One2many(
'project.maintenance.support',
'project_id',
string="Maintenance Logs"
)
all_deliverables_submitted = fields.Boolean(string="All Deliverables Submitted")
final_qa_done = fields.Boolean(string="Final QA Completed")
client_signoff_closure = fields.Boolean(string="Client Closure Sign-Off")
billing_completed = fields.Boolean(string="Billing Completed")
training_completed = fields.Boolean(string="Training Completed")
# ------------------------
# B. Closure Documents
# ------------------------
closure_document_ids = fields.One2many(
"project.closure.document",
"project_id",
string="Closure Documents"
)
# ------------------------
# C. Learning & Review
# ------------------------
lessons_learned = fields.Text(string="Lessons Learned")
challenges_faced = fields.Text(string="Challenges Faced")
future_recommendations = fields.Text(string="Future Recommendations")
@api.depends('require_sprint','project_stages','assign_approval_flow')
def _compute_project_project_stages(self):
for rec in self:
project_stages = self.env['project.project.stage'].sudo().search([])
if rec.assign_approval_flow:
project_stages = rec.project_stages.filtered(lambda x: x.activate).stage_id + self.env.ref("project_task_timesheet_extended.project_project_stage_sprint_planning")
stage_ids = self.env['project.project.stage'].sudo().search([('id','in',project_stages.ids),('active', '=', True), ('company_id','in',[self.env.company.id,False])]).ids
if not rec.require_sprint:
stage_ids = self.env['project.project.stage'].sudo().search([
('id', 'in', project_stages.ids),
('active', '=', True),('company_id','in',[self.env.company.id,False]), ('id', '!=', self.env.ref(
"project_task_timesheet_extended.project_project_stage_sprint_planning").id)
]).ids
rec.showable_stage_ids = stage_ids
@api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost")
def _compute_total_budget(self):
for project in self:
project.total_budget_amount = (
sum(project.resource_cost_ids.mapped("total_cost"))
+ sum(project.material_cost_ids.mapped("total_cost"))
+ sum(project.equipment_cost_ids.mapped("total_cost"))
)
def fetch_project_task_stage_users(self):
for project in self:
users_list = list()
if project.assign_approval_flow:
users_list.extend(project.project_stages.involved_users.ids)
else:
users_list.extend(project.showable_stage_ids.user_ids.ids)
if project.project_sponsor:
users_list.append(project.project_sponsor.id)
if project.user_id:
users_list.append(project.user_id.id)
if project.project_lead:
users_list.append(project.project_lead.id)
if project.type_ids:
for task_stage in project.type_ids:
if task_stage.team_id:
users_list.append(task_stage.team_id.team_lead.id)
if task_stage.involved_user_ids:
users_list.extend(task_stage.involved_user_ids.ids)
users_list = list(set(users_list))
base_users = project.members_ids.ids
removed_ids = set(base_users).difference(users_list)
newly_added_ids = set(users_list).difference(base_users)
project.update({
"members_ids": [(6, 0, users_list)],
"message_partner_ids": [(4, user_id.partner_id.id) for user_id in
self.env['res.users'].sudo().search([('id', 'in', list(newly_added_ids))])],
})
project.update({
"message_partner_ids": [(3, user_id.partner_id.id) for user_id in
self.env['res.users'].sudo().search([('id', 'in', list(removed_ids))])],
})
# --------------------------------------------------------
# Fetch Resource Data Button
# --------------------------------------------------------
def action_fetch_resource_data(self):
"""Fetch all members' employee records and create manpower cost lines with full auto-fill."""
for project in self:
# Project users = members + project manager + project lead
users = project.members_ids | project.user_id | project.project_lead
# Fetch employees linked to those users
employees = self.env["hr.employee"].search([("user_id", "in", users.ids)])
for emp in employees:
# Avoid duplicate manpower lines
existing = project.resource_cost_ids.filtered(lambda r: r.employee_id == emp)
if existing:
continue
# Get active contract for salary details
contract = emp.contract_id
monthly_salary = contract.wage if contract else 0.0
daily_rate = (contract.wage / 30) if contract and contract.wage else 0.0
# Project Dates
start_date = project.date_start or fields.Date.today()
end_date = project.date or start_date
# Duration
duration = (end_date - start_date).days + 1 if start_date and end_date else 0
# Total Cost
total_cost = daily_rate * duration if daily_rate else 0
# Create manpower line
self.env["project.resource.cost"].create({
"project_id": project.id,
"employee_id": emp.id,
"monthly_salary": monthly_salary,
"daily_rate": daily_rate,
"start_date": start_date,
"end_date": end_date,
"duration_days": duration,
"total_cost": total_cost,
})
@api.depends("project_stages", "stage_id", "approval_status")
def _compute_access_check(self):
"""Compute visibility of action buttons based on user permissions and project state"""
for project in self:
project.show_submission_button = False
project.show_approval_button = False
project.show_refuse_button = False
project.show_back_button = False
if not project.assign_approval_flow:
continue
user = self.env.user
project_manager = project.user_id
project_sponsor = project.project_sponsor
# Current approval timeline for this stage
current_approval_timeline = project.project_stages.filtered(
lambda s: s.stage_id == project.stage_id
)
# Compute button visibility based on approval flow
if current_approval_timeline:
line = current_approval_timeline[0]
assigned_to = line.assigned_to
responsible_lead = line.approval_by
# Submission button for assigned users
if (assigned_to == user and
project.approval_status != "submitted" and
assigned_to != responsible_lead):
project.show_submission_button = True
# Approval/refusal buttons for responsible leads
if (project.approval_status == "submitted" and
responsible_lead == user):
project.show_approval_button = True
project.show_refuse_button = True
# Direct approval when no assigned user
if not assigned_to and responsible_lead == user:
project.show_approval_button = True
# Direct approval when assigned user is also responsible
if (assigned_to == responsible_lead and
user == assigned_to):
project.show_approval_button = True
else:
# Managers can approve without specific flow
if user.has_group("project.group_project_manager"):
project.show_approval_button = True
# Managers get additional permissions
if user in [project_manager] or user.has_group("project.group_project_manager"):
project.show_back_button = True
if user.has_group("project.group_project_manager"):
project.show_approval_button = True
# Stage-specific button visibility
if project.stage_id:
stages = self.env['project.project.stage'].search([('id','in',project.showable_stage_ids.ids),
('id', '!=', self.env.ref("project.project_project_stage_3").id)
])
if stages:
first_stage = stages.sorted('sequence')[0]
last_stage = stages.sorted('sequence')[-1]
if project.stage_id == first_stage:
project.show_back_button = False
if project.stage_id == last_stage:
project.show_submission_button = False
project.show_approval_button = False
project.show_refuse_button = False
@api.depends("user_id")
def _compute_has_manager_level_edit_access(self):
"""Determine if current user has manager-level edit permissions"""
for rec in self:
rec.manager_level_edit_access = (
rec.user_id == self.env.user or
self.env.user.has_group("project.group_project_manager")
)
def action_show_project_chatter(self):
"""Toggle visibility of project chatter"""
for project in self:
project.show_project_chatter = not project.show_project_chatter
def action_assign_approval_flow(self):
"""Configure approval flow for project stages"""
for project in self:
if not project.project_sponsor or not project.user_id:
raise ValidationError(_("Sponsor and Manager are required to assign Stage Approvals"))
project.assign_approval_flow = not project.assign_approval_flow
if project.assign_approval_flow:
# Clear existing records
project.project_stages.unlink()
# Fetch all project stages
stages = self.env['project.project.stage'].sudo().search([('active', '=', True),
('company_id', 'in', [self.env.company.id, False])])
for stage in stages:
# Determine approval authority based on stage configuration
approval_by = (
project.user_id.id if stage.sudo().approval_by == 'project_manager' else
project.project_sponsor.id if stage.sudo().approval_by == 'project_sponsor' else
False
)
assigned_to = (approval_by if approval_by in stage.user_ids.ids else
min(stage.user_ids, key=lambda u: u.id).id if stage.user_ids else False
)
self.env['project.stages.approval.flow'].sudo().create({
'project_id': project.id,
'stage_id': stage.id,
'approval_by': approval_by,
'assigned_to': assigned_to,
'involved_users': [(6, 0, stage.user_ids.ids)],
'assigned_date': fields.Datetime.now() if stage == project.stage_id else False,
'submission_date': False,
})
# Log approval flow assignment
self.sudo()._add_activity_log("Approval flow assigned by %s" % self.env.user.name)
self.sudo()._post_to_project_channel(
_("Approval flow configured for project %s") % project.name
)
else:
project.sudo().project_stages.unlink()
self.sudo()._add_activity_log("Approval flow removed by %s" % self.env.user.name)
self.sudo()._post_to_project_channel(
_("Approval flow removed for project %s") % project.name
)
def submit_project_for_approval(self):
"""Submit project for current stage approval"""
for project in self:
project.sudo().approval_status = "submitted"
current_stage = project.sudo().stage_id
current_approval_timeline = project.sudo().project_stages.filtered(
lambda s: s.stage_id == project.sudo().stage_id
)
if current_approval_timeline:
current_approval_timeline.sudo().submission_date = fields.Datetime.now()
stage_line = project.sudo().project_stages.filtered(lambda s: s.stage_id == current_stage)
responsible_user = stage_line.sudo().approval_by if stage_line else False
# Create activity log
activity_log = "%s : %s submitted for approval to %s" % (
current_stage.sudo().name,
self.env.user.name,
responsible_user.sudo().name if responsible_user else "N/A"
)
project.sudo()._add_activity_log(activity_log)
# Post to project channel
if responsible_user:
channel_message = _("Project %s submitted for approval at stage %s. %s please review.") % (
project.sudo().name,
current_stage.sudo().name,
project.sudo()._create_odoo_mention(responsible_user.partner_id)
)
else:
channel_message = _("Project %s submitted for approval at stage %s") % (
project.sudo().name,
current_stage.sudo().name
)
project.sudo()._post_to_project_channel(channel_message)
# Send notification
if responsible_user:
project.sudo().message_post(
body=activity_log,
partner_ids=[responsible_user.sudo().partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment'
)
def project_proceed_further(self):
"""Advance project to next stage after approval"""
for project in self:
current_stage = project.stage_id
next_stage = self.env["project.project.stage"].search([
('sequence', '>', project.stage_id.sequence),
('id', '!=', self.env.ref("project.project_project_stage_3").id),
('id', 'in', project.showable_stage_ids.ids),
], order="sequence asc", limit=1)
current_approval_timeline = project.project_stages.filtered(
lambda s: s.stage_id == project.stage_id
)
if current_approval_timeline:
current_approval_timeline.submission_date = fields.Datetime.now()
if not current_approval_timeline.assigned_date:
current_approval_timeline.assigned_date = fields.Datetime.now()
if next_stage:
next_approval_timeline = project.project_stages.filtered(
lambda s: s.stage_id == next_stage
)
if next_approval_timeline and not next_approval_timeline.assigned_date:
next_approval_timeline.assigned_date = fields.Datetime.now()
project.stage_id = next_stage
project.approval_status = ""
# Create activity log
activity_log = "%s approved by %s → moved to %s" % (
current_stage.name,
self.env.user.name,
next_stage.name
)
project._add_activity_log(activity_log)
# Post to project channel
next_user = next_approval_timeline.assigned_to if next_approval_timeline else False
if next_user:
channel_message = _("Project %s approved at stage %s and moved to %s. %s please proceed.") % (
project.name,
current_stage.name,
next_stage.name,
project._create_odoo_mention(next_user.partner_id)
)
else:
channel_message = _("Project %s approved at stage %s and moved to %s") % (
project.name,
current_stage.name,
next_stage.name
)
project._post_to_project_channel(channel_message)
# Send notification
if next_user:
project.message_post(
body=activity_log,
partner_ids=[next_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment'
)
else:
# Last stage completed
project.approval_status = ""
activity_log = "%s fully approved and completed" % project.name
project._add_activity_log(activity_log)
project._post_to_project_channel(
_("Project %s completed and fully approved") % project.name
)
project.message_post(body=activity_log)
def reject_and_return(self, reason=None):
"""Reject project at current stage with optional reason"""
for project in self:
reason = reason or ""
current_stage = project.stage_id
project.approval_status = "reject"
# Create activity log
activity_log = "%s rejected by %s — %s" % (
current_stage.name,
self.env.user.name,
reason
)
project._add_activity_log(activity_log)
# Update approval timeline
current_approval_timeline = project.project_stages.filtered(
lambda s: s.stage_id == project.stage_id
)
if current_approval_timeline:
current_approval_timeline.note = f"Reject Reason: {reason}"
# Post to project channel
channel_message = _("Project %s rejected at stage %s. Reason: %s") % (
project.name,
current_stage.name,
reason
)
project._post_to_project_channel(channel_message)
# Send notification
project.message_post(body=activity_log)
# Notify responsible users
if current_approval_timeline:
responsible_user = (
current_approval_timeline.assigned_to or
current_approval_timeline.approval_by
)
if responsible_user:
project.message_post(
body=_("Project %s has been rejected and returned to you") % project.name,
partner_ids=[responsible_user.partner_id.id],
message_type='notification',
subtype_xmlid='mail.mt_comment'
)
def project_back_button(self):
"""Revert project to previous stage"""
for project in self:
prev_stage = self.env["project.project.stage"].search([
('sequence', '<', project.stage_id.sequence),
('id', 'in', project.showable_stage_ids.ids)
], order="sequence desc", limit=1)
if not prev_stage:
raise ValidationError(_("No previous stage available."))
# Create activity log
activity_log = "%s reverted back to %s by %s" % (
project.stage_id.name,
prev_stage.name,
self.env.user.name
)
project._add_activity_log(activity_log)
# Post to project channel
channel_message = _("Project %s reverted from %s back to %s") % (
project.name,
project.stage_id.name,
prev_stage.name
)
project._post_to_project_channel(channel_message)
# Update stage
project.stage_id = prev_stage
project.message_post(body=activity_log)
def action_open_reject_wizard(self):
"""Open rejection wizard for projects"""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Reject Project"),
"res_model": "project.reject.reason.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_project_id": self.id},
}
# Activity Log Helper Methods
def _get_current_datetime_formatted(self):
"""Get current datetime in 'DD-MMM-YYYY HH:MM AM/PM' format"""
now = fields.Datetime.context_timestamp(self, fields.datetime.now())
formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper()
return formatted_date[1:] if formatted_date.startswith('0') else formatted_date
def _add_activity_log(self, activity_text):
"""Add formatted entry to project activity log"""
formatted_datetime = self._get_current_datetime_formatted()
for project in self:
log_entry = f"[{formatted_datetime}] {activity_text}"
if project.project_activity_log:
project.project_activity_log = Markup(project.project_activity_log) + Markup('
') + Markup(log_entry)
else:
project.project_activity_log = Markup(log_entry)
def _post_to_project_channel(self, message_body, mention_partners=None):
"""Post message to project's discuss channel with proper mentions"""
for project in self:
if not project.id:
continue
# Get project channel
channel = (
project.discuss_channel_id or
project.default_projects_channel_id
)
if channel:
formatted_message = self._format_message_with_odoo_mentions(
message_body,
mention_partners
)
channel.message_post(
body=Markup(formatted_message),
message_type='comment',
subtype_xmlid='mail.mt_comment',
author_id=self.env.user.partner_id.id
)
def _format_message_with_odoo_mentions(self, message_body, mention_partners=None):
"""Format message with proper Odoo @mentions"""
if not mention_partners:
return f'{message_body}
'
message_parts = ['', message_body]
for partner in mention_partners:
if partner and partner.name:
mention_html = f'
@{partner.name}'
message_parts.append(mention_html)
message_parts.append('
')
return ' '.join(message_parts)
def _create_odoo_mention(self, partner):
"""Create Odoo mention link for a partner"""
if not partner:
return ""
return f'@{partner.name}'
@api.model
def _get_default_projects_channel(self):
"""Get or create the default Projects Channel"""
channel = self.env['discuss.channel'].search([
('name', '=', 'Projects Channel'),
('channel_type', '=', 'channel')
], limit=1)
if not channel:
channel = self.env['discuss.channel'].create({
'name': 'Projects Channel',
'description': 'Main channel for all project communications',
'channel_type': 'channel',
})
return channel
def action_create_project_channel(self):
"""Create a new channel for this project under the Projects Channel"""
self.ensure_one()
if self.discuss_channel_id:
raise UserError(_("This project already has a channel assigned."))
# Create new channel
channel_vals = {
'name': self.name,
'description': _("Communication channel for project %s") % self.name,
'channel_type': 'channel',
'parent_channel_id': self.default_projects_channel_id.id,
}
new_channel = self.env['discuss.channel'].create(channel_vals)
self.discuss_channel_id = new_channel.id
# Add project members to the channel
self._add_project_members_to_channel()
return {
'type': 'ir.actions.act_window',
'res_model': 'discuss.channel',
'res_id': new_channel.id,
'view_mode': 'form',
'target': 'current',
'context': {'create': False}
}
def _add_project_members_to_channel(self):
"""Add all project members as followers of the channel"""
if not self.discuss_channel_id:
return
# Get all users related to this project
members_to_add = self.env['res.users']
# Add project members
if self.members_ids:
members_to_add |= self.members_ids
# Add project manager
if self.user_id:
members_to_add |= self.user_id
# Add project lead if exists
if hasattr(self, 'project_lead') and self.project_lead:
members_to_add |= self.project_lead
# Add members to channel
for member in members_to_add:
self.discuss_channel_id.add_members(member.partner_id.ids)
def write(self, vals):
"""Override write to update channel members when project members change"""
result = super().write(vals)
# If members changed, update channel members
if any(field in vals for field in ['members_ids', 'user_id', 'project_lead']):
for project in self:
if project.discuss_channel_id:
project._add_project_members_to_channel()
return result
@api.model
def _get_shared_project_sequence(self):
"""Get or create a shared sequence for all projects"""
sequence = self.env['ir.sequence'].sudo().search([('code', '=', 'project.project.sequence')], limit=1)
if not sequence:
sequence = self.env['ir.sequence'].sudo().create({
'name': _("Project Sequence"),
'implementation': 'no_gap',
'padding': 3,
'use_date_range': False,
'prefix': 'PROJ-',
'code': 'project.project.sequence',
})
return sequence
@api.model_create_multi
def create(self, vals_list):
projects = super().create(vals_list)
sequence = self._get_shared_project_sequence()
for project in projects:
if not project.sequence_name:
project.sequence_name = sequence.next_by_id()
if project.discuss_channel_id:
project._add_project_members_to_channel()
return projects
def _default_type_ids(self):
default_stage_ids = [
self.env.ref('project_task_timesheet_extended.task_type_backlog').id,
self.env.ref('project_task_timesheet_extended.task_type_development').id,
self.env.ref('project_task_timesheet_extended.task_type_code_review_and_merging').id,
self.env.ref('project_task_timesheet_extended.task_type_testing').id,
self.env.ref('project_task_timesheet_extended.task_type_deployment').id,
self.env.ref('project_task_timesheet_extended.task_type_completed').id,
]
# self.env.ref('project_task_timesheet_extended.task_type_cancelled').id,
# self.env.ref('project_task_timesheet_extended.task_type_hold').id,
return self.env['project.task.type'].browse(default_stage_ids)
project_lead = fields.Many2one("res.users", string="Project Lead",
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_lead').user_ids.ids)])
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
'user_id', 'Project Members', help="""Project's
members are users who can have an access to
the tasks related to this project."""
)
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_manager').user_ids.ids),('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
estimated_hours = fields.Float(string="Estimated Hours")
task_estimated_hours = fields.Float(string="Task Estimated Hours", compute="_compute_task_estimated_hours", store=True)
actual_hours = fields.Float(string="Actual Hours", compute="_compute_actual_hours", store=True)
@api.depends('task_ids.estimated_hours')
def _compute_task_estimated_hours(self):
for project in self:
project.task_estimated_hours = sum(project.task_ids.mapped('estimated_hours'))
@api.depends('task_ids.timesheet_ids.unit_amount')
def _compute_actual_hours(self):
for project in self:
project.actual_hours = sum(project.task_ids.timesheet_ids.mapped('unit_amount'))
def add_users(self):
return {
'type': 'ir.actions.act_window',
'name': 'Add Users',
'res_model': 'project.user.assign.wizard',
'view_mode': 'form',
'view_id': self.env.ref('project_task_timesheet_extended.project_user_assignment_form_view').id,
'target': 'new',
'context': {'default_members_ids':[(6, 0, self.members_ids.ids)],
},
}
class ProjectTask(models.Model):
_inherit = 'project.task'
def _default_sprint_id(self):
"""Return the current active (in-progress) sprint of the project."""
if 'project_id' in self._context:
project_id = self._context.get('project_id')
sprint = self.env['project.sprint'].search([
('project_id', '=', project_id),
('status', '=', 'in_progress')
], limit=1)
return sprint.id
return False
sprint_id = fields.Many2one(
"project.sprint",
string="Sprint",
default=_default_sprint_id
)
require_sprint = fields.Boolean(
related="project_id.require_sprint",
store=False
)
commit_step_ids = fields.One2many(
'project.commit.step',
'task_id',
string="Commit Steps"
)
show_task_chatter = fields.Boolean(default=False)
development_document_ids = fields.One2many(
"task.development.document",
"task_id",
string="Development Documents"
)
testing_document_ids = fields.One2many(
"task.testing.document",
"task_id",
string="Testing Documents"
)
@api.onchange("project_id")
def _onchange_project_id_sprint_required(self):
for task in self:
if task.project_id and not task.project_id.require_sprint:
task.sprint_id = False
else:
if task.project_id and task.project_id.require_sprint:
sprint = self.env['project.sprint'].search([
('project_id', '=', task.project_id.id),
('status', '=', 'in_progress')
], limit=1)
task.sprint_id = sprint.id
def action_show_project_task_chatter(self):
"""Toggle visibility of project chatter"""
for project in self:
project.show_task_chatter = not project.show_task_chatter