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:
gsinghpal
2026-05-21 00:21:57 -04:00
parent f463600585
commit 638b223d3b
6 changed files with 240 additions and 2 deletions

View File

@@ -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";