fix(shopfloor,reports): make QR scan actually navigate after decode

Two bugs were colluding to make iPhone scans look like "nothing
happens":

1. The in-app scanner was calling action.doAction({res_model: 'fp.job',
   res_id: <decoded-id>}). Old physical stickers (still on every box)
   encode /fp/wo/<mrp.production.id> — that id space doesn't match
   fp.job, so the form opened on a non-existent record and silently
   showed nothing. New /fp/job/<id> stickers happened to work because
   the IDs lined up by coincidence.

2. The /fp/wo/<id> controller redirected to mrp.production / mrp.workorder
   forms, both of which still exist as legacy records but aren't the
   canonical source of truth post-migration.

Fix:
- qr_scanner._handleCode now navigates via window.location.href instead
  of action.doAction. It hands /fp/job/<n> and /fp/wo/<n> URLs straight
  to the existing server-side controllers, which know how to resolve
  the right record. Bare numeric ids pasted manually -> /fp/job/<n>.
  Anything else surfaces the decoded text as an error so the operator
  can see decode worked but the value isn't a sticker.

- Modal now shows "Detected: <value>" the moment a code is decoded
  (before navigation), so even on slow phones the operator sees
  immediate feedback that the camera read the QR.

- wo_scan.py now resolves in this order:
    1. fp.job by legacy_mrp_production_id (migration-aware — old
       stickers route to the new model)
    2. mrp.production direct browse
    3. mrp.workorder direct browse
    4. fall back to /odoo/plating-jobs (or work-orders list)

Versions: shopfloor 19.0.17.0.0 -> 19.0.18.0.0,
          reports   19.0.7.15.0 -> 19.0.7.16.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 13:14:06 -04:00
parent ecac43eef4
commit b93633d728
6 changed files with 99 additions and 31 deletions

View File

@@ -42,7 +42,6 @@ export class QrScanner extends Component {
};
setup() {
this.action = useService("action");
this.notification = useService("notification");
this.videoRef = useRef("video");
const hasNative = typeof BarcodeDetector !== "undefined";
@@ -51,6 +50,7 @@ export class QrScanner extends Component {
open: false,
error: null,
manualUrl: "",
detected: "", // last decoded value (for user feedback)
// True whenever ANY decoder (native or jsQR) is available.
// Drives the template: when true we show the camera <video>;
// when false we fall through to the manual paste UI only.
@@ -213,31 +213,65 @@ export class QrScanner extends Component {
}
/**
* Parse a decoded value (URL or raw id-bearing string) and route to
* the matching fp.job form. Accepts /fp/job/<id> and /fp/wo/<id>
* (legacy alias from older mrp.workorder stickers — both now point
* at fp.job).
* Route a decoded value to the right backend page.
*
* Stickers encode either /fp/job/<fp.job.id> (new) or
* /fp/wo/<mrp.production.id|mrp.workorder.id> (legacy — still on
* physical boxes from before the migration). Both URLs are
* handled by server-side controllers (job_scan.py / wo_scan.py)
* that resolve the correct record and redirect to its form.
*
* Rather than guessing ID spaces in the browser, we just navigate
* to the URL and let the controllers do the routing. This means:
* - new stickers (/fp/job/<id>) -> fp.job form
* - old stickers (/fp/wo/<id>) -> fp.job (via legacy_mrp_production_id)
* or mrp.production fallback
* - plain ids pasted manually -> assumed to be fp.job
* - anything else -> show the decoded text as an
* error so the operator knows
* decode worked but the value
* isn't a sticker URL.
*/
_handleCode(rawValue) {
const m = (rawValue || "").match(/\/fp\/(?:job|wo)\/(\d+)/);
if (!m) {
const value = (rawValue || "").trim();
this.state.detected = value.slice(0, 120);
// Path or full URL containing /fp/job/<n> or /fp/wo/<n>.
const pathMatch = value.match(/\/fp\/(?:job|wo)\/\d+/);
let target = null;
if (pathMatch) {
// If the decoded value is a full URL, keep its origin so we
// don't break links that point at a different host.
// Otherwise navigate to the path on the current origin.
try {
const u = new URL(value);
target = u.origin + pathMatch[0];
} catch (e) {
target = pathMatch[0];
}
} else if (/^\d+$/.test(value)) {
// Bare numeric id pasted manually -> treat as fp.job id.
target = "/fp/job/" + value;
} else if (/^https?:\/\//i.test(value)) {
// Some other URL on (presumably) this Odoo. Let the user
// see what was decoded; don't blindly navigate to arbitrary
// off-host URLs.
this.state.error =
"QR doesn't look like a job sticker. Got: " +
(rawValue || "").slice(0, 80);
"Decoded URL doesn't look like a sticker: " + value.slice(0, 80);
return;
} else {
this.state.error =
"QR doesn't look like a job sticker. Got: " + value.slice(0, 80);
this.state.manualUrl = "";
return;
}
const jobId = parseInt(m[1], 10);
this._stopCamera();
this.state.open = false;
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fp.job",
res_id: jobId,
view_mode: "form",
views: [[false, "form"]],
target: "current",
});
this.notification.add("Opened job " + jobId, { type: "success" });
this.notification.add("Opening " + target, { type: "success" });
// Full navigation — the server-side controller resolves the id
// to the right record (works for both new fp.job stickers and
// legacy mrp.production / mrp.workorder stickers).
window.location.href = target;
}
}

View File

@@ -65,18 +65,30 @@
}
.o_fp_qr_error,
.o_fp_qr_warn {
.o_fp_qr_warn,
.o_fp_qr_detected {
padding: $fp-space-2 $fp-space-3;
border-radius: $fp-radius-sm;
background: $fp-card-soft;
color: $fp-ink-soft;
font-size: $fp-text-sm;
word-break: break-all;
}
.o_fp_qr_error {
border-left: 3px solid $fp-bad;
}
.o_fp_qr_detected {
border-left: 3px solid $fp-ok;
color: $fp-ink;
.o_fp_qr_detected_val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 600;
}
}
.o_fp_qr_manual {
border-top: 1px solid $fp-border;
padding-top: $fp-space-3;

View File

@@ -32,6 +32,11 @@
</div>
<video t-if="state.canScan" t-ref="video"
class="o_fp_qr_video" muted="true" playsinline="true"/>
<div t-if="state.detected" class="o_fp_qr_detected">
<i class="fa fa-check-circle me-1"/>
<span>Detected: </span>
<span class="o_fp_qr_detected_val" t-esc="state.detected"/>
</div>
<div t-if="state.error" class="o_fp_qr_error">
<i class="fa fa-exclamation-triangle me-1"/>
<span t-esc="state.error"/>