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)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Reports',
|
'name': 'Fusion Plating — Reports',
|
||||||
'version': '19.0.7.15.0',
|
'version': '19.0.7.16.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
'summary': 'PDF reports for Fusion Plating: quote, SO, WO, packing, BoL, CoC, invoice, receipt, quality + compliance.',
|
||||||
'depends': [
|
'depends': [
|
||||||
|
|||||||
@@ -22,25 +22,42 @@ class FpWoScanController(http.Controller):
|
|||||||
def wo_scan_redirect(self, wo_id, **kwargs):
|
def wo_scan_redirect(self, wo_id, **kwargs):
|
||||||
"""Redirect a scanned sticker to the right backend form.
|
"""Redirect a scanned sticker to the right backend form.
|
||||||
|
|
||||||
Stickers are printed from two sources — mrp.workorder (WO) and
|
Resolution order:
|
||||||
mrp.production (MO) — and both embed their own numeric id in
|
1. fp.job mapped from this MO id via legacy_mrp_production_id
|
||||||
the QR. Try the MO table first (operators live on the MO
|
(post-migration: physical stickers still encode the old MO
|
||||||
form — customer, SO, all WOs visible) and fall back to WO.
|
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()
|
env = request.env
|
||||||
WO = request.env['mrp.workorder'].sudo()
|
|
||||||
|
|
||||||
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:
|
if mo:
|
||||||
return request.redirect(
|
return request.redirect(
|
||||||
'/odoo/action-mrp.mrp_production_action/%d' % mo.id
|
'/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:
|
if wo:
|
||||||
return request.redirect(
|
return request.redirect(
|
||||||
'/odoo/action-mrp.action_mrp_workorder/%d' % wo.id
|
'/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')
|
return request.redirect('/odoo/manufacturing/work-orders')
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Shop Floor',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.17.0.0',
|
'version': '19.0.18.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||||
'first-piece inspection gates.',
|
'first-piece inspection gates.',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export class QrScanner extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.action = useService("action");
|
|
||||||
this.notification = useService("notification");
|
this.notification = useService("notification");
|
||||||
this.videoRef = useRef("video");
|
this.videoRef = useRef("video");
|
||||||
const hasNative = typeof BarcodeDetector !== "undefined";
|
const hasNative = typeof BarcodeDetector !== "undefined";
|
||||||
@@ -51,6 +50,7 @@ export class QrScanner extends Component {
|
|||||||
open: false,
|
open: false,
|
||||||
error: null,
|
error: null,
|
||||||
manualUrl: "",
|
manualUrl: "",
|
||||||
|
detected: "", // last decoded value (for user feedback)
|
||||||
// True whenever ANY decoder (native or jsQR) is available.
|
// True whenever ANY decoder (native or jsQR) is available.
|
||||||
// Drives the template: when true we show the camera <video>;
|
// Drives the template: when true we show the camera <video>;
|
||||||
// when false we fall through to the manual paste UI only.
|
// 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
|
* Route a decoded value to the right backend page.
|
||||||
* the matching fp.job form. Accepts /fp/job/<id> and /fp/wo/<id>
|
*
|
||||||
* (legacy alias from older mrp.workorder stickers — both now point
|
* Stickers encode either /fp/job/<fp.job.id> (new) or
|
||||||
* at fp.job).
|
* /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) {
|
_handleCode(rawValue) {
|
||||||
const m = (rawValue || "").match(/\/fp\/(?:job|wo)\/(\d+)/);
|
const value = (rawValue || "").trim();
|
||||||
if (!m) {
|
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 =
|
this.state.error =
|
||||||
"QR doesn't look like a job sticker. Got: " +
|
"Decoded URL doesn't look like a sticker: " + value.slice(0, 80);
|
||||||
(rawValue || "").slice(0, 80);
|
return;
|
||||||
|
} else {
|
||||||
|
this.state.error =
|
||||||
|
"QR doesn't look like a job sticker. Got: " + value.slice(0, 80);
|
||||||
this.state.manualUrl = "";
|
this.state.manualUrl = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const jobId = parseInt(m[1], 10);
|
|
||||||
this._stopCamera();
|
this._stopCamera();
|
||||||
this.state.open = false;
|
this.state.open = false;
|
||||||
this.action.doAction({
|
this.notification.add("Opening " + target, { type: "success" });
|
||||||
type: "ir.actions.act_window",
|
// Full navigation — the server-side controller resolves the id
|
||||||
res_model: "fp.job",
|
// to the right record (works for both new fp.job stickers and
|
||||||
res_id: jobId,
|
// legacy mrp.production / mrp.workorder stickers).
|
||||||
view_mode: "form",
|
window.location.href = target;
|
||||||
views: [[false, "form"]],
|
|
||||||
target: "current",
|
|
||||||
});
|
|
||||||
this.notification.add("Opened job " + jobId, { type: "success" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,18 +65,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_qr_error,
|
.o_fp_qr_error,
|
||||||
.o_fp_qr_warn {
|
.o_fp_qr_warn,
|
||||||
|
.o_fp_qr_detected {
|
||||||
padding: $fp-space-2 $fp-space-3;
|
padding: $fp-space-2 $fp-space-3;
|
||||||
border-radius: $fp-radius-sm;
|
border-radius: $fp-radius-sm;
|
||||||
background: $fp-card-soft;
|
background: $fp-card-soft;
|
||||||
color: $fp-ink-soft;
|
color: $fp-ink-soft;
|
||||||
font-size: $fp-text-sm;
|
font-size: $fp-text-sm;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_qr_error {
|
.o_fp_qr_error {
|
||||||
border-left: 3px solid $fp-bad;
|
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 {
|
.o_fp_qr_manual {
|
||||||
border-top: 1px solid $fp-border;
|
border-top: 1px solid $fp-border;
|
||||||
padding-top: $fp-space-3;
|
padding-top: $fp-space-3;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<video t-if="state.canScan" t-ref="video"
|
<video t-if="state.canScan" t-ref="video"
|
||||||
class="o_fp_qr_video" muted="true" playsinline="true"/>
|
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">
|
<div t-if="state.error" class="o_fp_qr_error">
|
||||||
<i class="fa fa-exclamation-triangle me-1"/>
|
<i class="fa fa-exclamation-triangle me-1"/>
|
||||||
<span t-esc="state.error"/>
|
<span t-esc="state.error"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user