2026-08-31 11:01:49 -04:00

805 lines
36 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},
}
@api.model
def action_mcs_seed_v2_operating_data(self):
from odoo.addons.mcs_operating_system.hooks import OperatingSystemSetup
OperatingSystemSetup(self.env).run()
return True
@api.model
def get_ceo_dashboard_v2_data(self, selected_company_ids=None):
today = fields.Date.context_today(self)
month_start = today.replace(day=1)
month_end = month_start + relativedelta(months=1, days=-1)
week_start = today - relativedelta(days=today.weekday())
week_end = week_start + relativedelta(days=6)
company_ids = self._dashboard_company_ids(selected_company_ids)
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"]
Partner = self.env["res.partner"]
CeoAction = self.env["mcs.ceo.action"]
active_projects = Project.search(
[("active", "=", True)]
+ self._company_domain(Project, company_ids, include_shared=False)
)
client_projects = active_projects.filtered(
lambda project: project.mcs_delivery_type
not in ("internal_product", "internal_r_and_d", "sales_initiative")
)
internal_projects = active_projects.filtered(
lambda project: project.mcs_delivery_type
in ("internal_product", "internal_r_and_d")
)
open_pipeline = Lead.search(
[
("type", "=", "opportunity"),
("active", "=", True),
("probability", "<", 100),
]
+ self._company_domain(Lead, company_ids, include_shared=True)
)
won_month = Lead.search(
[
("type", "=", "opportunity"),
("active", "=", True),
("probability", "=", 100),
("date_closed", ">=", month_start),
("date_closed", "<=", month_end),
]
+ self._company_domain(Lead, company_ids, include_shared=True)
)
invoices_month = Invoice.search(
[
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("invoice_date", ">=", month_start),
("invoice_date", "<=", month_end),
]
+ self._company_domain(Invoice, company_ids, include_shared=False)
)
receivables = Invoice.search(
[
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("payment_state", "not in", ["paid", "in_payment", "reversed"]),
]
+ self._company_domain(Invoice, company_ids, include_shared=False)
)
posted_invoices = Invoice.search(
[("move_type", "=", "out_invoice"), ("state", "=", "posted")]
+ self._company_domain(Invoice, company_ids, include_shared=False)
)
overdue_receivables = receivables.filtered(
lambda invoice: invoice.invoice_date_due and invoice.invoice_date_due < today
)
timesheets_month = Timesheet.search(
[("date", ">=", month_start), ("date", "<=", month_end)]
+ self._company_domain(Timesheet, company_ids, include_shared=True)
)
timesheets_week = Timesheet.search(
[("date", ">=", week_start), ("date", "<=", week_end)]
+ self._company_domain(Timesheet, company_ids, include_shared=True)
)
active_employees = Employee.search(
[("active", "=", True)]
+ self._company_domain(Employee, company_ids, include_shared=True)
)
active_tasks = Task.search(
[("active", "=", True)]
+ self._company_domain(Task, company_ids, include_shared=False)
)
monthly_target = sum(active_projects.mapped("mcs_monthly_revenue_target"))
contracted_revenue = sum(client_projects.mapped("mcs_contracted_revenue"))
invoiced_month = sum(invoices_month.mapped("amount_total"))
cash_collected = sum(
invoice.amount_total - invoice.amount_residual for invoice in invoices_month
)
operating_cost = self._monthly_operating_cost(company_ids)
revenue_gap = max(monthly_target - contracted_revenue, 0)
open_pipeline_value = sum(open_pipeline.mapped("expected_revenue"))
weighted_pipeline = sum(
lead.expected_revenue * lead.probability / 100 for lead in open_pipeline
)
qualified_stage_names = ["Qualified", "Proposal", "Negotiation"]
qualified_pipeline = sum(
open_pipeline.filtered(
lambda lead: lead.stage_id.name in qualified_stage_names
).mapped("expected_revenue")
)
required_pipeline = revenue_gap * 3
pipeline_coverage = (
qualified_pipeline / required_pipeline * 100 if required_pipeline else 100
)
revenue_coverage = (
contracted_revenue / operating_cost * 100 if operating_cost else 0
)
remaining_gap = max(operating_cost - contracted_revenue, 0)
utilization_capacity = sum(active_employees.mapped("mcs_available_hours_week"))
billable_hours = sum(
timesheets_week.filtered(
lambda line: line.mcs_billable_classification == "billable"
).mapped("unit_amount")
)
total_week_hours = sum(timesheets_week.mapped("unit_amount"))
billable_utilization = (
billable_hours / utilization_capacity * 100 if utilization_capacity else 0
)
utilization = (
total_week_hours / utilization_capacity * 100 if utilization_capacity else 0
)
return {
"period": {
"today": fields.Date.to_string(today),
"month_start": fields.Date.to_string(month_start),
"month_end": fields.Date.to_string(month_end),
"week_start": fields.Date.to_string(week_start),
"week_end": fields.Date.to_string(week_end),
"company_ids": company_ids,
"companies": self.env["res.company"].browse(company_ids).mapped("name"),
"company_options": [
{"id": company.id, "name": company.name}
for company in self.env["res.company"].browse(
self._dashboard_company_ids()
)
],
},
"company_pulse": {
"revenue_coverage": self._format_percent(revenue_coverage),
"revenue_coverage_status": self._coverage_status(revenue_coverage),
"remaining_gap": remaining_gap,
"monthly_revenue_target": monthly_target,
"contracted_monthly_revenue": contracted_revenue,
"revenue_invoiced_month": invoiced_month,
"cash_collected_month": cash_collected,
"operating_cost": operating_cost,
"revenue_gap": revenue_gap,
"new_mrr_won": sum(won_month.mapped("mcs_expected_monthly_revenue")),
"open_pipeline": open_pipeline_value,
"weighted_pipeline": weighted_pipeline,
"outstanding_receivables": sum(receivables.mapped("amount_residual")),
"overdue_receivables": sum(overdue_receivables.mapped("amount_residual")),
"team_utilization": self._format_percent(utilization),
"billable_utilization": self._format_percent(billable_utilization),
"projects_at_risk": len(active_projects.filtered(lambda project: project.mcs_portfolio_health == "at_risk")),
"blocked_projects": len(active_projects.filtered(lambda project: project.mcs_portfolio_health == "blocked")),
},
"kpis": self._dashboard_kpis(
monthly_target,
contracted_revenue,
revenue_gap,
open_pipeline_value,
weighted_pipeline,
overdue_receivables,
utilization,
active_projects,
),
"revenue": {
"target": monthly_target,
"contracted": contracted_revenue,
"invoiced": invoiced_month,
"collected": cash_collected,
"forecast": contracted_revenue + weighted_pipeline,
"qualified_pipeline": qualified_pipeline,
"required_pipeline": required_pipeline,
"pipeline_coverage": self._format_percent(pipeline_coverage),
"pipeline_coverage_status": self._coverage_status(pipeline_coverage),
"funnel": self._crm_funnel_data(open_pipeline),
},
"portfolio": self._portfolio_rows(Partner, client_projects, posted_invoices),
"project_health": self._project_health_distribution(active_projects),
"capacity": self._capacity_rows(active_employees, active_tasks, timesheets_week),
"time_distribution": self._time_distribution(timesheets_week),
"allocation_guardrail": self._allocation_guardrail(timesheets_week),
"ceo_actions": self._ceo_action_rows(CeoAction),
"internal_bets": self._internal_bet_rows(internal_projects, timesheets_month),
"actions": self._dashboard_actions(
today, month_start, month_end, week_start, week_end, company_ids
),
}
def _format_percent(self, value):
return round(value or 0, 1)
def _dashboard_company_ids(self, selected_company_ids=None):
allowed_company_ids = (
self.env.context.get("allowed_company_ids") or self.env.companies.ids
)
allowed_company_ids = allowed_company_ids or [self.env.company.id]
if not selected_company_ids:
return allowed_company_ids
selected_company_ids = [int(company_id) for company_id in selected_company_ids]
selected_company_ids = [
company_id
for company_id in selected_company_ids
if company_id in allowed_company_ids
]
return selected_company_ids or allowed_company_ids
def _company_domain(self, Model, company_ids, include_shared=True):
if "company_id" not in Model._fields:
return []
if include_shared:
return ["|", ("company_id", "=", False), ("company_id", "in", company_ids)]
return [("company_id", "in", company_ids)]
def _monthly_operating_cost(self, company_ids=None):
try:
Contract = self.env["hr.contract"]
except KeyError:
return 0
company_ids = company_ids or self._dashboard_company_ids()
contracts = Contract.sudo().search(
[("state", "=", "open"), ("employee_id.active", "=", True)]
+ self._company_domain(Contract, company_ids, include_shared=True)
)
return sum(contracts.mapped("wage"))
def _coverage_status(self, value):
if value >= 100:
return "covered"
if value >= 80:
return "watch"
return "critical"
def _dashboard_kpis(
self,
monthly_target,
contracted_revenue,
revenue_gap,
open_pipeline_value,
weighted_pipeline,
overdue_receivables,
utilization,
active_projects,
):
return [
{
"label": "Revenue Target",
"value": monthly_target,
"action_key": "monthly_revenue_projects",
},
{
"label": "Contracted Revenue",
"value": contracted_revenue,
"action_key": "contracted_projects",
},
{"label": "Revenue Gap", "value": revenue_gap, "action_key": "pipeline"},
{"label": "Open Pipeline", "value": open_pipeline_value, "action_key": "pipeline"},
{
"label": "Weighted Pipeline",
"value": weighted_pipeline,
"action_key": "pipeline",
},
{
"label": "Overdue Receivables",
"value": sum(overdue_receivables.mapped("amount_residual")),
"action_key": "overdue_receivables",
},
{
"label": "Team Utilization",
"value": "%s%%" % self._format_percent(utilization),
"action_key": "capacity",
},
{
"label": "At Risk Projects",
"value": len(active_projects.filtered(lambda project: project.mcs_portfolio_health == "at_risk")),
"action_key": "projects_at_risk",
},
]
def _crm_funnel_data(self, open_pipeline):
stage_names = [
"Target",
"Contacted",
"Replied",
"Discovery",
"Qualified",
"Proposal",
"Negotiation",
"Won",
]
rows = []
for stage_name in stage_names:
leads = open_pipeline.filtered(lambda lead: lead.stage_id.name == stage_name)
rows.append(
{
"stage": stage_name,
"count": len(leads),
"value": sum(leads.mapped("expected_revenue")),
}
)
return rows
def _portfolio_rows(self, Partner, client_projects, posted_invoices):
partners = (
Partner.search([("mcs_is_client_account", "=", True)])
| client_projects.mapped("partner_id")
| posted_invoices.mapped("commercial_partner_id")
)
rows = []
for partner in partners:
projects = client_projects.filtered(lambda project: project.partner_id == partner)
invoices = posted_invoices.filtered(
lambda invoice: invoice.commercial_partner_id == partner.commercial_partner_id
)
unpaid_invoices = invoices.filtered(
lambda invoice: invoice.payment_state
not in ("paid", "in_payment", "reversed")
)
overdue_invoices = unpaid_invoices.filtered(
lambda invoice: invoice.invoice_date_due
and invoice.invoice_date_due < fields.Date.context_today(self)
)
if partner.parent_id and not projects:
continue
if (
not projects
and not invoices
):
continue
currencies = invoices.mapped("currency_id") or partner.mcs_currency_id
currency_name = (
currencies[0].name
if len(currencies) == 1
else "Mixed"
)
invoice_dates = [date for date in invoices.mapped("invoice_date") if date]
rows.append(
{
"id": partner.id,
"name": partner.display_name,
"currency": currency_name,
"health": partner.mcs_account_health,
"active_workstreams": len(projects),
"revenue": sum(projects.mapped("mcs_monthly_revenue_target")),
"total_invoiced": sum(invoices.mapped("amount_total_signed")),
"outstanding": sum(unpaid_invoices.mapped("amount_residual_signed")),
"overdue": sum(overdue_invoices.mapped("amount_residual_signed")),
"last_invoice_date": fields.Date.to_string(max(invoice_dates))
if invoice_dates
else "",
"hours": sum(projects.mapped("mcs_actual_hours")),
"next_milestone": next(
(project.mcs_next_milestone for project in projects if project.mcs_next_milestone),
"",
),
"nearest_deadline": self._nearest_deadline(projects),
"risk": max(projects.mapped("mcs_risk_score") or [0]),
"next_action": partner.mcs_next_account_action
or next((project.mcs_next_action for project in projects if project.mcs_next_action), ""),
}
)
return sorted(rows, key=lambda row: row["risk"], reverse=True)
def _nearest_deadline(self, projects):
deadlines = projects.mapped("tasks.date_deadline")
deadlines = [deadline for deadline in deadlines if deadline]
return fields.Date.to_string(min(deadlines)) if deadlines else ""
def _project_health_distribution(self, projects):
return [
{"label": "Healthy", "value": len(projects.filtered(lambda project: project.mcs_portfolio_health == "healthy"))},
{"label": "Watch", "value": len(projects.filtered(lambda project: project.mcs_portfolio_health == "watch"))},
{"label": "At Risk", "value": len(projects.filtered(lambda project: project.mcs_portfolio_health == "at_risk"))},
{"label": "Blocked", "value": len(projects.filtered(lambda project: project.mcs_portfolio_health == "blocked"))},
{"label": "Stale", "value": len(projects.filtered("mcs_stale"))},
{"label": "No Next Action", "value": len(projects.filtered(lambda project: not project.mcs_next_action))},
{"label": "Missing Planned Hours", "value": len(projects.filtered(lambda project: not project.mcs_planned_hours))},
{"label": "Over Planned Hours", "value": len(projects.filtered(lambda project: project.mcs_planned_hours and project.mcs_actual_hours > project.mcs_planned_hours))},
]
def _capacity_rows(self, employees, active_tasks, timesheets_week):
rows = []
for employee in employees:
employee_lines = timesheets_week.filtered(lambda line: line.employee_id == employee)
employee_tasks = active_tasks.filtered(
lambda task: employee.user_id and employee.user_id in task.user_ids
)
available = employee.mcs_available_hours_week
actual = sum(employee_lines.mapped("unit_amount"))
billable = sum(
employee_lines.filtered(
lambda line: line.mcs_billable_classification == "billable"
).mapped("unit_amount")
)
utilization = actual / available * 100 if available else 0
billable_utilization = billable / available * 100 if available else 0
rows.append(
{
"id": employee.id,
"name": employee.name,
"available": available,
"allocated": sum(employee_tasks.mapped("allocated_hours")),
"actual": actual,
"billable": billable,
"internal": sum(
employee_lines.filtered(
lambda line: line.mcs_billable_classification == "internal"
).mapped("unit_amount")
),
"rework": sum(employee_lines.filtered("mcs_is_rework").mapped("unit_amount")),
"utilization": self._format_percent(utilization),
"billable_utilization": self._format_percent(billable_utilization),
"status": self._capacity_status(utilization),
}
)
return sorted(rows, key=lambda row: row["utilization"], reverse=True)
def _capacity_status(self, utilization):
if utilization > 100:
return "overloaded"
if utilization >= 90:
return "near_capacity"
if utilization >= 50:
return "normal"
return "underutilized"
def _time_distribution(self, timesheets):
category_labels = dict(self.env["account.analytic.line"]._fields["mcs_work_category"].selection)
billable_labels = dict(self.env["account.analytic.line"]._fields["mcs_billable_classification"].selection)
by_category = []
by_billable = []
for key, label in category_labels.items():
by_category.append(
{
"label": label,
"hours": sum(timesheets.filtered(lambda line: line.mcs_work_category == key).mapped("unit_amount")),
}
)
for key, label in billable_labels.items():
by_billable.append(
{
"label": label,
"hours": sum(timesheets.filtered(lambda line: line.mcs_billable_classification == key).mapped("unit_amount")),
}
)
return {"by_category": by_category, "by_billable": by_billable}
def _allocation_guardrail(self, timesheets):
total = sum(timesheets.mapped("unit_amount"))
groups = [
("Client Delivery", ["billable_delivery", "client_support"], 75),
("Sales Support", ["sales_support"], 10),
("Internal Product/R&D", ["internal_product", "research"], 10),
("Admin/Other", ["admin", "meetings", "qa", "rework"], 5),
]
rows = []
for label, categories, target in groups:
hours = sum(
timesheets.filtered(
lambda line, categories=categories: line.mcs_work_category in categories
).mapped("unit_amount")
)
rows.append(
{
"label": label,
"hours": hours,
"percentage": self._format_percent(hours / total * 100 if total else 0),
"target": target,
}
)
return rows
def _ceo_action_rows(self, CeoAction):
actions = CeoAction.search(
[("state", "in", ["open", "waiting", "blocked"])],
order="priority_rank, date_due, id desc",
limit=12,
)
return [
{
"id": action.id,
"title": action.title,
"category": action.category,
"priority": action.priority,
"state": action.state,
"date_due": fields.Date.to_string(action.date_due) if action.date_due else "",
"next_action": action.next_action or "",
}
for action in actions
]
def _internal_bet_rows(self, projects, timesheets_month):
rows = []
for project in projects:
project_lines = timesheets_month.filtered(lambda line: line.project_id == project)
hours = sum(project_lines.mapped("unit_amount"))
rows.append(
{
"id": project.id,
"name": project.name,
"stage": project.mcs_internal_product_stage,
"hours": hours,
"estimated_cost": 0,
"expected_revenue": project.mcs_internal_expected_revenue,
"actual_revenue": project.mcs_internal_actual_revenue,
"owner": project.user_id.name or "",
"next_milestone": project.mcs_next_milestone or "",
"next_decision_date": fields.Date.to_string(project.mcs_next_decision_date)
if project.mcs_next_decision_date
else "",
}
)
return rows
def _dashboard_actions(self, today, month_start, month_end, week_start, week_end, company_ids):
Project = self.env["project.project"]
Lead = self.env["crm.lead"]
Invoice = self.env["account.move"]
Timesheet = self.env["account.analytic.line"]
return {
"monthly_revenue_projects": {
"type": "ir.actions.act_window",
"name": "Monthly Revenue Projects",
"res_model": "project.project",
"view_mode": "tree,form,kanban",
"views": self._action_views("tree", "form", "kanban"),
"domain": [("active", "=", True), ("mcs_monthly_revenue_target", ">", 0)]
+ self._company_domain(Project, company_ids, include_shared=False),
},
"contracted_projects": {
"type": "ir.actions.act_window",
"name": "Contracted Projects",
"res_model": "project.project",
"view_mode": "tree,form,kanban",
"views": self._action_views("tree", "form", "kanban"),
"domain": [("active", "=", True), ("mcs_contracted_revenue", ">", 0)]
+ self._company_domain(Project, company_ids, include_shared=False),
},
"pipeline": {
"type": "ir.actions.act_window",
"name": "Open Pipeline",
"res_model": "crm.lead",
"view_mode": "kanban,tree,form,pivot,graph",
"views": self._action_views("kanban", "tree", "form", "pivot", "graph"),
"domain": [
("type", "=", "opportunity"),
("active", "=", True),
("probability", "<", 100),
]
+ self._company_domain(Lead, company_ids, include_shared=True),
},
"overdue_receivables": {
"type": "ir.actions.act_window",
"name": "Overdue Receivables",
"res_model": "account.move",
"view_mode": "tree,form",
"views": self._action_views("tree", "form"),
"domain": [
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("payment_state", "not in", ["paid", "in_payment", "reversed"]),
("invoice_date_due", "<", fields.Date.to_string(today)),
]
+ self._company_domain(Invoice, company_ids, include_shared=False),
},
"capacity": {
"type": "ir.actions.act_window",
"name": "This Week Timesheets",
"res_model": "account.analytic.line",
"view_mode": "tree,pivot,graph,form",
"views": self._action_views("tree", "pivot", "graph", "form"),
"domain": [
("date", ">=", fields.Date.to_string(week_start)),
("date", "<=", fields.Date.to_string(week_end)),
]
+ self._company_domain(Timesheet, company_ids, include_shared=True),
"context": {"group_by": "employee_id"},
},
"projects_at_risk": {
"type": "ir.actions.act_window",
"name": "Projects At Risk",
"res_model": "project.project",
"view_mode": "tree,form,kanban",
"views": self._action_views("tree", "form", "kanban"),
"domain": [
("active", "=", True),
("mcs_portfolio_health", "in", ["at_risk", "blocked"]),
]
+ self._company_domain(Project, company_ids, include_shared=False),
},
"ceo_actions": {
"type": "ir.actions.act_window",
"name": "CEO Actions",
"res_model": "mcs.ceo.action",
"view_mode": "tree,form,kanban",
"views": self._action_views("tree", "form", "kanban"),
"domain": [("state", "in", ["open", "waiting", "blocked"])],
},
"internal_bets": {
"type": "ir.actions.act_window",
"name": "Internal Bets",
"res_model": "project.project",
"view_mode": "tree,form,kanban",
"views": self._action_views("tree", "form", "kanban"),
"domain": [("mcs_delivery_type", "in", ["internal_product", "internal_r_and_d"])]
+ self._company_domain(Project, company_ids, include_shared=False),
},
}
def _action_views(self, *view_types):
return [[False, view_type] for view_type in view_types]