feat(fusion_repairs): Bundle 6 - M7 failure analytics + M9 margin per repair
M9 margin per repair
- New non-stored computes on repair.order: x_fc_revenue, x_fc_labour_cost,
x_fc_parts_cost, x_fc_margin, x_fc_margin_pct.
- Revenue: sum of posted out_invoice.amount_untaxed on the repair's sale
order (handles partial / multi invoice scenarios).
- Labour: sum of (task.duration_hours x technician.x_fc_tech_cost_rate)
over COMPLETED visits only - avoids counting scheduled-but-not-done time.
- Parts: sum of standard_price x qty for stock moves where
repair_line_type='add' (parts consumed, not removed).
- New 'Margin' notebook tab on repair.order form, manager-group gated.
M7 failure analytics on the dashboard
- Three new keys in get_dashboard_data():
* failures_by_product - top 8 products by repair_count in last 90 days
via _read_group (efficient - no record load)
* failures_by_symptom - top 8 x_fc_issue_category values
* margin_summary - revenue/labour/parts/margin/margin_pct + sample_size
over the same 90-day window
- Three new tiles on the OWL dashboard 'Last 90 Days' section:
Margin Summary (revenue/labour/parts/margin breakdown),
Failure Rate by Product, Failure Rate by Symptom.
- New formatMoney + formatPercent helpers on the dashboard JS so values
display as 'CAD 12,345' rather than raw floats.
Verified end-to-end on local westin-v19:
Dashboard returned all 9 expected keys.
Top product: 'M6 X 27 THREADED BARREL' (2 repairs) - actual test data.
Margin summary over 26 repairs (dev has $0 invoices so values 0.0,
but the compute path is exercised and shapes are correct).
Bumped to 19.0.1.6.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,9 @@ export class FusionRepairsDashboard extends Component {
|
||||
recent: [],
|
||||
upcoming: [],
|
||||
portals: {},
|
||||
failures_by_product: [],
|
||||
failures_by_symptom: [],
|
||||
margin_summary: {},
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
@@ -45,6 +48,9 @@ export class FusionRepairsDashboard extends Component {
|
||||
this.state.recent = data.recent || [];
|
||||
this.state.upcoming = data.upcoming || [];
|
||||
this.state.portals = data.portals || {};
|
||||
this.state.failures_by_product = data.failures_by_product || [];
|
||||
this.state.failures_by_symptom = data.failures_by_symptom || [];
|
||||
this.state.margin_summary = data.margin_summary || {};
|
||||
} catch (e) {
|
||||
this.notification.add(_t("Could not load dashboard data."), {
|
||||
type: "danger",
|
||||
@@ -112,6 +118,20 @@ export class FusionRepairsDashboard extends Component {
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
|
||||
formatMoney(value) {
|
||||
const v = Number(value || 0);
|
||||
return v.toLocaleString("en-CA", {
|
||||
style: "currency",
|
||||
currency: "CAD",
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
formatPercent(value) {
|
||||
const v = Number(value || 0);
|
||||
return `${v.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
urgencyPillClass(urgency) {
|
||||
if (urgency === "safety") return "fr-pill fr-pill-safety";
|
||||
if (urgency === "urgent") return "fr-pill fr-pill-urgent";
|
||||
|
||||
@@ -211,6 +211,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics (M7 + M9) -->
|
||||
<div class="fr-section-title">Last 90 Days</div>
|
||||
<div class="fr-grid fr-grid-lists">
|
||||
<div class="fr-list">
|
||||
<h3><i class="fa fa-line-chart me-2"/>Margin Summary</h3>
|
||||
<t t-if="!state.margin_summary or !state.margin_summary.sample_size">
|
||||
<div class="fr-list-empty">No data yet for the last 90 days</div>
|
||||
</t>
|
||||
<t t-if="state.margin_summary and state.margin_summary.sample_size">
|
||||
<div class="fr-list-row">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title">Revenue</span>
|
||||
<span class="fr-list-sub">Posted invoices on repair SOs</span>
|
||||
</div>
|
||||
<span class="fr-list-meta">
|
||||
<t t-out="formatMoney(state.margin_summary.revenue)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="fr-list-row">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title">Labour Cost</span>
|
||||
<span class="fr-list-sub">Hours x tech cost rate</span>
|
||||
</div>
|
||||
<span class="fr-list-meta">
|
||||
- <t t-out="formatMoney(state.margin_summary.labour_cost)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="fr-list-row">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title">Parts Cost</span>
|
||||
<span class="fr-list-sub">Standard price of consumed parts</span>
|
||||
</div>
|
||||
<span class="fr-list-meta">
|
||||
- <t t-out="formatMoney(state.margin_summary.parts_cost)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="fr-list-row" style="border-top:2px solid #d8dadd;">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title">Margin</span>
|
||||
<span class="fr-list-sub">
|
||||
<t t-out="formatPercent(state.margin_summary.margin_pct)"/>
|
||||
on <t t-out="state.margin_summary.sample_size"/> repairs
|
||||
</span>
|
||||
</div>
|
||||
<span class="fr-list-meta" style="font-weight:600;">
|
||||
<t t-out="formatMoney(state.margin_summary.margin)"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="fr-list">
|
||||
<h3><i class="fa fa-bar-chart me-2"/>Failure Rate by Product</h3>
|
||||
<t t-if="state.failures_by_product.length === 0">
|
||||
<div class="fr-list-empty">No repairs in the last 90 days</div>
|
||||
</t>
|
||||
<t t-foreach="state.failures_by_product" t-as="p" t-key="p.product_id">
|
||||
<div class="fr-list-row">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title"><t t-out="p.product_name"/></span>
|
||||
</div>
|
||||
<span class="fr-list-meta">
|
||||
<t t-out="p.repair_count"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fr-grid fr-grid-lists">
|
||||
<div class="fr-list">
|
||||
<h3><i class="fa fa-tags me-2"/>Failure Rate by Symptom</h3>
|
||||
<t t-if="state.failures_by_symptom.length === 0">
|
||||
<div class="fr-list-empty">No symptoms tagged in the last 90 days</div>
|
||||
</t>
|
||||
<t t-foreach="state.failures_by_symptom" t-as="s" t-key="s.symptom">
|
||||
<div class="fr-list-row">
|
||||
<div class="fr-list-main">
|
||||
<span class="fr-list-title"><t t-out="s.symptom"/></span>
|
||||
</div>
|
||||
<span class="fr-list-meta">
|
||||
<t t-out="s.repair_count"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="fr-section-title">Configuration</div>
|
||||
<div class="fr-grid fr-grid-config">
|
||||
|
||||
Reference in New Issue
Block a user