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:
@@ -9,7 +9,7 @@ recent activity, and upcoming maintenance. Lives as an AbstractModel
|
||||
because it stores nothing - all values are computed on demand.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
@@ -121,6 +121,61 @@ class FusionRepairDashboard(models.AbstractModel):
|
||||
'sales_rep_portal_url': base_url + '/my/repair/new',
|
||||
}
|
||||
|
||||
# ---------------- M7: failure-rate analytics ----------------
|
||||
# Top products by repair count in the last 90 days (excludes draft).
|
||||
ninety = datetime.now() - timedelta(days=90)
|
||||
failure_rows = Repair._read_group(
|
||||
[
|
||||
('create_date', '>=', ninety),
|
||||
('product_id', '!=', False),
|
||||
('state', '!=', 'cancel'),
|
||||
],
|
||||
['product_id'],
|
||||
['__count'],
|
||||
order='__count desc',
|
||||
limit=8,
|
||||
)
|
||||
failures_by_product = [{
|
||||
'product_id': p.id,
|
||||
'product_name': p.display_name,
|
||||
'repair_count': c,
|
||||
} for p, c in failure_rows]
|
||||
|
||||
# Top symptom categories (issue_category) in the last 90 days.
|
||||
symptom_rows = Repair._read_group(
|
||||
[
|
||||
('create_date', '>=', ninety),
|
||||
('x_fc_issue_category', '!=', False),
|
||||
('state', '!=', 'cancel'),
|
||||
],
|
||||
['x_fc_issue_category'],
|
||||
['__count'],
|
||||
order='__count desc',
|
||||
limit=8,
|
||||
)
|
||||
failures_by_symptom = [{
|
||||
'symptom': s or 'Other',
|
||||
'repair_count': c,
|
||||
} for s, c in symptom_rows]
|
||||
|
||||
# M9: margin summary (open + done in the last 90 days).
|
||||
margin_rows = self.env['repair.order'].search([
|
||||
('create_date', '>=', ninety),
|
||||
('state', '!=', 'cancel'),
|
||||
])
|
||||
total_revenue = sum(margin_rows.mapped('x_fc_revenue'))
|
||||
total_labour = sum(margin_rows.mapped('x_fc_labour_cost'))
|
||||
total_parts = sum(margin_rows.mapped('x_fc_parts_cost'))
|
||||
total_margin = total_revenue - total_labour - total_parts
|
||||
margin_summary = {
|
||||
'revenue': total_revenue,
|
||||
'labour_cost': total_labour,
|
||||
'parts_cost': total_parts,
|
||||
'margin': total_margin,
|
||||
'margin_pct': (total_margin / total_revenue * 100) if total_revenue else 0.0,
|
||||
'sample_size': len(margin_rows),
|
||||
}
|
||||
|
||||
return {
|
||||
'stats': stats,
|
||||
'urgency_breakdown': urgency_breakdown,
|
||||
@@ -128,4 +183,7 @@ class FusionRepairDashboard(models.AbstractModel):
|
||||
'recent': recent,
|
||||
'upcoming': upcoming,
|
||||
'portals': portals,
|
||||
'failures_by_product': failures_by_product,
|
||||
'failures_by_symptom': failures_by_symptom,
|
||||
'margin_summary': margin_summary,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user