diff --git a/addons/mcs_operating_system/models/dashboard.py b/addons/mcs_operating_system/models/dashboard.py index 7a084fa..4423cb4 100644 --- a/addons/mcs_operating_system/models/dashboard.py +++ b/addons/mcs_operating_system/models/dashboard.py @@ -187,6 +187,7 @@ class McsOperatingDashboard(models.Model): 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() Lead = self.env["crm.lead"] Project = self.env["project.project"] @@ -197,7 +198,10 @@ class McsOperatingDashboard(models.Model): Partner = self.env["res.partner"] CeoAction = self.env["mcs.ceo.action"] - active_projects = Project.search([("active", "=", True)]) + active_projects = Project.search( + [("active", "=", True)] + + self._company_domain(Project, company_ids, include_shared=True) + ) client_projects = active_projects.filtered( lambda project: project.mcs_delivery_type not in ("internal_product", "internal_r_and_d", "sales_initiative") @@ -207,7 +211,12 @@ class McsOperatingDashboard(models.Model): in ("internal_product", "internal_r_and_d") ) open_pipeline = Lead.search( - [("type", "=", "opportunity"), ("active", "=", True), ("probability", "<", 100)] + [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("probability", "<", 100), + ] + + self._company_domain(Lead, company_ids, include_shared=True) ) won_month = Lead.search( [ @@ -217,6 +226,7 @@ class McsOperatingDashboard(models.Model): ("date_closed", ">=", month_start), ("date_closed", "<=", month_end), ] + + self._company_domain(Lead, company_ids, include_shared=True) ) invoices_month = Invoice.search( [ @@ -225,6 +235,7 @@ class McsOperatingDashboard(models.Model): ("invoice_date", ">=", month_start), ("invoice_date", "<=", month_end), ] + + self._company_domain(Invoice, company_ids, include_shared=False) ) receivables = Invoice.search( [ @@ -232,18 +243,31 @@ class McsOperatingDashboard(models.Model): ("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=True) ) - active_employees = Employee.search([("active", "=", True)]) - active_tasks = Task.search([("active", "=", True)]) monthly_target = sum(active_projects.mapped("mcs_monthly_revenue_target")) contracted_revenue = sum(client_projects.mapped("mcs_contracted_revenue")) @@ -293,6 +317,8 @@ class McsOperatingDashboard(models.Model): "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_pulse": { "revenue_coverage": self._format_percent(revenue_coverage), @@ -336,19 +362,32 @@ class McsOperatingDashboard(models.Model): "pipeline_coverage_status": self._coverage_status(pipeline_coverage), "funnel": self._crm_funnel_data(open_pipeline), }, - "portfolio": self._portfolio_rows(Partner, client_projects), + "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), + "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): + company_ids = self.env.context.get("allowed_company_ids") or self.env.companies.ids + return company_ids or [self.env.company.id] + + 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): try: Contract = self.env["hr.contract"] @@ -356,6 +395,9 @@ class McsOperatingDashboard(models.Model): return 0 contracts = Contract.sudo().search( [("state", "=", "open"), ("employee_id.active", "=", True)] + + self._company_domain( + Contract, self._dashboard_company_ids(), include_shared=True + ) ) return sum(contracts.mapped("wage")) @@ -435,32 +477,53 @@ class McsOperatingDashboard(models.Model): ) return rows - def _portfolio_rows(self, Partner, client_projects): - partners = Partner.search([("mcs_is_client_account", "=", True)]) + 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 partner.mcs_total_invoiced - and not partner.mcs_total_outstanding + 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": partner.mcs_currency_id.name or "", + "currency": currency_name, "health": partner.mcs_account_health, "active_workstreams": len(projects), "revenue": sum(projects.mapped("mcs_monthly_revenue_target")), - "total_invoiced": partner.mcs_total_invoiced, - "outstanding": partner.mcs_total_outstanding, - "overdue": partner.mcs_total_overdue, - "last_invoice_date": fields.Date.to_string(partner.mcs_last_invoice_date) - if partner.mcs_last_invoice_date + "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( @@ -626,7 +689,11 @@ class McsOperatingDashboard(models.Model): ) return rows - def _dashboard_actions(self, today, month_start, month_end, week_start, week_end): + 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", @@ -634,7 +701,8 @@ class McsOperatingDashboard(models.Model): "res_model": "project.project", "view_mode": "tree,form,kanban", "views": self._action_views("tree", "form", "kanban"), - "domain": [("active", "=", True), ("mcs_monthly_revenue_target", ">", 0)], + "domain": [("active", "=", True), ("mcs_monthly_revenue_target", ">", 0)] + + self._company_domain(Project, company_ids, include_shared=True), }, "contracted_projects": { "type": "ir.actions.act_window", @@ -642,7 +710,8 @@ class McsOperatingDashboard(models.Model): "res_model": "project.project", "view_mode": "tree,form,kanban", "views": self._action_views("tree", "form", "kanban"), - "domain": [("active", "=", True), ("mcs_contracted_revenue", ">", 0)], + "domain": [("active", "=", True), ("mcs_contracted_revenue", ">", 0)] + + self._company_domain(Project, company_ids, include_shared=True), }, "pipeline": { "type": "ir.actions.act_window", @@ -650,7 +719,12 @@ class McsOperatingDashboard(models.Model): "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)], + "domain": [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("probability", "<", 100), + ] + + self._company_domain(Lead, company_ids, include_shared=True), }, "overdue_receivables": { "type": "ir.actions.act_window", @@ -663,7 +737,8 @@ class McsOperatingDashboard(models.Model): ("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", @@ -674,7 +749,8 @@ class McsOperatingDashboard(models.Model): "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": { @@ -683,7 +759,11 @@ class McsOperatingDashboard(models.Model): "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"])], + "domain": [ + ("active", "=", True), + ("mcs_portfolio_health", "in", ["at_risk", "blocked"]), + ] + + self._company_domain(Project, company_ids, include_shared=True), }, "ceo_actions": { "type": "ir.actions.act_window", @@ -699,7 +779,8 @@ class McsOperatingDashboard(models.Model): "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"])], + "domain": [("mcs_delivery_type", "in", ["internal_product", "internal_r_and_d"])] + + self._company_domain(Project, company_ids, include_shared=True), }, }