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 odoo import api, fields, models
|
||||
@ -188,12 +190,14 @@ class McsOperatingDashboard(models.Model):
|
||||
week_start = today - relativedelta(days=today.weekday())
|
||||
week_end = week_start + relativedelta(days=6)
|
||||
company_ids = self._dashboard_company_ids(selected_company_ids)
|
||||
default_currency_ids = self._default_currency_ids(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"]
|
||||
Payment = self.env["account.payment"]
|
||||
Employee = self.env["hr.employee"]
|
||||
Partner = self.env["res.partner"]
|
||||
CeoAction = self.env["mcs.ceo.action"]
|
||||
@ -249,6 +253,16 @@ class McsOperatingDashboard(models.Model):
|
||||
[("move_type", "=", "out_invoice"), ("state", "=", "posted")]
|
||||
+ 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(
|
||||
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"))
|
||||
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
|
||||
)
|
||||
invoiced_month = sum(invoices_month.mapped("amount_total_signed"))
|
||||
cash_collected = sum(payments_month.mapped("amount_company_currency_signed"))
|
||||
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"))
|
||||
@ -325,22 +337,50 @@ class McsOperatingDashboard(models.Model):
|
||||
self._dashboard_company_ids()
|
||||
)
|
||||
],
|
||||
"currency_summary": self._company_currency_summary(company_ids),
|
||||
},
|
||||
"company_pulse": {
|
||||
"revenue_coverage": self._format_percent(revenue_coverage),
|
||||
"revenue_coverage_status": self._coverage_status(revenue_coverage),
|
||||
"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_display": self._project_amount_display(
|
||||
active_projects, "mcs_monthly_revenue_target"
|
||||
),
|
||||
"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_display": self._invoice_amount_display(
|
||||
invoices_month, "amount_total", default_currency_ids
|
||||
),
|
||||
"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_display": self._operating_cost_display(company_ids),
|
||||
"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")),
|
||||
"open_pipeline": open_pipeline_value,
|
||||
"open_pipeline_display": self._lead_amount_display(open_pipeline),
|
||||
"weighted_pipeline": weighted_pipeline,
|
||||
"weighted_pipeline_display": self._lead_weighted_amount_display(open_pipeline),
|
||||
"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_display": self._invoice_amount_display(
|
||||
overdue_receivables, "amount_residual", default_currency_ids
|
||||
),
|
||||
"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")),
|
||||
@ -355,6 +395,25 @@ class McsOperatingDashboard(models.Model):
|
||||
overdue_receivables,
|
||||
utilization,
|
||||
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": {
|
||||
"target": monthly_target,
|
||||
@ -362,11 +421,36 @@ class McsOperatingDashboard(models.Model):
|
||||
"invoiced": invoiced_month,
|
||||
"collected": cash_collected,
|
||||
"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,
|
||||
"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),
|
||||
"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),
|
||||
"project_health": self._project_health_distribution(active_projects),
|
||||
@ -383,6 +467,111 @@ class McsOperatingDashboard(models.Model):
|
||||
def _format_percent(self, value):
|
||||
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):
|
||||
allowed_company_ids = (
|
||||
self.env.context.get("allowed_company_ids") or self.env.companies.ids
|
||||
@ -434,28 +623,41 @@ class McsOperatingDashboard(models.Model):
|
||||
overdue_receivables,
|
||||
utilization,
|
||||
active_projects,
|
||||
display_values=None,
|
||||
):
|
||||
display_values = display_values or {}
|
||||
return [
|
||||
{
|
||||
"label": "Revenue Target",
|
||||
"value": monthly_target,
|
||||
"value": display_values.get("monthly_target", monthly_target),
|
||||
"action_key": "monthly_revenue_projects",
|
||||
},
|
||||
{
|
||||
"label": "Contracted Revenue",
|
||||
"value": contracted_revenue,
|
||||
"value": display_values.get("contracted_revenue", 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": "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",
|
||||
"value": weighted_pipeline,
|
||||
"value": display_values.get("weighted_pipeline", weighted_pipeline),
|
||||
"action_key": "pipeline",
|
||||
},
|
||||
{
|
||||
"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",
|
||||
},
|
||||
{
|
||||
@ -521,11 +723,7 @@ class McsOperatingDashboard(models.Model):
|
||||
):
|
||||
continue
|
||||
currencies = invoices.mapped("currency_id") or partner.mcs_currency_id
|
||||
currency_name = (
|
||||
currencies[0].name
|
||||
if len(currencies) == 1
|
||||
else "Mixed"
|
||||
)
|
||||
currency_name = " / ".join(currencies.mapped("name")) if currencies else ""
|
||||
invoice_dates = [date for date in invoices.mapped("invoice_date") if date]
|
||||
rows.append(
|
||||
{
|
||||
@ -536,8 +734,17 @@ class McsOperatingDashboard(models.Model):
|
||||
"active_workstreams": len(projects),
|
||||
"revenue": sum(projects.mapped("mcs_monthly_revenue_target")),
|
||||
"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_display": self._invoice_amount_display(
|
||||
unpaid_invoices, "amount_residual"
|
||||
),
|
||||
"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))
|
||||
if invoice_dates
|
||||
else "",
|
||||
@ -554,6 +761,59 @@ class McsOperatingDashboard(models.Model):
|
||||
)
|
||||
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):
|
||||
deadlines = projects.mapped("tasks.date_deadline")
|
||||
deadlines = [deadline for deadline in deadlines if deadline]
|
||||
|
||||
@ -185,7 +185,7 @@
|
||||
.o_mcs_capacity_row,
|
||||
.o_mcs_distribution_row {
|
||||
display: grid;
|
||||
grid-template-columns: 120px minmax(80px, 1fr) 80px;
|
||||
grid-template-columns: 120px minmax(80px, 1fr) minmax(150px, max-content);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
@ -231,6 +231,32 @@
|
||||
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_health_item,
|
||||
.o_mcs_action_item,
|
||||
@ -333,7 +359,8 @@
|
||||
|
||||
.o_mcs_bar_row,
|
||||
.o_mcs_capacity_row,
|
||||
.o_mcs_distribution_row {
|
||||
.o_mcs_distribution_row,
|
||||
.o_mcs_company_summary_row {
|
||||
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 }}">
|
||||
<span>Revenue Coverage</span>
|
||||
<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>
|
||||
<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)">
|
||||
@ -57,7 +57,7 @@
|
||||
<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>
|
||||
<strong t-esc="state.data.revenue[key]"/>
|
||||
<strong t-esc="state.data.revenue.display[key]"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
@ -97,9 +97,9 @@
|
||||
<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.active_workstreams"/>
|
||||
<span t-esc="row.total_invoiced"/>
|
||||
<span t-esc="row.outstanding"/>
|
||||
<span t-esc="row.overdue"/>
|
||||
<span t-esc="row.total_invoiced_display"/>
|
||||
<span t-esc="row.outstanding_display"/>
|
||||
<span t-esc="row.overdue_display"/>
|
||||
<span t-esc="row.last_invoice_date"/>
|
||||
<span t-esc="row.next_action"/>
|
||||
</button>
|
||||
@ -121,6 +121,22 @@
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<header>
|
||||
<h2>Team Capacity</h2>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user