Fix CEO dashboard multi-company revenue totals
This commit is contained in:
parent
7cf6f1413b
commit
2ca8373b19
@ -1,3 +1,5 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
@ -188,12 +190,14 @@ class McsOperatingDashboard(models.Model):
|
|||||||
week_start = today - relativedelta(days=today.weekday())
|
week_start = today - relativedelta(days=today.weekday())
|
||||||
week_end = week_start + relativedelta(days=6)
|
week_end = week_start + relativedelta(days=6)
|
||||||
company_ids = self._dashboard_company_ids(selected_company_ids)
|
company_ids = self._dashboard_company_ids(selected_company_ids)
|
||||||
|
default_currency_ids = self._default_currency_ids(company_ids)
|
||||||
|
|
||||||
Lead = self.env["crm.lead"]
|
Lead = self.env["crm.lead"]
|
||||||
Project = self.env["project.project"]
|
Project = self.env["project.project"]
|
||||||
Task = self.env["project.task"]
|
Task = self.env["project.task"]
|
||||||
Timesheet = self.env["account.analytic.line"]
|
Timesheet = self.env["account.analytic.line"]
|
||||||
Invoice = self.env["account.move"]
|
Invoice = self.env["account.move"]
|
||||||
|
Payment = self.env["account.payment"]
|
||||||
Employee = self.env["hr.employee"]
|
Employee = self.env["hr.employee"]
|
||||||
Partner = self.env["res.partner"]
|
Partner = self.env["res.partner"]
|
||||||
CeoAction = self.env["mcs.ceo.action"]
|
CeoAction = self.env["mcs.ceo.action"]
|
||||||
@ -249,6 +253,16 @@ class McsOperatingDashboard(models.Model):
|
|||||||
[("move_type", "=", "out_invoice"), ("state", "=", "posted")]
|
[("move_type", "=", "out_invoice"), ("state", "=", "posted")]
|
||||||
+ self._company_domain(Invoice, company_ids, include_shared=False)
|
+ self._company_domain(Invoice, company_ids, include_shared=False)
|
||||||
)
|
)
|
||||||
|
payments_month = Payment.search(
|
||||||
|
[
|
||||||
|
("state", "=", "posted"),
|
||||||
|
("payment_type", "=", "inbound"),
|
||||||
|
("partner_type", "=", "customer"),
|
||||||
|
("date", ">=", month_start),
|
||||||
|
("date", "<=", month_end),
|
||||||
|
]
|
||||||
|
+ self._company_domain(Payment, company_ids, include_shared=False)
|
||||||
|
)
|
||||||
overdue_receivables = receivables.filtered(
|
overdue_receivables = receivables.filtered(
|
||||||
lambda invoice: invoice.invoice_date_due and invoice.invoice_date_due < today
|
lambda invoice: invoice.invoice_date_due and invoice.invoice_date_due < today
|
||||||
)
|
)
|
||||||
@ -271,10 +285,8 @@ class McsOperatingDashboard(models.Model):
|
|||||||
|
|
||||||
monthly_target = sum(active_projects.mapped("mcs_monthly_revenue_target"))
|
monthly_target = sum(active_projects.mapped("mcs_monthly_revenue_target"))
|
||||||
contracted_revenue = sum(client_projects.mapped("mcs_contracted_revenue"))
|
contracted_revenue = sum(client_projects.mapped("mcs_contracted_revenue"))
|
||||||
invoiced_month = sum(invoices_month.mapped("amount_total"))
|
invoiced_month = sum(invoices_month.mapped("amount_total_signed"))
|
||||||
cash_collected = sum(
|
cash_collected = sum(payments_month.mapped("amount_company_currency_signed"))
|
||||||
invoice.amount_total - invoice.amount_residual for invoice in invoices_month
|
|
||||||
)
|
|
||||||
operating_cost = self._monthly_operating_cost(company_ids)
|
operating_cost = self._monthly_operating_cost(company_ids)
|
||||||
revenue_gap = max(monthly_target - contracted_revenue, 0)
|
revenue_gap = max(monthly_target - contracted_revenue, 0)
|
||||||
open_pipeline_value = sum(open_pipeline.mapped("expected_revenue"))
|
open_pipeline_value = sum(open_pipeline.mapped("expected_revenue"))
|
||||||
@ -325,22 +337,50 @@ class McsOperatingDashboard(models.Model):
|
|||||||
self._dashboard_company_ids()
|
self._dashboard_company_ids()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
"currency_summary": self._company_currency_summary(company_ids),
|
||||||
},
|
},
|
||||||
"company_pulse": {
|
"company_pulse": {
|
||||||
"revenue_coverage": self._format_percent(revenue_coverage),
|
"revenue_coverage": self._format_percent(revenue_coverage),
|
||||||
"revenue_coverage_status": self._coverage_status(revenue_coverage),
|
"revenue_coverage_status": self._coverage_status(revenue_coverage),
|
||||||
"remaining_gap": remaining_gap,
|
"remaining_gap": remaining_gap,
|
||||||
|
"remaining_gap_display": self._company_amount_display(
|
||||||
|
active_projects, "mcs_contracted_revenue", company_ids, invert_from=operating_cost
|
||||||
|
),
|
||||||
"monthly_revenue_target": monthly_target,
|
"monthly_revenue_target": monthly_target,
|
||||||
|
"monthly_revenue_target_display": self._project_amount_display(
|
||||||
|
active_projects, "mcs_monthly_revenue_target"
|
||||||
|
),
|
||||||
"contracted_monthly_revenue": contracted_revenue,
|
"contracted_monthly_revenue": contracted_revenue,
|
||||||
|
"contracted_monthly_revenue_display": self._project_amount_display(
|
||||||
|
client_projects, "mcs_contracted_revenue"
|
||||||
|
),
|
||||||
"revenue_invoiced_month": invoiced_month,
|
"revenue_invoiced_month": invoiced_month,
|
||||||
|
"revenue_invoiced_month_display": self._invoice_amount_display(
|
||||||
|
invoices_month, "amount_total", default_currency_ids
|
||||||
|
),
|
||||||
"cash_collected_month": cash_collected,
|
"cash_collected_month": cash_collected,
|
||||||
|
"cash_collected_month_display": self._payment_amount_display(
|
||||||
|
payments_month, "amount", default_currency_ids
|
||||||
|
),
|
||||||
"operating_cost": operating_cost,
|
"operating_cost": operating_cost,
|
||||||
|
"operating_cost_display": self._operating_cost_display(company_ids),
|
||||||
"revenue_gap": revenue_gap,
|
"revenue_gap": revenue_gap,
|
||||||
|
"revenue_gap_display": self._company_amount_display(
|
||||||
|
client_projects, "mcs_contracted_revenue", company_ids, invert_from=monthly_target
|
||||||
|
),
|
||||||
"new_mrr_won": sum(won_month.mapped("mcs_expected_monthly_revenue")),
|
"new_mrr_won": sum(won_month.mapped("mcs_expected_monthly_revenue")),
|
||||||
"open_pipeline": open_pipeline_value,
|
"open_pipeline": open_pipeline_value,
|
||||||
|
"open_pipeline_display": self._lead_amount_display(open_pipeline),
|
||||||
"weighted_pipeline": weighted_pipeline,
|
"weighted_pipeline": weighted_pipeline,
|
||||||
|
"weighted_pipeline_display": self._lead_weighted_amount_display(open_pipeline),
|
||||||
"outstanding_receivables": sum(receivables.mapped("amount_residual")),
|
"outstanding_receivables": sum(receivables.mapped("amount_residual")),
|
||||||
|
"outstanding_receivables_display": self._invoice_amount_display(
|
||||||
|
receivables, "amount_residual", default_currency_ids
|
||||||
|
),
|
||||||
"overdue_receivables": sum(overdue_receivables.mapped("amount_residual")),
|
"overdue_receivables": sum(overdue_receivables.mapped("amount_residual")),
|
||||||
|
"overdue_receivables_display": self._invoice_amount_display(
|
||||||
|
overdue_receivables, "amount_residual", default_currency_ids
|
||||||
|
),
|
||||||
"team_utilization": self._format_percent(utilization),
|
"team_utilization": self._format_percent(utilization),
|
||||||
"billable_utilization": self._format_percent(billable_utilization),
|
"billable_utilization": self._format_percent(billable_utilization),
|
||||||
"projects_at_risk": len(active_projects.filtered(lambda project: project.mcs_portfolio_health == "at_risk")),
|
"projects_at_risk": len(active_projects.filtered(lambda project: project.mcs_portfolio_health == "at_risk")),
|
||||||
@ -355,6 +395,25 @@ class McsOperatingDashboard(models.Model):
|
|||||||
overdue_receivables,
|
overdue_receivables,
|
||||||
utilization,
|
utilization,
|
||||||
active_projects,
|
active_projects,
|
||||||
|
{
|
||||||
|
"monthly_target": self._project_amount_display(
|
||||||
|
active_projects, "mcs_monthly_revenue_target"
|
||||||
|
),
|
||||||
|
"contracted_revenue": self._project_amount_display(
|
||||||
|
client_projects, "mcs_contracted_revenue"
|
||||||
|
),
|
||||||
|
"revenue_gap": self._company_amount_display(
|
||||||
|
client_projects,
|
||||||
|
"mcs_contracted_revenue",
|
||||||
|
company_ids,
|
||||||
|
invert_from=monthly_target,
|
||||||
|
),
|
||||||
|
"open_pipeline": self._lead_amount_display(open_pipeline),
|
||||||
|
"weighted_pipeline": self._lead_weighted_amount_display(open_pipeline),
|
||||||
|
"overdue_receivables": self._invoice_amount_display(
|
||||||
|
overdue_receivables, "amount_residual", default_currency_ids
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"revenue": {
|
"revenue": {
|
||||||
"target": monthly_target,
|
"target": monthly_target,
|
||||||
@ -362,11 +421,36 @@ class McsOperatingDashboard(models.Model):
|
|||||||
"invoiced": invoiced_month,
|
"invoiced": invoiced_month,
|
||||||
"collected": cash_collected,
|
"collected": cash_collected,
|
||||||
"forecast": contracted_revenue + weighted_pipeline,
|
"forecast": contracted_revenue + weighted_pipeline,
|
||||||
|
"display": {
|
||||||
|
"target": self._project_amount_display(
|
||||||
|
active_projects, "mcs_monthly_revenue_target"
|
||||||
|
),
|
||||||
|
"contracted": self._project_amount_display(
|
||||||
|
client_projects, "mcs_contracted_revenue"
|
||||||
|
),
|
||||||
|
"invoiced": self._invoice_amount_display(
|
||||||
|
invoices_month, "amount_total", default_currency_ids
|
||||||
|
),
|
||||||
|
"collected": self._payment_amount_display(
|
||||||
|
payments_month, "amount", default_currency_ids
|
||||||
|
),
|
||||||
|
"forecast": self._forecast_amount_display(client_projects, open_pipeline),
|
||||||
|
},
|
||||||
"qualified_pipeline": qualified_pipeline,
|
"qualified_pipeline": qualified_pipeline,
|
||||||
"required_pipeline": required_pipeline,
|
"required_pipeline": required_pipeline,
|
||||||
"pipeline_coverage": self._format_percent(pipeline_coverage),
|
"pipeline_coverage": self._format_percent(pipeline_coverage),
|
||||||
"pipeline_coverage_status": self._coverage_status(pipeline_coverage),
|
"pipeline_coverage_status": self._coverage_status(pipeline_coverage),
|
||||||
"funnel": self._crm_funnel_data(open_pipeline),
|
"funnel": self._crm_funnel_data(open_pipeline),
|
||||||
|
"company_summaries": self._dashboard_company_summaries(
|
||||||
|
company_ids,
|
||||||
|
active_projects,
|
||||||
|
client_projects,
|
||||||
|
invoices_month,
|
||||||
|
payments_month,
|
||||||
|
receivables,
|
||||||
|
overdue_receivables,
|
||||||
|
posted_invoices,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"portfolio": self._portfolio_rows(Partner, client_projects, posted_invoices),
|
"portfolio": self._portfolio_rows(Partner, client_projects, posted_invoices),
|
||||||
"project_health": self._project_health_distribution(active_projects),
|
"project_health": self._project_health_distribution(active_projects),
|
||||||
@ -383,6 +467,111 @@ class McsOperatingDashboard(models.Model):
|
|||||||
def _format_percent(self, value):
|
def _format_percent(self, value):
|
||||||
return round(value or 0, 1)
|
return round(value or 0, 1)
|
||||||
|
|
||||||
|
def _format_currency_amount(self, currency, amount):
|
||||||
|
amount = round(amount or 0, 2)
|
||||||
|
currency_name = currency.name if currency else self.env.company.currency_id.name
|
||||||
|
return "%s %s" % (currency_name, "{:,.2f}".format(amount))
|
||||||
|
|
||||||
|
def _format_currency_buckets(self, buckets, default_currency_ids=None):
|
||||||
|
if not buckets:
|
||||||
|
buckets = {currency_id: 0 for currency_id in (default_currency_ids or [])}
|
||||||
|
if not buckets:
|
||||||
|
buckets = {self.env.company.currency_id.id: 0}
|
||||||
|
Currency = self.env["res.currency"].sudo()
|
||||||
|
parts = []
|
||||||
|
for currency_id, amount in sorted(buckets.items()):
|
||||||
|
parts.append(self._format_currency_amount(Currency.browse(currency_id), amount))
|
||||||
|
return " / ".join(parts)
|
||||||
|
|
||||||
|
def _default_currency_ids(self, company_ids):
|
||||||
|
return self.env["res.company"].sudo().browse(company_ids).mapped("currency_id").ids
|
||||||
|
|
||||||
|
def _company_currency_summary(self, company_ids):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"company_id": company.id,
|
||||||
|
"company": company.name,
|
||||||
|
"currency": company.currency_id.name,
|
||||||
|
}
|
||||||
|
for company in self.env["res.company"].sudo().browse(company_ids)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _invoice_amount_display(self, invoices, amount_field, default_currency_ids=None):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for invoice in invoices:
|
||||||
|
currency = invoice.currency_id or invoice.company_currency_id
|
||||||
|
buckets[currency.id] += invoice[amount_field] or 0
|
||||||
|
return self._format_currency_buckets(buckets, default_currency_ids)
|
||||||
|
|
||||||
|
def _payment_amount_display(self, payments, amount_field, default_currency_ids=None):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for payment in payments:
|
||||||
|
currency = payment.currency_id or payment.company_currency_id
|
||||||
|
buckets[currency.id] += payment[amount_field] or 0
|
||||||
|
return self._format_currency_buckets(buckets, default_currency_ids)
|
||||||
|
|
||||||
|
def _project_amount_display(self, projects, amount_field):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for project in projects:
|
||||||
|
currency = project.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += project[amount_field] or 0
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
|
def _lead_amount_display(self, leads):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for lead in leads:
|
||||||
|
currency = lead.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += lead.expected_revenue or 0
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
|
def _lead_weighted_amount_display(self, leads):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for lead in leads:
|
||||||
|
currency = lead.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += (lead.expected_revenue or 0) * (lead.probability or 0) / 100
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
|
def _forecast_amount_display(self, projects, leads):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for project in projects:
|
||||||
|
currency = project.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += project.mcs_contracted_revenue or 0
|
||||||
|
for lead in leads:
|
||||||
|
currency = lead.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += (lead.expected_revenue or 0) * (lead.probability or 0) / 100
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
|
def _operating_cost_display(self, company_ids):
|
||||||
|
try:
|
||||||
|
Contract = self.env["hr.contract"]
|
||||||
|
except KeyError:
|
||||||
|
return self._format_currency_amount(self.env.company.currency_id, 0)
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
contracts = Contract.sudo().search(
|
||||||
|
[("state", "=", "open"), ("employee_id.active", "=", True)]
|
||||||
|
+ self._company_domain(Contract, company_ids, include_shared=True)
|
||||||
|
)
|
||||||
|
for contract in contracts:
|
||||||
|
currency = contract.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += contract.wage or 0
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
|
def _company_amount_display(self, records, amount_field, company_ids, invert_from=None):
|
||||||
|
buckets = defaultdict(float)
|
||||||
|
for company in self.env["res.company"].sudo().browse(company_ids):
|
||||||
|
buckets[company.currency_id.id] += 0
|
||||||
|
for record in records:
|
||||||
|
currency = record.company_id.currency_id or self.env.company.currency_id
|
||||||
|
buckets[currency.id] += record[amount_field] or 0
|
||||||
|
if invert_from is not None:
|
||||||
|
total = sum(buckets.values())
|
||||||
|
currency = (
|
||||||
|
self.env["res.company"].sudo().browse(company_ids[:1]).currency_id
|
||||||
|
or self.env.company.currency_id
|
||||||
|
)
|
||||||
|
return self._format_currency_amount(currency, max((invert_from or 0) - total, 0))
|
||||||
|
return self._format_currency_buckets(buckets)
|
||||||
|
|
||||||
def _dashboard_company_ids(self, selected_company_ids=None):
|
def _dashboard_company_ids(self, selected_company_ids=None):
|
||||||
allowed_company_ids = (
|
allowed_company_ids = (
|
||||||
self.env.context.get("allowed_company_ids") or self.env.companies.ids
|
self.env.context.get("allowed_company_ids") or self.env.companies.ids
|
||||||
@ -434,28 +623,41 @@ class McsOperatingDashboard(models.Model):
|
|||||||
overdue_receivables,
|
overdue_receivables,
|
||||||
utilization,
|
utilization,
|
||||||
active_projects,
|
active_projects,
|
||||||
|
display_values=None,
|
||||||
):
|
):
|
||||||
|
display_values = display_values or {}
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"label": "Revenue Target",
|
"label": "Revenue Target",
|
||||||
"value": monthly_target,
|
"value": display_values.get("monthly_target", monthly_target),
|
||||||
"action_key": "monthly_revenue_projects",
|
"action_key": "monthly_revenue_projects",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Contracted Revenue",
|
"label": "Contracted Revenue",
|
||||||
"value": contracted_revenue,
|
"value": display_values.get("contracted_revenue", contracted_revenue),
|
||||||
"action_key": "contracted_projects",
|
"action_key": "contracted_projects",
|
||||||
},
|
},
|
||||||
{"label": "Revenue Gap", "value": revenue_gap, "action_key": "pipeline"},
|
{
|
||||||
{"label": "Open Pipeline", "value": open_pipeline_value, "action_key": "pipeline"},
|
"label": "Revenue Gap",
|
||||||
|
"value": display_values.get("revenue_gap", revenue_gap),
|
||||||
|
"action_key": "pipeline",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Open Pipeline",
|
||||||
|
"value": display_values.get("open_pipeline", open_pipeline_value),
|
||||||
|
"action_key": "pipeline",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Weighted Pipeline",
|
"label": "Weighted Pipeline",
|
||||||
"value": weighted_pipeline,
|
"value": display_values.get("weighted_pipeline", weighted_pipeline),
|
||||||
"action_key": "pipeline",
|
"action_key": "pipeline",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Overdue Receivables",
|
"label": "Overdue Receivables",
|
||||||
"value": sum(overdue_receivables.mapped("amount_residual")),
|
"value": display_values.get(
|
||||||
|
"overdue_receivables",
|
||||||
|
sum(overdue_receivables.mapped("amount_residual")),
|
||||||
|
),
|
||||||
"action_key": "overdue_receivables",
|
"action_key": "overdue_receivables",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -521,11 +723,7 @@ class McsOperatingDashboard(models.Model):
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
currencies = invoices.mapped("currency_id") or partner.mcs_currency_id
|
currencies = invoices.mapped("currency_id") or partner.mcs_currency_id
|
||||||
currency_name = (
|
currency_name = " / ".join(currencies.mapped("name")) if currencies else ""
|
||||||
currencies[0].name
|
|
||||||
if len(currencies) == 1
|
|
||||||
else "Mixed"
|
|
||||||
)
|
|
||||||
invoice_dates = [date for date in invoices.mapped("invoice_date") if date]
|
invoice_dates = [date for date in invoices.mapped("invoice_date") if date]
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
@ -536,8 +734,17 @@ class McsOperatingDashboard(models.Model):
|
|||||||
"active_workstreams": len(projects),
|
"active_workstreams": len(projects),
|
||||||
"revenue": sum(projects.mapped("mcs_monthly_revenue_target")),
|
"revenue": sum(projects.mapped("mcs_monthly_revenue_target")),
|
||||||
"total_invoiced": sum(invoices.mapped("amount_total_signed")),
|
"total_invoiced": sum(invoices.mapped("amount_total_signed")),
|
||||||
|
"total_invoiced_display": self._invoice_amount_display(
|
||||||
|
invoices, "amount_total"
|
||||||
|
),
|
||||||
"outstanding": sum(unpaid_invoices.mapped("amount_residual_signed")),
|
"outstanding": sum(unpaid_invoices.mapped("amount_residual_signed")),
|
||||||
|
"outstanding_display": self._invoice_amount_display(
|
||||||
|
unpaid_invoices, "amount_residual"
|
||||||
|
),
|
||||||
"overdue": sum(overdue_invoices.mapped("amount_residual_signed")),
|
"overdue": sum(overdue_invoices.mapped("amount_residual_signed")),
|
||||||
|
"overdue_display": self._invoice_amount_display(
|
||||||
|
overdue_invoices, "amount_residual"
|
||||||
|
),
|
||||||
"last_invoice_date": fields.Date.to_string(max(invoice_dates))
|
"last_invoice_date": fields.Date.to_string(max(invoice_dates))
|
||||||
if invoice_dates
|
if invoice_dates
|
||||||
else "",
|
else "",
|
||||||
@ -554,6 +761,59 @@ class McsOperatingDashboard(models.Model):
|
|||||||
)
|
)
|
||||||
return sorted(rows, key=lambda row: row["risk"], reverse=True)
|
return sorted(rows, key=lambda row: row["risk"], reverse=True)
|
||||||
|
|
||||||
|
def _dashboard_company_summaries(
|
||||||
|
self,
|
||||||
|
company_ids,
|
||||||
|
active_projects,
|
||||||
|
client_projects,
|
||||||
|
invoices_month,
|
||||||
|
payments_month,
|
||||||
|
receivables,
|
||||||
|
overdue_receivables,
|
||||||
|
posted_invoices,
|
||||||
|
):
|
||||||
|
rows = []
|
||||||
|
for company in self.env["res.company"].sudo().browse(company_ids):
|
||||||
|
company_projects = active_projects.filtered(lambda project: project.company_id == company)
|
||||||
|
company_client_projects = client_projects.filtered(lambda project: project.company_id == company)
|
||||||
|
company_invoices_month = invoices_month.filtered(lambda invoice: invoice.company_id == company)
|
||||||
|
company_payments_month = payments_month.filtered(lambda payment: payment.company_id == company)
|
||||||
|
company_receivables = receivables.filtered(lambda invoice: invoice.company_id == company)
|
||||||
|
company_overdue = overdue_receivables.filtered(lambda invoice: invoice.company_id == company)
|
||||||
|
company_posted = posted_invoices.filtered(lambda invoice: invoice.company_id == company)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"company_id": company.id,
|
||||||
|
"company": company.name,
|
||||||
|
"currency": company.currency_id.name,
|
||||||
|
"target": self._format_currency_amount(
|
||||||
|
company.currency_id,
|
||||||
|
sum(company_projects.mapped("mcs_monthly_revenue_target")),
|
||||||
|
),
|
||||||
|
"contracted": self._format_currency_amount(
|
||||||
|
company.currency_id,
|
||||||
|
sum(company_client_projects.mapped("mcs_contracted_revenue")),
|
||||||
|
),
|
||||||
|
"invoiced_this_month": self._invoice_amount_display(
|
||||||
|
company_invoices_month, "amount_total", [company.currency_id.id]
|
||||||
|
),
|
||||||
|
"collected_this_month": self._payment_amount_display(
|
||||||
|
company_payments_month, "amount", [company.currency_id.id]
|
||||||
|
),
|
||||||
|
"outstanding": self._invoice_amount_display(
|
||||||
|
company_receivables, "amount_residual", [company.currency_id.id]
|
||||||
|
),
|
||||||
|
"overdue": self._invoice_amount_display(
|
||||||
|
company_overdue, "amount_residual", [company.currency_id.id]
|
||||||
|
),
|
||||||
|
"lifetime_invoiced": self._invoice_amount_display(
|
||||||
|
company_posted, "amount_total", [company.currency_id.id]
|
||||||
|
),
|
||||||
|
"invoice_count": len(company_posted),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
def _nearest_deadline(self, projects):
|
def _nearest_deadline(self, projects):
|
||||||
deadlines = projects.mapped("tasks.date_deadline")
|
deadlines = projects.mapped("tasks.date_deadline")
|
||||||
deadlines = [deadline for deadline in deadlines if deadline]
|
deadlines = [deadline for deadline in deadlines if deadline]
|
||||||
|
|||||||
@ -185,7 +185,7 @@
|
|||||||
.o_mcs_capacity_row,
|
.o_mcs_capacity_row,
|
||||||
.o_mcs_distribution_row {
|
.o_mcs_distribution_row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 120px minmax(80px, 1fr) 80px;
|
grid-template-columns: 120px minmax(80px, 1fr) minmax(150px, max-content);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
@ -231,6 +231,32 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.o_mcs_company_summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_mcs_company_summary_row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 1.2fr) minmax(110px, .7fr) repeat(4, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
display: block;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.o_mcs_funnel_stage,
|
.o_mcs_funnel_stage,
|
||||||
.o_mcs_health_item,
|
.o_mcs_health_item,
|
||||||
.o_mcs_action_item,
|
.o_mcs_action_item,
|
||||||
@ -333,7 +359,8 @@
|
|||||||
|
|
||||||
.o_mcs_bar_row,
|
.o_mcs_bar_row,
|
||||||
.o_mcs_capacity_row,
|
.o_mcs_capacity_row,
|
||||||
.o_mcs_distribution_row {
|
.o_mcs_distribution_row,
|
||||||
|
.o_mcs_company_summary_row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<div t-attf-class="o_mcs_coverage o_mcs_status_{{ state.data.company_pulse.revenue_coverage_status }}">
|
<div t-attf-class="o_mcs_coverage o_mcs_status_{{ state.data.company_pulse.revenue_coverage_status }}">
|
||||||
<span>Revenue Coverage</span>
|
<span>Revenue Coverage</span>
|
||||||
<strong><t t-esc="state.data.company_pulse.revenue_coverage"/>%</strong>
|
<strong><t t-esc="state.data.company_pulse.revenue_coverage"/>%</strong>
|
||||||
<small>Remaining gap: <t t-esc="state.data.company_pulse.remaining_gap"/></small>
|
<small>Remaining gap: <t t-esc="state.data.company_pulse.remaining_gap_display"/></small>
|
||||||
</div>
|
</div>
|
||||||
<t t-foreach="state.data.kpis" t-as="kpi" t-key="kpi.label">
|
<t t-foreach="state.data.kpis" t-as="kpi" t-key="kpi.label">
|
||||||
<button class="o_mcs_kpi" t-on-click="() => this.openAction(kpi.action_key)">
|
<button class="o_mcs_kpi" t-on-click="() => this.openAction(kpi.action_key)">
|
||||||
@ -57,7 +57,7 @@
|
|||||||
<div class="o_mcs_bar_track">
|
<div class="o_mcs_bar_track">
|
||||||
<div class="o_mcs_bar_fill" t-att-style="this.barStyle(state.data.revenue[key], state.data.revenue.target)"/>
|
<div class="o_mcs_bar_fill" t-att-style="this.barStyle(state.data.revenue[key], state.data.revenue.target)"/>
|
||||||
</div>
|
</div>
|
||||||
<strong t-esc="state.data.revenue[key]"/>
|
<strong t-esc="state.data.revenue.display[key]"/>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
@ -97,9 +97,9 @@
|
|||||||
<span t-attf-class="o_mcs_badge o_mcs_status_{{ row.health }}" t-esc="row.health"/>
|
<span t-attf-class="o_mcs_badge o_mcs_status_{{ row.health }}" t-esc="row.health"/>
|
||||||
<span t-esc="row.currency"/>
|
<span t-esc="row.currency"/>
|
||||||
<span t-esc="row.active_workstreams"/>
|
<span t-esc="row.active_workstreams"/>
|
||||||
<span t-esc="row.total_invoiced"/>
|
<span t-esc="row.total_invoiced_display"/>
|
||||||
<span t-esc="row.outstanding"/>
|
<span t-esc="row.outstanding_display"/>
|
||||||
<span t-esc="row.overdue"/>
|
<span t-esc="row.overdue_display"/>
|
||||||
<span t-esc="row.last_invoice_date"/>
|
<span t-esc="row.last_invoice_date"/>
|
||||||
<span t-esc="row.next_action"/>
|
<span t-esc="row.next_action"/>
|
||||||
</button>
|
</button>
|
||||||
@ -121,6 +121,22 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="o_mcs_grid o_mcs_grid_two">
|
<section class="o_mcs_grid o_mcs_grid_two">
|
||||||
|
<div class="o_mcs_panel">
|
||||||
|
<header><h2>Company Revenue Summary</h2></header>
|
||||||
|
<div class="o_mcs_company_summary">
|
||||||
|
<t t-foreach="state.data.revenue.company_summaries" t-as="company" t-key="company.company_id">
|
||||||
|
<div class="o_mcs_company_summary_row">
|
||||||
|
<strong t-esc="company.company"/>
|
||||||
|
<span><t t-esc="company.invoice_count"/> posted invoices</span>
|
||||||
|
<span>Month invoiced: <b t-esc="company.invoiced_this_month"/></span>
|
||||||
|
<span>Month collected: <b t-esc="company.collected_this_month"/></span>
|
||||||
|
<span>Outstanding: <b t-esc="company.outstanding"/></span>
|
||||||
|
<span>Lifetime invoiced: <b t-esc="company.lifetime_invoiced"/></span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="o_mcs_panel">
|
<div class="o_mcs_panel">
|
||||||
<header>
|
<header>
|
||||||
<h2>Team Capacity</h2>
|
<h2>Team Capacity</h2>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user