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:
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Reports',
|
||||
'version': '19.0.7.15.0',
|
||||
'version': '19.0.7.16.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||
'depends': [
|
||||
|
||||
@@ -22,25 +22,42 @@ class FpWoScanController(http.Controller):
|
||||
def wo_scan_redirect(self, wo_id, **kwargs):
|
||||
"""Redirect a scanned sticker to the right backend form.
|
||||
|
||||
Stickers are printed from two sources — mrp.workorder (WO) and
|
||||
mrp.production (MO) — and both embed their own numeric id in
|
||||
the QR. Try the MO table first (operators live on the MO
|
||||
form — customer, SO, all WOs visible) and fall back to WO.
|
||||
Resolution order:
|
||||
1. fp.job mapped from this MO id via legacy_mrp_production_id
|
||||
(post-migration: physical stickers still encode the old MO
|
||||
id, but the canonical record is now an fp.job)
|
||||
2. mrp.production with this id (pre-migration callers, or if
|
||||
the legacy mapping wasn't run)
|
||||
3. mrp.workorder with this id (older stickers that encoded
|
||||
the WO id rather than the MO id)
|
||||
4. fall back to the jobs list so staff can search manually.
|
||||
"""
|
||||
MO = request.env['mrp.production'].sudo()
|
||||
WO = request.env['mrp.workorder'].sudo()
|
||||
env = request.env
|
||||
|
||||
mo = MO.browse(wo_id).exists()
|
||||
# 1) New native model — preferred when migration has run.
|
||||
if 'fp.job' in env and 'legacy_mrp_production_id' in env['fp.job']._fields:
|
||||
job = env['fp.job'].sudo().search(
|
||||
[('legacy_mrp_production_id', '=', wo_id)], limit=1)
|
||||
if job:
|
||||
return request.redirect(
|
||||
'/odoo/action-fusion_plating.action_fp_job/%d' % job.id
|
||||
)
|
||||
|
||||
# 2) Legacy MO form (pre-migration or non-migrated records).
|
||||
mo = env['mrp.production'].sudo().browse(wo_id).exists()
|
||||
if mo:
|
||||
return request.redirect(
|
||||
'/odoo/action-mrp.mrp_production_action/%d' % mo.id
|
||||
)
|
||||
|
||||
wo = WO.browse(wo_id).exists()
|
||||
# 3) Legacy WO form.
|
||||
wo = env['mrp.workorder'].sudo().browse(wo_id).exists()
|
||||
if wo:
|
||||
return request.redirect(
|
||||
'/odoo/action-mrp.action_mrp_workorder/%d' % wo.id
|
||||
)
|
||||
|
||||
# Neither resolved — land on the WO list so staff can search manually.
|
||||
# 4) Fall back: native jobs list if it exists, otherwise WO list.
|
||||
if 'fp.job' in env:
|
||||
return request.redirect('/odoo/plating-jobs')
|
||||
return request.redirect('/odoo/manufacturing/work-orders')
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.17.0.0',
|
||||
'version': '19.0.18.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||
'first-piece inspection gates.',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user