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

175 lines
7.8 KiB
Python

from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class McsOperatingDashboard(models.Model):
_name = "mcs.operating.dashboard"
_description = "Operating Dashboard"
name = fields.Char(default="CEO Dashboard", required=True)
currency_id = fields.Many2one(
"res.currency", default=lambda self: self.env.company.currency_id
)
month_start = fields.Date(compute="_compute_metrics")
month_end = fields.Date(compute="_compute_metrics")
monthly_revenue_target = fields.Monetary(compute="_compute_metrics")
revenue_closed_month = fields.Monetary(compute="_compute_metrics")
monthly_cash_gap = fields.Monetary(compute="_compute_metrics")
new_mrr_won = fields.Monetary(compute="_compute_metrics")
pipeline_value = fields.Monetary(compute="_compute_metrics")
weighted_pipeline = fields.Monetary(compute="_compute_metrics")
proposals_outstanding = fields.Integer(compute="_compute_metrics")
deals_expected_month = fields.Integer(compute="_compute_metrics")
cash_collected = fields.Monetary(compute="_compute_metrics")
invoices_overdue = fields.Integer(compute="_compute_metrics")
active_clients = fields.Integer(compute="_compute_metrics")
opportunities_created = fields.Integer(compute="_compute_metrics")
opportunities_won = fields.Integer(compute="_compute_metrics")
conversion_rate = fields.Float(compute="_compute_metrics")
followups_due_today = fields.Integer(compute="_compute_metrics")
projects_at_risk = fields.Integer(compute="_compute_metrics")
blockers = fields.Integer(compute="_compute_metrics")
overdue_tasks = fields.Integer(compute="_compute_metrics")
team_capacity_hours = fields.Float(compute="_compute_metrics")
planned_hours = fields.Float(compute="_compute_metrics")
actual_hours = fields.Float(compute="_compute_metrics")
billable_hours = fields.Float(compute="_compute_metrics")
non_billable_hours = fields.Float(compute="_compute_metrics")
utilization = fields.Float(compute="_compute_metrics")
@api.depends_context("uid")
def _compute_metrics(self):
today = fields.Date.context_today(self)
month_start = today.replace(day=1)
month_end = month_start + relativedelta(months=1, days=-1)
Lead = self.env["crm.lead"]
Project = self.env["project.project"]
Task = self.env["project.task"]
Timesheet = self.env["account.analytic.line"]
Invoice = self.env["account.move"]
Employee = self.env["hr.employee"]
open_pipeline = Lead.search(
[("type", "=", "opportunity"), ("active", "=", True), ("probability", "<", 100)]
)
won_month = Lead.search(
[
("type", "=", "opportunity"),
("active", "=", True),
("probability", "=", 100),
("date_closed", ">=", month_start),
("date_closed", "<=", month_end),
]
)
invoices_month = Invoice.search(
[
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("invoice_date", ">=", month_start),
("invoice_date", "<=", month_end),
]
)
overdue_invoices = Invoice.search(
[
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("payment_state", "not in", ["paid", "in_payment", "reversed"]),
("invoice_date_due", "<", today),
]
)
timesheets_month = Timesheet.search(
[("date", ">=", month_start), ("date", "<=", month_end)]
)
active_projects = Project.search([("active", "=", True)])
active_tasks = Task.search([("active", "=", True)])
employees = Employee.search([("active", "=", True)])
for dashboard in self:
dashboard.month_start = month_start
dashboard.month_end = month_end
dashboard.monthly_revenue_target = sum(
active_projects.mapped("mcs_monthly_revenue_target")
)
dashboard.revenue_closed_month = sum(won_month.mapped("expected_revenue"))
dashboard.monthly_cash_gap = (
dashboard.monthly_revenue_target - dashboard.revenue_closed_month
)
dashboard.new_mrr_won = sum(won_month.mapped("mcs_expected_monthly_revenue"))
dashboard.pipeline_value = sum(open_pipeline.mapped("expected_revenue"))
dashboard.weighted_pipeline = sum(
lead.expected_revenue * lead.probability / 100 for lead in open_pipeline
)
dashboard.proposals_outstanding = Lead.search_count(
[
("type", "=", "opportunity"),
("active", "=", True),
("mcs_proposal_sent_date", "!=", False),
("probability", "<", 100),
]
)
dashboard.deals_expected_month = Lead.search_count(
[
("type", "=", "opportunity"),
("active", "=", True),
("date_deadline", ">=", month_start),
("date_deadline", "<=", month_end),
("probability", "<", 100),
]
)
dashboard.cash_collected = sum(
invoice.amount_total - invoice.amount_residual for invoice in invoices_month
)
dashboard.invoices_overdue = len(overdue_invoices)
dashboard.active_clients = len(set(active_projects.mapped("partner_id").ids))
created = Lead.search_count(
[
("type", "=", "opportunity"),
("create_date", ">=", month_start),
("create_date", "<=", month_end),
]
)
dashboard.opportunities_created = created
dashboard.opportunities_won = len(won_month)
dashboard.conversion_rate = len(won_month) / created * 100 if created else 0
dashboard.followups_due_today = self.env["mail.activity"].search_count(
[
("user_id", "=", self.env.user.id),
("date_deadline", "<=", today),
("res_model", "=", "crm.lead"),
]
)
dashboard.projects_at_risk = len(active_projects.filtered("mcs_at_risk"))
dashboard.blockers = len(active_tasks.filtered("mcs_is_blocked"))
dashboard.overdue_tasks = len(
active_tasks.filtered(
lambda task: task.date_deadline
and fields.Date.to_date(task.date_deadline) < today
and not (task.stage_id and task.stage_id.fold)
)
)
dashboard.team_capacity_hours = sum(employees.mapped("mcs_available_hours_week"))
dashboard.planned_hours = sum(active_tasks.mapped("allocated_hours"))
dashboard.actual_hours = sum(timesheets_month.mapped("unit_amount"))
dashboard.billable_hours = sum(
timesheets_month.filtered(
lambda line: line.mcs_billable_classification == "billable"
).mapped("unit_amount")
)
dashboard.non_billable_hours = dashboard.actual_hours - dashboard.billable_hours
dashboard.utilization = (
dashboard.billable_hours / dashboard.team_capacity_hours * 100
if dashboard.team_capacity_hours
else 0
)
def action_refresh(self):
return {
"type": "ir.actions.act_window",
"name": "CEO Dashboard",
"res_model": "mcs.operating.dashboard",
"view_mode": "form",
"target": "current",
"context": {"create": False},
}