feat(fusion_repairs): Bundle 9 - service callout pricing + store labor warranty
Full home-service pricing engine plus the store labor warranty model. The
call price now itemises base callout + extra techs + hourly labour (with
the 30-min-included + 1-hour-minimum rule) + travel both ways past
threshold, with three independent waive paths: in-warranty / manager
override / sales-rep override. CS cannot waive (RBAC).
NEW MODELS
fusion.repair.callout.rate (rate card)
Per (tier, company) row. Tiers: regular / after_hours / weekend / holiday.
Fields:
- base_callout_fee (INCLUDES first 30 min for inspection / report)
- second_tech_fee + additional_tech_fee (3rd, 4th tech)
- hourly_labor_rate + minimum_labor_hours (default 1.0 floor)
- travel_distance_threshold_km + travel_per_km_fee
- effective_from (newer rows supersede older)
Seeded with 4 default rows (regular $120/$95/0.85, after-hours
$180/$140/1.10, weekend $240/$170/1.35, holiday $300/$200/1.50).
fusion.repair.labor.warranty (store labor warranty)
Per (partner, product/lot, sale_order) record with warranty_years +
start_date + computed end_date. State machine: active / expired / void
/ consumed. Void reasons spec'd by the user: user_negligence /
gross_negligence / misuse / over_recommended_use / accidental_damage
/ not_covered_part / other.
find_active_for(partner, product, lot) - lot-first then product+partner
then partner-only fallback so warranty resolution survives partner-
contact / product-variant differences.
action_void(reason, notes) - manager-only; audit stamps voided_by_id
+ voided_at + reason; posts chatter.
PRODUCT EXTENSION
product.template.x_fc_labor_warranty_years (Integer, default 0).
SALE-ORDER EXTENSION
sale.order.action_confirm now also runs _fc_spawn_labor_warranties()
which creates one fusion.repair.labor.warranty per unit of any product
with x_fc_labor_warranty_years > 0. Lives alongside the existing
service-plan spawn so a 5y-LW stairlift sold with a maintenance plan
spawns both records in one go.
PRICING ENGINE ON REPAIR.ORDER
9 new fields:
x_fc_callout_tier (regular/after_hours/weekend/holiday)
x_fc_callout_distance_km (one-way; system bills both ways)
x_fc_callout_techs (1, 2, 3+)
x_fc_callout_labor_hours (hours above the 30 min the callout covers)
x_fc_labor_warranty_id (auto-resolved on visit)
x_fc_labor_warranty_status (not_checked / eligible / not_covered /
expired / void_misuse / waived)
x_fc_labor_waived + _by_id + _at + _reason
6 computed quote fields:
x_fc_quote_callout_base (base_callout_fee)
x_fc_quote_extra_techs (second + additional fees)
x_fc_quote_labor (max(hours, min_hours) * rate * techs)
x_fc_quote_travel (max(distance - threshold, 0) * 2 * per_km * techs)
x_fc_quote_waived (= labor if warranty eligible OR labor waived)
x_fc_quote_total (sum minus waived; stored, indexable)
+ a human-readable x_fc_quote_breakdown_text used in the email template.
3 new actions:
action_check_labor_warranty (anyone) - resolves the warranty and
stamps x_fc_labor_warranty_status. Called automatically by the
visit-report wizard.
action_waive_labor_fee (SECURITY GATED) - raises UserError unless
caller is in group_fusion_repairs_manager OR
group_fusion_repairs_sales_rep. CS users get the explicit message
'Only Repairs Managers and Sales Reps can waive the labor fee.'
action_acknowledge_rush - Bundle 8 carryover.
SECURITY
New group_fusion_repairs_sales_rep
Independent group so a sales rep can waive labor on their accounts
without becoming a Repairs Dispatcher / Manager. Manager IMPLIES
sales_rep so managers automatically inherit the right.
ACLs: callout.rate user-read / manager-full; labor.warranty user-read /
sales_rep-write / manager-full / technician-read+write.
VISIT-REPORT WIZARD EXTENSIONS
Pricing block (visible when outcome=completed):
callout_tier / techs / distance_km / labor_hours_used (default 1.0
minimum). Live quote_total_preview + breakdown shown to the tech so
they can confirm the price with the client right at the door.
Warranty block:
labor_warranty_id_preview + labor_warranty_status_preview (badge
coloured by status). 'warranty_void_reason' selection lets the tech
void the warranty in real time when they find misuse / negligence /
accidental damage - on submit the matching warranty record is voided
permanently (action_void) AND the repair's labor charge re-computes
without the waive.
On confirm the wizard:
1. Persists callout_labor_hours_used to the repair
2. Calls repair.action_check_labor_warranty()
3. If warranty_void_reason set + warranty resolved -> voids it,
posts chatter, repair labor_warranty_status -> void_misuse
NAVIGATION
Repair form 4 new header buttons:
Check Labor Warranty (anyone)
Waive Labor Fee (sales_rep + manager only, server-side gated)
(plus the Bundle 8 Squeeze + Ack Rush from before)
New 'Callout Pricing' notebook tab on repair form with:
inputs, warranty/waiver, and the 6-line quote breakdown.
New menus:
Fusion Repairs > Labor Warranties
Configuration > Callout Rate Card
Configuration > Emergency Surcharges (Bundle 8 carryover)
VERIFICATION END-TO-END (7 scenarios, 0 bugs)
A. Sale of a product with 5y LW -> LW-00002 spawned, expires 2031-05-21.
B. In-warranty regular 12km 20-min repair:
base 120 + labor 95 - waived 95 = $120 (callout only)
C. After-hours 2-tech 40km 1.5h, NO warranty:
180 + 90 + (1.5*140*2) + (15*2*1.10*2) = $756.00 exact
D. In-warranty visit -> tech ticks misuse void_reason:
Warranty record -> state=void / reason=misuse.
Repair labor_warranty_status -> void_misuse.
Quote re-computes WITHOUT waive: labor 1.5 * 95 = $142.50 charged.
E. Manager waives labor on a no-warranty repair:
Pre-waive $310 -> post-waive $120 (labor $190 -> waived).
Audit: waived_by_id stamped to gsingh@.
F. CS rep tries to waive: correctly denied with the spec'd error
'Only Repairs Managers and Sales Reps can waive the labor fee.'
G. Weekend 1-tech 30km 30-min:
240 + (1.0*170) + (5*2*1.35) = $423.50 exact (min-1h floor
correctly applied to the 0.5h actual work).
Bumped to 19.0.2.0.0 (minor version bump - new public-facing model).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -63,6 +63,18 @@
|
||||
action="action_repair_emergency_charge"
|
||||
sequence="60"/>
|
||||
|
||||
<menuitem id="menu_fusion_repairs_callout_rate"
|
||||
name="Callout Rate Card"
|
||||
parent="menu_fusion_repairs_configuration"
|
||||
action="action_repair_callout_rate"
|
||||
sequence="65"/>
|
||||
|
||||
<menuitem id="menu_fusion_repairs_labor_warranty"
|
||||
name="Labor Warranties"
|
||||
parent="menu_fusion_repairs_root"
|
||||
action="action_repair_labor_warranty"
|
||||
sequence="36"/>
|
||||
|
||||
<!-- Configuration -->
|
||||
<menuitem id="menu_fusion_repairs_configuration"
|
||||
name="Configuration"
|
||||
|
||||
30
fusion_repairs/views/repair_callout_rate_views.xml
Normal file
30
fusion_repairs/views/repair_callout_rate_views.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_repair_callout_rate_list" model="ir.ui.view">
|
||||
<field name="name">fusion.repair.callout.rate.list</field>
|
||||
<field name="model">fusion.repair.callout.rate</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Callout Rate Card" editable="bottom">
|
||||
<field name="tier"/>
|
||||
<field name="base_callout_fee" widget="monetary"/>
|
||||
<field name="second_tech_fee" widget="monetary"/>
|
||||
<field name="additional_tech_fee" widget="monetary"/>
|
||||
<field name="hourly_labor_rate" widget="monetary"/>
|
||||
<field name="minimum_labor_hours"/>
|
||||
<field name="travel_distance_threshold_km"/>
|
||||
<field name="travel_per_km_fee" widget="monetary"/>
|
||||
<field name="effective_from"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_repair_callout_rate" model="ir.actions.act_window">
|
||||
<field name="name">Callout Rate Card</field>
|
||||
<field name="res_model">fusion.repair.callout.rate</field>
|
||||
<field name="view_mode">list</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
77
fusion_repairs/views/repair_labor_warranty_views.xml
Normal file
77
fusion_repairs/views/repair_labor_warranty_views.xml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_repair_labor_warranty_list" model="ir.ui.view">
|
||||
<field name="name">fusion.repair.labor.warranty.list</field>
|
||||
<field name="model">fusion.repair.labor.warranty</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Store Labor Warranties"
|
||||
decoration-success="state == 'active'"
|
||||
decoration-warning="state == 'expired'"
|
||||
decoration-danger="state == 'void'"
|
||||
decoration-muted="state == 'consumed'">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="lot_id" optional="show"/>
|
||||
<field name="sale_order_id" optional="show"/>
|
||||
<field name="warranty_years"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="void_reason" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_repair_labor_warranty_form" model="ir.ui.view">
|
||||
<field name="name">fusion.repair.labor.warranty.form</field>
|
||||
<field name="model">fusion.repair.labor.warranty</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Store Labor Warranty">
|
||||
<header>
|
||||
<button name="action_reinstate" type="object"
|
||||
string="Reinstate" class="btn-secondary"
|
||||
invisible="state != 'void'"
|
||||
groups="fusion_repairs.group_fusion_repairs_manager"
|
||||
confirm="Reinstate this voided warranty? Use only when the original void was a mistake."/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="active,expired,void,consumed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id" options="{'no_create': True}"/>
|
||||
<field name="product_id" options="{'no_create': True}"/>
|
||||
<field name="lot_id"/>
|
||||
<field name="sale_order_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="warranty_years"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Void Information" invisible="state != 'void'">
|
||||
<field name="void_reason" readonly="1"/>
|
||||
<field name="voided_by_id" readonly="1"/>
|
||||
<field name="voided_at" readonly="1"/>
|
||||
<field name="void_notes" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_repair_labor_warranty" model="ir.actions.act_window">
|
||||
<field name="name">Labor Warranties</field>
|
||||
<field name="res_model">fusion.repair.labor.warranty</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -46,6 +46,21 @@
|
||||
icon="fa-check"
|
||||
invisible="not x_fc_rush_requested or x_fc_rush_acknowledged_at"
|
||||
groups="fusion_repairs.group_fusion_repairs_user"/>
|
||||
<!-- Bundle 9: warranty + waive (waive group-gated server-side too) -->
|
||||
<button name="action_check_labor_warranty"
|
||||
type="object"
|
||||
string="Check Labor Warranty"
|
||||
class="btn-secondary"
|
||||
icon="fa-shield"
|
||||
invisible="state in ('done', 'cancel')"
|
||||
groups="fusion_repairs.group_fusion_repairs_user"/>
|
||||
<button name="action_waive_labor_fee"
|
||||
type="object"
|
||||
string="Waive Labor Fee"
|
||||
class="btn-warning"
|
||||
icon="fa-percent"
|
||||
invisible="x_fc_labor_waived or state in ('done', 'cancel')"
|
||||
groups="fusion_repairs.group_fusion_repairs_sales_rep"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Smart buttons: Technician Tasks + Intake Answers + Original SO. -->
|
||||
@@ -144,6 +159,46 @@
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Callout Pricing" name="fusion_callout">
|
||||
<group>
|
||||
<group string="Inputs">
|
||||
<field name="x_fc_callout_tier"/>
|
||||
<field name="x_fc_callout_techs"/>
|
||||
<field name="x_fc_callout_distance_km"/>
|
||||
<field name="x_fc_callout_labor_hours"/>
|
||||
</group>
|
||||
<group string="Warranty / Waiver">
|
||||
<field name="x_fc_labor_warranty_id" readonly="1"/>
|
||||
<field name="x_fc_labor_warranty_status" widget="badge"
|
||||
decoration-success="x_fc_labor_warranty_status == 'eligible'"
|
||||
decoration-warning="x_fc_labor_warranty_status in ('expired', 'waived')"
|
||||
decoration-danger="x_fc_labor_warranty_status == 'void_misuse'"/>
|
||||
<field name="x_fc_labor_waived" readonly="1"/>
|
||||
<field name="x_fc_labor_waived_by_id" readonly="1"
|
||||
invisible="not x_fc_labor_waived"/>
|
||||
<field name="x_fc_labor_waived_at" readonly="1"
|
||||
invisible="not x_fc_labor_waived"/>
|
||||
<field name="x_fc_labor_waived_reason"
|
||||
invisible="not x_fc_labor_waived"
|
||||
readonly="not x_fc_labor_waived"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Quote Breakdown"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="x_fc_quote_callout_base" widget="monetary" readonly="1"/>
|
||||
<field name="x_fc_quote_extra_techs" widget="monetary" readonly="1"/>
|
||||
<field name="x_fc_quote_labor" widget="monetary" readonly="1"/>
|
||||
<field name="x_fc_quote_travel" widget="monetary" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="x_fc_quote_waived" widget="monetary" readonly="1"/>
|
||||
<field name="x_fc_quote_total" widget="monetary" readonly="1"
|
||||
class="oe_subtotal_footer_separator"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Rush / Parts" name="fusion_rush_parts">
|
||||
<group>
|
||||
<group string="Rush Service">
|
||||
|
||||
Reference in New Issue
Block a user