2026-08-30 00:48:25 -04:00

147 lines
5.3 KiB
Python

from odoo import api, fields, models
WORK_CATEGORY_SELECTION = [
("billable_delivery", "Billable Delivery"),
("internal_product", "Internal Product"),
("sales_support", "Sales Support"),
("rework", "Rework"),
("client_support", "Client Support"),
("admin", "Admin"),
("meetings", "Meetings"),
("qa", "QA"),
("research", "Research"),
]
class ProjectProject(models.Model):
_inherit = "project.project"
mcs_initiative_type = fields.Selection(
[
("client", "Client Engagement"),
("internal", "Internal Initiative"),
("product", "Product Development"),
("sales", "Sales Execution"),
("parking_lot", "Idea Parking Lot"),
],
string="Initiative Type",
tracking=True,
)
mcs_classification = fields.Selection(
[
("finish_now", "Finish Now"),
("active", "Active"),
("parked", "Parked"),
("kill", "Kill"),
],
string="Operating Classification",
default="active",
tracking=True,
)
mcs_revenue_connected = fields.Boolean(string="Connected to Revenue", tracking=True)
mcs_monthly_revenue_target = fields.Monetary(string="Monthly Revenue Target")
mcs_contracted_revenue = fields.Monetary(string="Contracted Revenue")
mcs_cash_gap = fields.Monetary(string="Cash Gap", compute="_compute_mcs_cash_gap")
mcs_planned_hours = fields.Float(
string="Planned Hours", compute="_compute_mcs_project_hours", store=True
)
mcs_actual_hours = fields.Float(
string="Actual Hours", compute="_compute_mcs_project_hours", store=True
)
mcs_utilization = fields.Float(
string="Planned vs Actual %", compute="_compute_mcs_project_hours", store=True
)
mcs_next_action = fields.Char(string="Next Action", tracking=True)
mcs_blocker = fields.Text(string="Blocker", tracking=True)
mcs_at_risk = fields.Boolean(
string="At Risk", compute="_compute_mcs_at_risk", store=True
)
@api.depends("mcs_monthly_revenue_target", "mcs_contracted_revenue")
def _compute_mcs_cash_gap(self):
for project in self:
project.mcs_cash_gap = (
project.mcs_monthly_revenue_target - project.mcs_contracted_revenue
)
@api.depends("tasks.allocated_hours", "tasks.effective_hours")
def _compute_mcs_project_hours(self):
for project in self:
planned = sum(project.tasks.mapped("allocated_hours"))
actual = sum(project.tasks.mapped("effective_hours"))
project.mcs_planned_hours = planned
project.mcs_actual_hours = actual
project.mcs_utilization = actual / planned * 100 if planned else 0
@api.depends("tasks.mcs_at_risk", "mcs_blocker", "mcs_next_action")
def _compute_mcs_at_risk(self):
for project in self:
project.mcs_at_risk = bool(
project.mcs_blocker or not project.mcs_next_action or any(project.tasks.mapped("mcs_at_risk"))
)
class ProjectTask(models.Model):
_inherit = "project.task"
mcs_priority = fields.Selection(
[
("p0", "P0 - Critical"),
("p1", "P1 - High"),
("p2", "P2 - Normal"),
("p3", "P3 - Low"),
],
string="Operating Priority",
default="p2",
tracking=True,
)
mcs_definition_of_done = fields.Html(string="Definition of Done")
mcs_next_action = fields.Char(string="Next Action", tracking=True)
mcs_blocker = fields.Text(string="Blocker", tracking=True)
mcs_client_internal = fields.Selection(
[("client", "Client"), ("internal", "Internal")],
string="Client / Internal",
default="client",
tracking=True,
)
mcs_work_category = fields.Selection(WORK_CATEGORY_SELECTION, string="Work Category")
mcs_is_blocked = fields.Boolean(string="Blocked", compute="_compute_mcs_flags", store=True)
mcs_at_risk = fields.Boolean(string="At Risk", compute="_compute_mcs_flags", store=True)
mcs_missing_operating_data = fields.Boolean(
string="Missing Operating Data", compute="_compute_mcs_flags", store=True
)
@api.depends(
"user_ids",
"date_deadline",
"allocated_hours",
"mcs_definition_of_done",
"mcs_next_action",
"mcs_blocker",
"depend_on_ids",
"stage_id.fold",
)
def _compute_mcs_flags(self):
today = fields.Date.context_today(self)
for task in self:
is_done = bool(task.stage_id and task.stage_id.fold)
has_deadline_passed = bool(
task.date_deadline and fields.Date.to_date(task.date_deadline) < today
)
task.mcs_is_blocked = bool(task.mcs_blocker or task.depend_on_ids)
task.mcs_missing_operating_data = bool(
not is_done
and (
not task.user_ids
or not task.date_deadline
or not task.allocated_hours
or not task.mcs_definition_of_done
or not task.mcs_next_action
)
)
task.mcs_at_risk = bool(
not is_done
and (task.mcs_is_blocked or has_deadline_passed or task.mcs_missing_operating_data)
)