feat(numbering): wire CoC/RCV/DLV/PU into parent-numbered mixin + rename counters

Per-model counter fields on sale.order renamed to x_fc_pn_*_count
to avoid collision with pre-existing compute fields of the same
short name in bridge_mrp / receiving / configurator (silent
compute-override was suppressing the storage). 4 child models
(fp.certificate, fp.receiving, fusion.plating.delivery,
fusion.plating.pickup.request) now derive names as PFX-<parent>
with -NN suffix from the 2nd onward.

fusion.plating.pickup.request gains a sale_order_id field
(optional) so pickups created against an SO get parent-derived
names, while standalone pickups (pre-SO) fall back to PU/YYYY/NNNN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-12 13:30:37 -04:00
parent 765a0a4c82
commit 0d85063b5e
17 changed files with 489 additions and 64 deletions

View File

@@ -69,6 +69,7 @@ export class FpRecordInputsDialog extends Component {
saving: false,
stepName: "",
jobName: "",
recipeRootId: false,
rows: [],
// Operator's persisted initials — pre-filled into signature
// / "Reviewer Initials" prompts on load. When the operator
@@ -103,6 +104,7 @@ export class FpRecordInputsDialog extends Component {
}
this.state.stepName = data.step.name;
this.state.jobName = data.job.name;
this.state.recipeRootId = data.recipe_root_id || false;
this.state.userInitials = data.user_initials || "";
this.state.instructionsHtml = data.instructions_html || "";
this.state.instructionImages = data.instruction_images || [];
@@ -193,13 +195,14 @@ export class FpRecordInputsDialog extends Component {
isSelection(row) { return row.input_type === "selection"; }
isPassFail(row) { return row.input_type === "pass_fail"; }
isSignature(row) { return row.input_type === "signature"; }
// Fallback to text for anything else (text, time_hms, ...)
isTimeHms(row) { return row.input_type === "time_hms"; }
// Fallback to text for anything else
isText(row) {
return !this.isNumeric(row) && !this.isBoolean(row)
&& !this.isDate(row) && !this.isPhoto(row)
&& !this.isMulti(row) && !this.isPanel(row)
&& !this.isSelection(row) && !this.isPassFail(row)
&& !this.isSignature(row);
&& !this.isSignature(row) && !this.isTimeHms(row);
}
// Friendly label for the type pill — defaults to the raw key when no
@@ -208,6 +211,60 @@ export class FpRecordInputsDialog extends Component {
return TYPE_LABELS[row.input_type] || row.input_type || "Text";
}
// Step granularity for <input type="number"> — drives the up/down
// arrow increment AND the typed-decimal validity. Defaults of step=1
// make tablet entry painful when the spec is 0.03 0.05 mil because
// every arrow press jumps a full unit. Derive from the recipe-author's
// target_min / target_max precision so operator arrow-taps move in the
// same decimal magnitude the spec was written in. Falls back to
// input-type defaults when no targets are set.
stepFor(row) {
const decimals = Math.max(
this._fpCountDecimals(row.target_min),
this._fpCountDecimals(row.target_max),
);
if (decimals > 0) {
return Math.pow(10, -decimals).toFixed(decimals);
}
const t = row.input_type || "";
if (t === "thickness" || t === "multi_point_thickness") return "0.0001";
if (t === "ph") return "0.01";
if (t === "temperature" || t === "time_seconds") return "1";
return "any";
}
_fpCountDecimals(n) {
if (n === null || n === undefined || n === "" || n === 0) return 0;
const s = String(n);
const idx = s.indexOf(".");
if (idx < 0) return 0;
// Trim trailing zeros so "0.0500" doesn't look like 4-decimals
// when the author actually wrote 2-decimal precision.
return s.slice(idx + 1).replace(/0+$/, "").length;
}
// Jump from the runtime dialog into the Simple Recipe Editor on the
// EXACT recipe variant this job step is bound to. Closes the dialog
// (operator returns by re-opening Record Inputs after editing). The
// intent is to remove the "I edited the recipe but nothing changed"
// confusion — they were editing a sibling variant.
async openSimpleEditor() {
if (!this.state.recipeRootId) {
this.notification.add(
_t("No recipe linked to this step yet."),
{ type: "warning" },
);
return;
}
this.props.close();
await this.action.doAction({
type: "ir.actions.client",
tag: "fp_simple_recipe_editor",
name: _t("Edit Recipe"),
context: { recipe_id: this.state.recipeRootId },
});
}
// True when the recipe author defined BOTH target_min and target_max
// on the prompt — the signal that the operator is expected to capture
// a range (multiple readings → record their min and max observation).

View File

@@ -11,6 +11,12 @@
Job <t t-esc="state.jobName"/>
</span>
</div>
<button t-if="state.recipeRootId"
class="btn btn-link o_fp_ri_edit_recipe"
title="Edit this step's prompts (target ranges, type, options) in the Simple Recipe Editor."
t-on-click="openSimpleEditor">
<i class="fa fa-pencil me-1"/> Edit Recipe
</button>
</div>
</t>
@@ -116,7 +122,7 @@
class="o_fp_ri_numeric">
<input type="number"
class="o_fp_ri_input o_fp_ri_input_numeric"
step="any"
t-att-step="stepFor(row)"
t-model.number="row.value_number"
t-att-placeholder="row.target_min or '0.00'"/>
<t t-set="hint" t-value="rangeHint(row)"/>
@@ -136,7 +142,7 @@
<span class="o_fp_ri_dual_label">Min Reading</span>
<input type="number"
class="o_fp_ri_input o_fp_ri_input_numeric"
step="any"
t-att-step="stepFor(row)"
t-model.number="row.value_min"
t-att-placeholder="row.target_min or '0.00'"/>
</label>
@@ -144,7 +150,7 @@
<span class="o_fp_ri_dual_label">Max Reading</span>
<input type="number"
class="o_fp_ri_input o_fp_ri_input_numeric"
step="any"
t-att-step="stepFor(row)"
t-model.number="row.value_max"
t-att-placeholder="row.target_max or '0.00'"/>
</label>
@@ -167,7 +173,7 @@
<span class="o_fp_ri_dual_label">Min Reading</span>
<input type="number"
class="o_fp_ri_input o_fp_ri_input_numeric"
step="any"
t-att-step="stepFor(row)"
t-model.number="row.value_min"
t-att-placeholder="row.target_min or '0.00'"/>
</label>
@@ -175,7 +181,7 @@
<span class="o_fp_ri_dual_label">Max Reading</span>
<input type="number"
class="o_fp_ri_input o_fp_ri_input_numeric"
step="any"
t-att-step="stepFor(row)"
t-model.number="row.value_max"
t-att-placeholder="row.target_max or '0.00'"/>
</label>
@@ -301,19 +307,19 @@
<div t-if="isMulti(row)" class="o_fp_ri_multi">
<div class="o_fp_ri_multi_grid">
<label>R1
<input type="number" step="any" t-model.number="row.point_1"/>
<input type="number" t-att-step="stepFor(row)" t-model.number="row.point_1"/>
</label>
<label>R2
<input type="number" step="any" t-model.number="row.point_2"/>
<input type="number" t-att-step="stepFor(row)" t-model.number="row.point_2"/>
</label>
<label>R3
<input type="number" step="any" t-model.number="row.point_3"/>
<input type="number" t-att-step="stepFor(row)" t-model.number="row.point_3"/>
</label>
<label>R4
<input type="number" step="any" t-model.number="row.point_4"/>
<input type="number" t-att-step="stepFor(row)" t-model.number="row.point_4"/>
</label>
<label>R5
<input type="number" step="any" t-model.number="row.point_5"/>
<input type="number" t-att-step="stepFor(row)" t-model.number="row.point_5"/>
</label>
<div class="o_fp_ri_multi_avg">
<span class="text-muted">Avg</span>
@@ -325,20 +331,28 @@
<!-- Bath chemistry panel — pH / conc / temp / bath -->
<div t-if="isPanel(row)" class="o_fp_ri_panel">
<label>pH
<input type="number" step="any" t-model.number="row.panel_ph"/>
<input type="number" step="0.01" t-model.number="row.panel_ph"/>
</label>
<label>Concentration
<input type="number" step="any" t-model.number="row.panel_concentration"/>
<input type="number" step="0.1" t-model.number="row.panel_concentration"/>
</label>
<label>Temperature
<input type="number" step="any" t-model.number="row.panel_temperature"/>
<input type="number" step="1" t-model.number="row.panel_temperature"/>
</label>
<label>Bath ID
<input type="text" t-model="row.panel_bath_id"/>
</label>
</div>
<!-- Text fallback (text, signature, time_hms, anything else) -->
<!-- Time (HH:MM:SS) — native time picker with seconds.
Mobile/tablet browsers surface the OS time wheel. -->
<input t-if="isTimeHms(row)"
type="time"
step="1"
class="o_fp_ri_input o_fp_ri_input_text"
t-model="row.value_text"/>
<!-- Text fallback (text, signature, anything else) -->
<input t-if="isText(row)"
type="text"
class="o_fp_ri_input o_fp_ri_input_text"