feat(jobs): Record Inputs OWL Dialog (v4) — replaces list-as-cards hack
Scrapped the v2/v3 form-view + list-as-cards CSS approach after
extensive failure to make Odoo's editable list look like cards.
Built a proper OWL Dialog component instead, mirroring the pattern
used by fusion_plating_shopfloor's move_parts_dialog.js.
What changed
============
* New OWL Dialog: fp_record_inputs_dialog.js
- Loads step + prompt definitions via /fp/record_inputs/load
- Renders each prompt as a semantic <div class="o_fp_ri_card">
- Per-row widget chosen by input_type:
numeric/temperature/thickness/time_seconds/ph -> number input
boolean/pass_fail -> custom CSS toggle (clearer than Bootstrap)
date -> datetime-local input
photo -> file picker w/ preview + clear
multi_point_thickness -> 5-cell grid + live average
bath_chemistry_panel -> pH/Conc/Temp/Bath grid
selection -> dropdown sourced from selection_options
text/signature/... -> text input
- Live in-range hint for numeric prompts
("in range" / "below target" / "above target")
- Save validates ad-hoc rows have a Prompt label
- Save dispatches the next_action returned by the wizard model
(e.g. action_finish_and_advance for the Finish & Next flow)
* New XML template: fp_record_inputs_dialog.xml
Full DOM control. No fighting Odoo's list view, no class-stripping
bugs from canUseFormatter, no read-mode-vs-edit-mode CSS dance.
* New SCSS: fp_record_inputs_dialog.scss
- Dark mode aware (compile-time @if $o-webclient-color-scheme==dark)
- Pure semantic selectors (.o_fp_ri_card, .o_fp_ri_input, etc.)
- 14 surface tokens with light/dark hex pairs
- Tablet polish via @media (max-width: 768px)
- Custom toggle widget (no <input type="checkbox"> hidden trick)
* New controller: controllers/record_inputs.py
- /fp/record_inputs/load: returns step + prompts payload
- /fp/record_inputs/commit: creates a wizard, populates lines,
calls action_commit (reuses existing audit-trail / synthetic
move semantics — no commit logic duplicated)
* fp_job_step.py wired to dispatch the new action
- _fp_open_input_wizard returns
{ type: 'ir.actions.client', tag: 'fp_record_inputs_dialog' }
- action_open_input_wizard same
- Contract-review redirect gate preserved (Sub 4 work intact)
* Manifest registers JS/XML/SCSS in BOTH backend + dark bundles
per the dark-mode pattern in CLAUDE.md.
What was kept
=============
* fp.job.step.input.wizard TransientModel — UNCHANGED. The new
controller's commit endpoint creates a wizard record and calls
action_commit() on it, so all the audit-trail / synthetic-move
/ chatter logic stays in Python where it belongs.
* v2 + v3 form views still exist in the XML file. If the OWL
dialog ever fails, switch action_open_input_wizard back to
ir.actions.act_window with view_id=v2 or v3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating_jobs.FpRecordInputsDialog">
|
||||
<Dialog size="'lg'" contentClass="'o_fp_ri_dialog_content'">
|
||||
<t t-set-slot="header">
|
||||
<div class="o_fp_ri_header">
|
||||
<div class="o_fp_ri_header_titles">
|
||||
<h4 class="o_fp_ri_step_name" t-esc="state.stepName"/>
|
||||
<span class="o_fp_ri_job_label">
|
||||
Job <t t-esc="state.jobName"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div t-if="state.loading" class="o_fp_ri_loading">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
<span class="ms-2">Loading prompts...</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div t-elif="!state.rows.length" class="o_fp_ri_empty">
|
||||
<p>No measurement prompts on this step.</p>
|
||||
<button class="btn btn-secondary" t-on-click="addAdHocRow">
|
||||
<i class="fa fa-plus me-1"/> Add a measurement
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Cards -->
|
||||
<div t-else="" class="o_fp_ri_cards">
|
||||
<t t-foreach="state.rows" t-as="row" t-key="row_index">
|
||||
<div class="o_fp_ri_card"
|
||||
t-att-class="{ 'o_fp_ri_card_required': row.required }">
|
||||
|
||||
<!-- Card header — prompt name + meta pills -->
|
||||
<div class="o_fp_ri_card_head">
|
||||
<div class="o_fp_ri_prompt">
|
||||
<!-- Authored prompt: read-only label -->
|
||||
<span t-if="row.is_authored"
|
||||
class="o_fp_ri_prompt_label">
|
||||
<span t-esc="row.name"/>
|
||||
<span t-if="row.required" class="o_fp_ri_required_mark" title="Required">*</span>
|
||||
</span>
|
||||
<!-- Ad-hoc prompt: editable -->
|
||||
<input t-else=""
|
||||
type="text"
|
||||
class="o_fp_ri_prompt_input"
|
||||
placeholder="Measurement name…"
|
||||
t-model="row.name"/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_ri_meta">
|
||||
<span class="o_fp_ri_pill o_fp_ri_pill_type"
|
||||
t-esc="row.input_type"/>
|
||||
<span t-if="row.target_unit"
|
||||
class="o_fp_ri_pill o_fp_ri_pill_unit"
|
||||
t-esc="row.target_unit"/>
|
||||
</div>
|
||||
|
||||
<button t-if="!row.is_authored"
|
||||
class="o_fp_ri_remove btn btn-link"
|
||||
title="Remove this measurement"
|
||||
t-on-click="() => this.removeRow(row_index)">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Target range hint (if recipe author set one) -->
|
||||
<div t-if="(row.target_min or row.target_max) and isNumeric(row)"
|
||||
class="o_fp_ri_target">
|
||||
Target:
|
||||
<strong>
|
||||
<t t-if="row.target_min" t-esc="row.target_min"/><t t-if="row.target_min and row.target_max">–</t><t t-if="row.target_max" t-esc="row.target_max"/>
|
||||
</strong>
|
||||
<span t-if="row.target_unit" class="ms-1 text-muted" t-esc="row.target_unit"/>
|
||||
</div>
|
||||
|
||||
<!-- Hint text from recipe author -->
|
||||
<div t-if="row.hint" class="o_fp_ri_hint" t-esc="row.hint"/>
|
||||
|
||||
<!-- Card body — live input widget per type -->
|
||||
<div class="o_fp_ri_card_body">
|
||||
|
||||
<!-- Numeric (number, temperature, thickness, time_seconds, ph) -->
|
||||
<div t-if="isNumeric(row)" class="o_fp_ri_numeric">
|
||||
<input type="number"
|
||||
class="o_fp_ri_input o_fp_ri_input_numeric"
|
||||
step="any"
|
||||
t-model.number="row.value_number"
|
||||
t-att-placeholder="row.target_min or '0.00'"/>
|
||||
<t t-set="hint" t-value="rangeHint(row)"/>
|
||||
<span t-if="hint"
|
||||
class="o_fp_ri_range_hint"
|
||||
t-att-class="'o_fp_ri_range_' + hint.kind"
|
||||
t-esc="hint.text"/>
|
||||
</div>
|
||||
|
||||
<!-- Boolean / pass-fail toggle -->
|
||||
<label t-if="isBoolean(row)" class="o_fp_ri_toggle">
|
||||
<input type="checkbox" t-model="row.value_boolean"/>
|
||||
<span class="o_fp_ri_toggle_track">
|
||||
<span class="o_fp_ri_toggle_thumb"/>
|
||||
</span>
|
||||
<span class="o_fp_ri_toggle_label"
|
||||
t-esc="row.value_boolean ? 'Yes' : 'No'"/>
|
||||
</label>
|
||||
|
||||
<!-- Date / time -->
|
||||
<input t-if="isDate(row)"
|
||||
type="datetime-local"
|
||||
class="o_fp_ri_input o_fp_ri_input_date"
|
||||
t-model="row.value_date"/>
|
||||
|
||||
<!-- Selection (uses recipe author's selection_options) -->
|
||||
<select t-if="isSelection(row)"
|
||||
class="o_fp_ri_input o_fp_ri_input_select"
|
||||
t-model="row.value_text">
|
||||
<option value="">— choose —</option>
|
||||
<t t-foreach="selectionOptions(row)" t-as="opt" t-key="opt">
|
||||
<option t-att-value="opt" t-esc="opt"/>
|
||||
</t>
|
||||
</select>
|
||||
|
||||
<!-- Photo upload -->
|
||||
<div t-if="isPhoto(row)" class="o_fp_ri_photo">
|
||||
<div t-if="!row.photo_value" class="o_fp_ri_photo_placeholder">
|
||||
<label class="btn btn-secondary">
|
||||
<i class="fa fa-camera me-2"/> Take or upload photo
|
||||
<input type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
hidden=""
|
||||
t-on-change="(ev) => this.onPhotoChange(row, ev)"/>
|
||||
</label>
|
||||
</div>
|
||||
<div t-else="" class="o_fp_ri_photo_preview">
|
||||
<img t-att-src="photoPreviewSrc(row)" alt="Captured photo"/>
|
||||
<button class="btn btn-sm btn-link text-danger"
|
||||
t-on-click="() => this.onPhotoClear(row)">
|
||||
<i class="fa fa-trash me-1"/> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-point thickness — 5 readings + live avg -->
|
||||
<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"/>
|
||||
</label>
|
||||
<label>R2
|
||||
<input type="number" step="any" t-model.number="row.point_2"/>
|
||||
</label>
|
||||
<label>R3
|
||||
<input type="number" step="any" t-model.number="row.point_3"/>
|
||||
</label>
|
||||
<label>R4
|
||||
<input type="number" step="any" t-model.number="row.point_4"/>
|
||||
</label>
|
||||
<label>R5
|
||||
<input type="number" step="any" t-model.number="row.point_5"/>
|
||||
</label>
|
||||
<div class="o_fp_ri_multi_avg">
|
||||
<span class="text-muted">Avg</span>
|
||||
<strong t-esc="multiPointAvg(row)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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"/>
|
||||
</label>
|
||||
<label>Concentration
|
||||
<input type="number" step="any" t-model.number="row.panel_concentration"/>
|
||||
</label>
|
||||
<label>Temperature
|
||||
<input type="number" step="any" 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) -->
|
||||
<input t-if="isText(row)"
|
||||
type="text"
|
||||
class="o_fp_ri_input o_fp_ri_input_text"
|
||||
t-model="row.value_text"
|
||||
placeholder="Enter value…"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Add-row CTA -->
|
||||
<button class="o_fp_ri_add_btn btn btn-link"
|
||||
t-on-click="addAdHocRow">
|
||||
<i class="fa fa-plus me-1"/> Add another measurement
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary"
|
||||
t-att-disabled="state.saving or state.loading"
|
||||
t-on-click="onSave">
|
||||
<i t-if="state.saving" class="fa fa-spinner fa-spin me-2"/>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="onCancel">
|
||||
Cancel
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user