175 lines
7.8 KiB
Python
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},
|
|
}
|