Plan task P3.5. Single 'Workstation' menu item replaces both the
legacy 'Tablet Station' and 'Plant Overview' entries. The new
fp_shopfloor_landing component has a Station/All-Plant toggle so
one menu covers both old surfaces.
Old action records redirected for back-compat (so existing bookmarks
+ smart-button references keep working):
action_fp_shopfloor_tablet tag → fp_shopfloor_landing
action_fp_plant_overview tag → fp_shopfloor_landing
params → {'mode': 'all_plant'}
The legacy OWL components (fp_shopfloor_tablet, fp_plant_overview)
remain registered — no code removed, just no menu points at them.
Phase 5 cleanup will remove the OWL components after a release of
soak time on entech.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P3.2 + P3.3 + P3.4 batched. Full ShopfloorLanding OWL
client action — replaces fp_shopfloor_tablet AND folds in
fp_plant_overview.
Header strip Title, station chip, station picker dropdown,
Station/All-Plant mode toggle, QR scan controls,
last-refresh indicator.
KPI strip 4 tech-relevant tiles: Ready · Running ·
Bakes Due (warning) · Holds (red when > 0).
Search Live debounced (200ms) across WO# + customer +
part. ESC clears.
Kanban board Columns = work centres from /fp/landing/kanban.
Cards = FpKanbanCard (Phase 1 — P1.7).
Drag-and-drop reuses existing
/fp/shopfloor/plant_overview/move_card.
Card tap doAction → fp_job_workspace with
{job_id, focus_step_id}.
QR scan FP-STATION pairs, FP-JOB / FP-STEP jump to the
Workspace.
Mode + station_id persist in localStorage (LS_STATION_ID, LS_MODE).
Auto-refresh every 15s; suppressed during a drop and for 5s after.
Registers client action `fp_shopfloor_landing`. Menu rewire + endpoint
stubs land in P3.5 + P3.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P3.1. New JSON-RPC endpoint for the Shop Floor Landing
client action (Phase 3). Two modes:
station — paired WC + Unassigned + next 1-2 WCs in recipe flow
all_plant — every active WC, recipe-flow order (replaces the data
path for the standalone fp_plant_overview action)
Returns {columns: [{work_center_id, work_center_name, cards}], kpis:
{ready, running, bakes_due, holds}, stations: [...], facility_name,
server_time}. Card payload matches the KanbanCard OWL component
(P1.7) — same shape, no client-side adapter needed.
Light implementation — no urgency scoring or batch prefetch yet.
Both can be ported from plant_overview if performance demands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P2.6. Per the spec's "techs wear multiple hats" rule, lift
gates so technicians can do their work without permission walls:
fp.certificate operator: read → read+write
(flip draft→issued from tablet)
fp.thickness.reading operator: read → read+write+create
(capture Fischerscope readings from tablet)
fp.job.node.override operator: NEW read-only
(see opt-out badges on steps)
Supervisor-only operations (step Skip, hold Release, override
Re-include) remain enforced in workspace_controller, not ACL — so the
ACL stays minimal and the controller centralizes the gate logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P2.4 + P2.5 batched.
Adds _cron_autopause_stale_steps method on fp.job.step + 30-min cron
registration. Flips in_progress steps idle > threshold to paused with
a chatter audit ("Auto-paused after Nh idle. Resume from the tablet
when work continues.").
Threshold from ir.config_parameter:
fp.shopfloor.autopause_threshold_hours (default 8.0)
Recipe nodes opt out via fusion.plating.process.node.long_running
(added in P2.1) — useful for 24h bakes and multi-shift soaks.
Fixes the 411-hour ghost timer that motivated the redesign. Doesn't
replace the existing nudge crons — those still notify the supervisor;
this one actually pauses the timer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P2.2 + P2.3 batched (both small additive computes on fp.job;
local tests not run between them — entech verifies).
late_risk_ratio — stored Float, remaining_planned / minutes_to_deadline.
Drives the Manager At-Risk view (Phase 4).
Recomputes on step state, duration, deadline changes.
active_step_id — non-stored Many2one. Currently in_progress step
(lowest sequence if multiple — defensive).
Drives JobWorkspace landing focus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P2.1. Boolean on fusion.plating.process.node that exempts
steps generated from this node from the shop-floor auto-pause cron
(added in P2.4/P2.5). Use for 24h bakes, multi-shift soaks, and
similar long-but-legitimate operations.
Toggle visible on the process-node form for operation/step types,
grouped with parallel_start in the Behaviour section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.16. Header button on the fp.job form that opens the
JobWorkspace OWL client action focused on the current WO. Primary
entry point for techs before the Landing kanban (Phase 3) ships;
remains as a back-office shortcut after.
Hidden when state == 'draft' (no steps to work yet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan tasks P1.8 through P1.11 batched into one commit (local tests not
run between them; entech is the verification env).
POST /fp/workspace/load — full payload for one fp.job
POST /fp/workspace/hold — quality.hold create with photo
POST /fp/workspace/sign_off — signature + finish step atomic
POST /fp/workspace/advance_milestone — fire next_milestone_action
Each endpoint logs INFO on success, EXCEPTION on failure, returns a
consistent {'ok': bool, 'error': str?} envelope. Hold endpoint isolates
photo-attach failures so they don't roll back the hold record.
Tests cover: payload shape, bad job_id, hold create with/without photo,
empty qty rejection, empty-signature rejection, sign-off finish, and
the no-milestone-action error path.
Verify on entech: -u fusion_plating_shopfloor --test-tags fp_shopfloor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.7. Final shared service — standard WO card used on Landing
kanban, Manager Plant Board, and Workflow Funnel. Embeds WorkflowChip,
shows progress bar, priority dot, blocker badge from step.blocker_kind.
Density prop ('compact' vs 'normal') swaps padding for funnel use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.6. Modal hold-creation form: reason picker, qty split,
optional photo (camera input on mobile), description, mark-for-scrap
toggle. Calls /fp/workspace/hold (added in P1.9). Reason list kept
client-side, keep in sync with fusion.plating.quality.hold.hold_reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.5. Modal canvas signature capture using HTML pointer events
+ Odoo Dialog service. Returns image/png dataURI via onSubmit callback;
caller decides what to do with it (e.g. /fp/workspace/sign_off attaches
to fp.job.step).
Canvas stays light even in dark mode for signature legibility.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.4. "Can't start yet — Waiting on Step N: X" block reused
across JobWorkspace step rows and Manager Plant Board cards. Icon set
maps to blocker_kind (predecessor/contract_review/parts_not_received/
racking_required/manager_input). Optional Jump button propagates to
parent via onJump callback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.3. Bootstraps the tests/ dir and adds the first of 5
shared OWL services. Pill renders fp.job.workflow.state with color
mapping + optional next-action hint.
Per CLAUDE.md "Dark Mode" rule: registered once in web.assets_backend;
Odoo 19 auto-compiles into both bright and dark bundles via the
\$o-webclient-color-scheme SCSS branch.
Version bumped to 19.0.27.0.0 (Phase 1 — Workspace foundation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.2. Reuses _fp_should_block_predecessors so the new compute
stays in sync with the existing can_start logic. Drives the OWL GateViz
component on the tablet — "Can't start yet — Waiting on Step N: X".
Future work: extend with explicit branches for contract_review /
parts_not_received / racking_required / manager_input as those gate
models mature.
Tests not run locally (no fusion_plating mount in odoo-modsdev).
Verify on entech: -u fusion_plating_jobs --test-tags fp_jobs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan task P1.1. Formats fp.job.name as "WO # <last-segment>" for
tablet/dashboard surfaces. Underlying name field is unchanged so
back-office forms, reports, and emails keep WH/JOB/00001.
Tests not run locally — fusion_plating not mounted in odoo-modsdev
container. Verify on entech: -u fusion_plating_jobs --test-tags fp_jobs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aggressive sheet override: flex-basis 100%%, !important on width and
max-width to beat parent flex/media-query constraints. Also overrides
the o_form_sheet_bg wrapper.
Layout at xl (>=1200px) now splits into 3 columns:
- Col 1 (3/12): Your Activities + Bottlenecks
- Col 2 (5/12): ADP Pre + ADP Post + MOD
- Col 3 (4/12): Aging + Other Funders + Recent ADP Exports
Falls back to 5/7 on lg (Col 3 wraps below as full row) and stacked
single column on md and below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .o_fc_dashboard .o_form_sheet override wasn't winning specificity
against Odoo's default form-sheet constraints. Added a dedicated class
o_fc_dashboard_sheet directly on the <sheet> element + !important
overrides on max-width, width, and flex to stretch the sheet to the
full container width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 4 new sections:
- This Month rollup: submitted/approved/delivered/billed counts MTD
- Pipeline $ by stage: pre-submit / submitted / approved / ready-to-bill amounts
- Aging buckets: 30-59d, 60-89d, 90+ days
- Recent ADP Exports: last 5 with totals
Also overrides Odoo's form-sheet max-width on .o_fc_dashboard so the
dashboard uses the full browser width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dashboard action existed but no menuitem ever pointed to it (latent
bug in the original module). Adding menu_fusion_claims_dashboard as the
first child of menu_adp_claims_root so the dashboard becomes the default
landing for the Fusion Claims app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Action-oriented dashboard replacing the existing 4-panel HTML overview:
posting-week banner with live countdown, 3 KPI tiles, 8 funder hotlinks,
ADP + MOD workflow flag tiles, role-aware filtering, dark-mode aware SCSS.
Spec captures all design decisions from the brainstorm session; ready to
hand off to writing-plans.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User shared their actual published service-rate card. Bundle 9's seeded
numbers were placeholders that no longer match. Realigned the rate card,
added the LIFT & ELEVATING SERVICE class, added the in-shop labour
rate path, added the delivery / pickup charge model, added rush as a
proper tier (distinct from after-hours), and added 30-min increment
rounding on top of the existing 1-hour minimum.
EQUIPMENT CLASS
fusion.repair.product.category gets a new x_fc_equipment_class
selection: 'standard' vs 'lift_elevating'. The published card splits
pricing into two service classes - lift_elevating has higher rates
($160 callout vs $95, $110/h vs $85).
Categories marked lift_elevating in seed:
stairlift, porch_lift, lift_chair (new)
New 'Lift Chair' category seeded (power recliner / lift chair).
CALLOUT RATE CARD
fusion.repair.callout.rate gets:
- equipment_class field (standard / lift_elevating)
- in_shop_labor_rate field (separate $75 vs $85 on-site)
- 'rush' tier value (was missing - rush was implicit via emergency
surcharge from Bundle 8; now a proper tier matching the printed
rate card row 'Rush Service Calls $120')
Re-seeded with the PUBLISHED Westin rate card (exact values):
STANDARD SERVICE
regular $95 callout / $85/h on-site / $75/h in-shop
rush $120 callout / $85/h / $75/h
after_hours $140 callout / $85/h / $75/h
weekend $180 callout / $85/h / $75/h (extension)
holiday $220 callout / $85/h / $75/h (extension)
LIFT & ELEVATING SERVICE
regular $160 callout / $110/h on-site / $110/h in-shop
rush $200 callout / $110/h / $110/h (extension)
after_hours $240 callout / $110/h / $110/h (extension)
weekend $300 callout / $110/h / $110/h (extension)
holiday $360 callout / $110/h / $110/h (extension)
Travel: $0.70 per km, BOTH WAYS, past 25 km, per technician
(matches the per-card '$0.70 per km x 2-way' footnote).
get_for_tier(tier, equipment_class) now resolves with a fallback:
tries (tier, lift_elevating) first, falls back to (tier, standard)
if no lift-specific row exists - so an admin can leave standard rows
as the catch-all and only customise lift for the exceptions.
DELIVERY / PICKUP RATE CARD
New fusion.repair.delivery.charge model + seed of all 7 items from
the printed card:
Local Service Area (within Brampton) ........ $35
Outside Local Area .......................... $60
Rush Pickups / Delivery ..................... $60 + $0.70/km x 2-way
Lift Chair Delivery and Set-Up .............. $120
Hospital Bed Delivery and Set-Up ............ $120
Stairlift Delivery and Set-Up ............... $300
Stairlift Removal ........................... $300
quote_rush(distance_km) helper for the office's delivery scheduling.
New menu: Configuration > Delivery / Pickup Charges.
PRICING ENGINE UPDATES (repair.order._compute_callout_quote)
- Class-aware rate lookup (uses category.equipment_class).
- In-shop mode (x_fc_in_shop=True): skips callout fee + extra-tech +
travel; charges in_shop_labor_rate * hours * techs only. Per the
rate-card footnote 'In-Shop Labour Rate'.
- 30-min increment rounding ON TOP of the 1-hour floor:
billable_h = max(ceil(actual * 2) / 2, min_hours)
-> 20-min work bills 1.0 h
-> 75-min work bills 1.5 h
-> 95-min work bills 2.0 h
- Improved breakdown text shows the rate-card row name + class +
pro-ration math so the client can see how the total was computed.
NEW FIELDS
repair.order:
x_fc_in_shop (Boolean) - flip to switch the quote engine to
in-shop mode.
x_fc_callout_tier now includes 'rush' as a value (was missing).
visit-report wizard:
callout_in_shop related field - tech can flip the mode on-site if
the work was actually done in-store after pickup.
MIGRATION SCRIPT
migrations/19.0.2.1.0/post-migration.py runs once on existing
installs:
1. Updates stairlift / porch_lift / lift_chair categories
equipment_class -> lift_elevating
2. Wipes the 4 Bundle 9 rate-card xml_ids so the new noupdate=1
seed creates them with the correct printed values.
Fresh installs get the right values directly from the seed XML.
Admin-created custom rate rows (no xml_id) are NEVER touched.
VERIFIED END-TO-END (0 bugs across 28 checks)
Rate card matches printed values exactly:
regular/standard = $95/$85h/$75h PASS
rush/standard = $120/$85h/$75h PASS
after_hours/standard = $140/$85h/$75h PASS
regular/lift = $160/$110h/$110h PASS
Six end-to-end quote scenarios:
A. Standard 12km 20-min -> $180 ($95 + 1h*$85)
B. Lift 12km 20-min -> $270 ($160 + 1h*$110)
C. Rush 30km 1.2h -> $254.50
($120 + ceil(2.4)/2=1.5h * $85 + 5km*2*$0.70 = $7)
D. After-hours lift 2-tech 35km 2.6h -> $928.00
($240 + ceil(5.2)/2=3.0h * $110 * 2 + 10km*2*$0.70*2)
E. In-shop standard 2h -> $150 (2h * $75 in-shop, no callout)
F. In-shop lift 1.5h -> $165 (1.5h * $110 in-shop)
Seven delivery rates loaded with correct amounts; rush 40km calc
= $81 ($60 base + 15km*2*$0.70).
Stairlift / Porch Lift / Lift Chair categories correctly marked
lift_elevating; rest stay standard.
Bumped to 19.0.2.1.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
New module `fusion_service_charges` that creates the standard
service-billing product catalog for Westin Healthcare and Mobility
Specialties:
Standard Service
SVC-STD-CALL Service Call (incl. 30 min) $95
SVC-STD-LABOUR Standard Labour (hourly) $85
SVC-INSHOP-LABOUR In-Shop Labour (hourly) $75
SVC-RUSH-CALL Rush Service Call $120
SVC-AH-CALL After-Hours Service Call $140
Lift & Elevating
SVC-LIFT-CALL Lift Service Call (incl. 30 min) $160
SVC-LIFT-LABOUR Lift Labour (hourly) $110
Delivery / Pickup
DEL-LOCAL Local (within Brampton) $35
DEL-OUT Outside Local Area $60
DEL-RUSH Rush Delivery / Pickup $60
DEL-LIFT-CHAIR Lift Chair Delivery + Set-up $120
DEL-HOSP-BED Hospital Bed Delivery + Set-up $120
DEL-STAIRLIFT Stairlift Delivery + Set-up $300
SVC-STAIRLIFT-RM Stairlift Removal $300
Loading pattern (intentional):
- Products created via post_init_hook on FIRST install only.
- Manifest's `data` list is EMPTY so no XML is loaded on `-u`.
- Hook is idempotent — sentinel ir.model.data xmlid check skips
records that already exist. Safe to re-run.
- User edits / deletes survive every upgrade (proven on entech-
westin: edited SVC-STD-CALL price to $999.99 → ran -u → price
stuck. Reset to $95 after test.).
- Uninstall + reinstall does re-seed (ir.model.data sentinels drop
on uninstall, fresh install treats it as new).
Per-km surcharges (Rush, Outside Local, After-Hours) are noted in
the product description so the dispatcher knows to add a separate
mileage line. Formula-based pricelist for auto-mileage is out of
scope — matches current manual workflow on both shops.
Odoo 19 compatibility: dropped uom_po_id from the create vals
(retired in 18; uom_id is now the single source of truth for sale
and purchase UoM on product.template).
Deployed and verified on:
- odoo-westin / westin-v19 (Docker: odoo-dev-app) — 14 products
- odoo-mobility / mobility (Docker: odoo-mobility-app) — 14 products
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>