fix(fusion_repairs): persona-driven workflow audit - 6 real bugs
Full end-to-end walk acting as customer, CS rep, dispatcher, technician,
and manager surfaced 6 real bugs (1 critical state-machine, 4 missing UX
wires, 1 docstring). Server endpoints existed for everything but several
were not wired into the templates.
B1 (HIGH) - Visit-report wizard never closed the repair
Tech submitted visit -> state stayed 'draft' -> x_fc_done_at never
stamped -> NPS cron never fired -> the whole post-visit flow died
silently. Customers never got their NPS email.
Fix: action_confirm() now drives the Odoo native state machine
draft -> action_validate (with _action_repair_confirm fallback) ->
action_repair_start -> action_repair_end. Each step guarded by the
current state and exception-logged. Leaves the repair open if:
- requires_requote=True (variance flag - office must re-quote)
- no_show=True (office reschedules)
- x_fc_is_quote_only (still a quote)
- found_another_issue spawned a stub
Posts a clear chatter line on success or failure.
Verified: e2e walk now shows state=done + x_fc_done_at stamped +
NPS cron fires + flags x_fc_nps_email_sent=True.
B2 (HIGH) - /repair/new form never called /repair/self_check
The AI self-check engine was the headline weekend feature but it was
invisible to the client. The endpoint worked server-side, just had
no frontend.
Fix: new portal_client_repair.js (Interaction class, registered on
registry.category('public.interactions')). 'Try 1-3 safe self-check
steps first' button POSTs to /repair/self_check, renders steps via
createElement + textContent (no innerHTML - all server output is
treated as untrusted text). Shows the AI's safety disclaimer on
every result. On escalate_immediately, shows a clear 'submit the
form, we'll come to you' message instead of the steps.
Verified: HTTP POST returns full JSON with instruction +
expected_result + disclaimer; new button + result panel appear in
rendered HTML.
B3 (HIGH) - No phone-lookup UI for returning clients
Same problem - endpoint existed but no UI. Returning clients had to
retype everything from scratch.
Fix:
- lookup_phone now returns a 'partners' array (id, name, email,
street, city) - cap of 3 results, rate-limited, every match logged
at INFO level for audit. Privacy compromise: a phone holder
deserves to see their own pre-fill; rate limit caps harvesting.
- JS lookup widget at the top of the form posts to /repair/lookup_phone
and pre-fills the 5 contact fields + writes the partner_id to a
hidden #fr_known_partner_id input.
- controller /repair/submit now trusts known_partner_id if present
(skips the phone re-match) so we don't create duplicate partners
when the lookup widget already identified the right one.
Verified: HTTP POST returns the 2 partner records we have for
+19055551234 with full id/name/email/street/city.
B4 (MEDIUM) - /repair?sn=<serial> from QR sticker did nothing
Spec: 'Client scans QR sticker - portal pre-fills the unit info.'
Reality: the form had no serial field; ?sn= was ignored.
Fix: new _resolve_serial_info(serial) on the controller resolves
the lot via stock.lot.search([('name','=',sn)]) and returns
{serial, lot_id, product_id, product_name, category_id}. Both
/repair (landing) and /repair/new pass it as serial_info template
context. Templates show 'Recognized X (Serial: Y)' + auto-select
the matching category in the dropdown. Hidden #fr_serial_number
carries it through to /repair/submit, which attaches the lot_id +
uses the QR category as fallback if user didn't pick one.
Verified: ?sn=stella23-20040164 produces 'Pre-filled from QR scan:'
banner + hidden input populated.
B5 (MEDIUM) - No upsell after submit
Spec required an upsell - 'reduce future calls'. Page was a bare
'Got it'.
Fix: /repair/thanks now shows a 2-card layout:
- 'Want to avoid this next time?' with 4 bullets (priority booking,
free inspection cert, discounted parts, annual reminder) +
'See our maintenance plans' CTA to /shop?category=maintenance
- 'What happens next' 4-step bulleted explanation
Verified: both cards render.
B6 (LOW) - SyntaxWarning '\-->' in repair_service_plan.py
Made the module docstring a raw string (r''') so the ASCII flowchart
arrows don't trigger Python's invalid-escape-sequence warning.
Bumped to 19.0.1.8.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -228,6 +228,25 @@ class RepairVisitReportWizard(models.TransientModel):
|
||||
if not repair.x_fc_is_quote_only:
|
||||
self._burn_service_plan_visit(repair)
|
||||
|
||||
# BUG-B1 fix: actually close the repair so the whole downstream chain
|
||||
# (NPS cron, dashboard "done this month" stats, customer survey) fires.
|
||||
# Leave open if requote needed - the office will re-quote and the tech
|
||||
# will revisit. No-show or quote-only also stays open.
|
||||
if (not self.requires_requote
|
||||
and not self.no_show
|
||||
and not repair.x_fc_is_quote_only
|
||||
and not stub):
|
||||
self._close_repair(repair)
|
||||
elif self.no_show:
|
||||
# No-show: drop back to draft for re-scheduling.
|
||||
repair.message_post(body=Markup(_(
|
||||
'Repair kept <b>open</b> due to no-show. Office to reschedule.'
|
||||
)))
|
||||
elif self.requires_requote:
|
||||
repair.message_post(body=Markup(_(
|
||||
'Repair kept <b>open</b> pending re-quote (variance flag).'
|
||||
)))
|
||||
|
||||
# If a stub was spawned, open it directly so the tech can fill in details.
|
||||
# Otherwise, if a certificate was issued, jump to it so the tech can print.
|
||||
if stub:
|
||||
@@ -287,6 +306,42 @@ class RepairVisitReportWizard(models.TransientModel):
|
||||
'Replaced part serials captured:<br/><pre>%s</pre>'
|
||||
)) % self.parts_serial_capture.strip())
|
||||
|
||||
def _close_repair(self, repair):
|
||||
"""Drive the Odoo native state machine from draft -> done.
|
||||
|
||||
Odoo 19 sequence: draft -> action_validate (confirmed/under_repair)
|
||||
-> action_repair_start (under_repair) -> action_repair_end (done).
|
||||
Calls are guarded - silently re-runs only the missing steps.
|
||||
"""
|
||||
try:
|
||||
if repair.state == 'draft':
|
||||
# action_validate is the standard entry path; if the product is
|
||||
# storable it expects reservations etc., so fall back to the
|
||||
# simpler _action_repair_confirm() helper if validate refuses.
|
||||
try:
|
||||
repair.action_validate()
|
||||
except Exception as e:
|
||||
_logger.info(
|
||||
'action_validate skipped for %s: %s; using internal confirm.',
|
||||
repair.name, e,
|
||||
)
|
||||
repair._action_repair_confirm()
|
||||
if repair.state == 'confirmed':
|
||||
repair.action_repair_start()
|
||||
if repair.state == 'under_repair':
|
||||
repair.action_repair_end()
|
||||
repair.message_post(body=Markup(_(
|
||||
'Visit report submitted - repair closed by <b>%s</b>.'
|
||||
)) % (self.technician_id.name or self.env.user.name))
|
||||
except Exception as e:
|
||||
_logger.exception(
|
||||
'Visit report could not close repair %s automatically: %s',
|
||||
repair.name, e,
|
||||
)
|
||||
repair.message_post(body=Markup(_(
|
||||
'<b>Could not auto-close repair</b>: %s. Office must close manually.'
|
||||
)) % str(e))
|
||||
|
||||
def _burn_service_plan_visit(self, repair):
|
||||
"""M5: deduct one visit from the most-recently-active service plan
|
||||
covering this repair. Quietly no-ops if the client has no plan."""
|
||||
|
||||
Reference in New Issue
Block a user