feat(jobs): step details quick-look modal for backend managers
Click a step's name in the embedded job-form list → opens a read-only
modal with everything a manager wants in one scroll: equipment,
schedule, master collect-measurements banner, operator instructions
(rich-text from recipe_node.description), measurement prompts list,
and values recorded so far.
Implementation: separate read-only form view bound to the embedded
field via context={'form_view_ref': '...'}. The standalone editable
form view stays registered for the Job Steps menu, so direct
navigation still loads the editable variant.
Three new computed/related fields on fp.job.step:
- quick_look_instructions (Html, related from recipe_node_id.description)
- quick_look_prompt_ids (filtered+sorted recipe_node.input_ids, step_input only)
- quick_look_recorded_value_ids (search across moves: input_value rows
whose move.from_step_id == self.id)
Plus a small action_open_full_form method that escapes from the modal
to the editable form when the manager actually needs to edit.
Edge cases:
- No recipe_node_id → instructions panel shows empty-state hint
- collect_measurements=False → amber banner: "Master switch off — no
values will be collected at runtime"
- Multiple moves on same step → values list shows all, newest first
Spec: docs/superpowers/specs/2026-04-30-step-details-modal-design.md.
Verified on entech: step "11. Hard Anodize Type III" populates with
516 chars instructions + 7 prompts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
# Step Details Quick-Look Modal — Design
|
||||||
|
|
||||||
|
**Date:** 2026-04-30
|
||||||
|
**Module:** `fusion_plating_jobs`
|
||||||
|
**Status:** Approved, implementing inline.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The job's embedded step list shows minimal columns (Name · Work Centre · State · Times · Qty · action buttons). Backend managers and supervisors viewing a job have no fast way to see what each step entails — instructions written by the office, prompts the operator will be asked, and values already recorded so far. The existing editable form view (registered for the Job Steps menu) is editable + has 4 tabs — too dense for a "what's this step about?" glance, and presents a regression risk if a manager accidentally edits state.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Click a step's name in the embedded job-form list → open a read-only quick-look modal with all the manager-relevant context for that step in one scroll.
|
||||||
|
2. Preserve the existing editable form for the standalone Job Steps menu / direct navigation.
|
||||||
|
3. Zero risk of accidental edits — the modal is read-only by construction (`edit="false" create="false" delete="false"`).
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Editable fields, time-logs, NCRs, predecessor status, action buttons (Start/Skip/Move). Available via "Open Full Form" link in the modal footer.
|
||||||
|
- New TransientModel mirroring step fields (rejected during brainstorm — duplication).
|
||||||
|
- Inline-row expansion / accordion (rejected — heavier OWL work).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Component | What it does |
|
||||||
|
|---|---|
|
||||||
|
| **Three new related/computed fields on `fp.job.step`** | `quick_look_instructions` (Html, related from `recipe_node_id.description`), `quick_look_prompt_ids` (computed Many2many → `recipe_node_id.input_ids` filtered to step_input), `quick_look_recorded_value_ids` (computed Many2many → `fp.job.step.move.input.value` rows whose `move_id.from_step_id == self.id`) |
|
||||||
|
| **One new form view** `view_fp_job_step_quick_look_form` | Read-only single-sheet form. Header shows status + tank/bath/rack. Three panels: Instructions, Prompts, Recorded Values |
|
||||||
|
| **One new action method** `action_open_full_form` on `fp.job.step` | Returns `ir.actions.act_window` opening the existing editable form view, replacing the modal |
|
||||||
|
| **Single context attribute** on the parent job form's `<field name="step_ids">` | `context="{'form_view_ref': 'fusion_plating_jobs.view_fp_job_step_quick_look_form'}"` — makes Odoo open the quick-look variant on row click |
|
||||||
|
|
||||||
|
## Form Layout
|
||||||
|
|
||||||
|
Single sheet, no notebook. From top to bottom:
|
||||||
|
|
||||||
|
1. **Title row** — `name` as h1 + `state` widget="badge"
|
||||||
|
2. **Sub-header** — sequence · kind · tank · bath · rack (one line, separated by ·)
|
||||||
|
3. **Instructions panel** — Html field, `max-height: 40vh; overflow: auto`. Empty-state placeholder when blank: *"No instructions authored for this step."*
|
||||||
|
4. **Prompts panel** — read-only embedded list with columns: collect ✓/✗ (boolean_toggle, readonly), prompt name, type, target range (computed concat of `target_min–target_max`), unit, required ★
|
||||||
|
5. **Values Recorded panel** — read-only embedded list with columns: prompt name (from `node_input_id.name`), value (computed display of typed value), recorded by, when. Sorted `create_date desc`. Empty-state: *"No values recorded yet."*
|
||||||
|
6. **Footer buttons** — `[Open Full Form]` (primary, calls action_open_full_form) · `[Close]` (special="cancel")
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
- **No `recipe_node_id`** — instructions panel shows empty-state; prompts list empty; recorded values still resolve via move search.
|
||||||
|
- **`collect_measurements=False`** — banner: *"Master switch off — no values will be collected at runtime."* Prompts list still rendered for reference.
|
||||||
|
- **Multiple moves on same step (re-records)** — Values Recorded list shows all, newest first.
|
||||||
|
- **Long instructions** — instruction panel scrolls so prompts/values stay visible.
|
||||||
|
|
||||||
|
## Files Touched
|
||||||
|
|
||||||
|
| File | Status | Change |
|
||||||
|
|---|---|---|
|
||||||
|
| `fusion_plating_jobs/models/fp_job_step.py` | MOD | Add 3 fields + `action_open_full_form` method |
|
||||||
|
| `fusion_plating_jobs/views/fp_job_step_quick_look_views.xml` | NEW | Read-only form view |
|
||||||
|
| `fusion_plating_jobs/views/fp_job_views.xml` (or wherever step_ids is rendered) | MOD | Add `context` attr to `<field name="step_ids">` |
|
||||||
|
| `fusion_plating_jobs/__manifest__.py` | MOD | Register new view file, bump version |
|
||||||
|
|
||||||
|
## Effort
|
||||||
|
|
||||||
|
~150 lines, ~1 hour build + verify on entech.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Native Jobs',
|
'name': 'Fusion Plating — Native Jobs',
|
||||||
'version': '19.0.8.13.0',
|
'version': '19.0.8.14.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||||
'author': 'Nexa Systems Inc.',
|
'author': 'Nexa Systems Inc.',
|
||||||
@@ -54,6 +54,7 @@ full design rationale and §6.2 of the implementation plan for task list.
|
|||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/fp_cron_data.xml',
|
'data/fp_cron_data.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/fp_job_step_quick_look_views.xml',
|
||||||
'views/fp_job_form_inherit.xml',
|
'views/fp_job_form_inherit.xml',
|
||||||
'views/fp_job_quality_buttons.xml',
|
'views/fp_job_quality_buttons.xml',
|
||||||
'views/sale_order_views.xml',
|
'views/sale_order_views.xml',
|
||||||
|
|||||||
@@ -872,3 +872,64 @@ class FpJobStep(models.Model):
|
|||||||
step.duration_running_minutes = closed + running
|
step.duration_running_minutes = closed + running
|
||||||
else:
|
else:
|
||||||
step.duration_running_minutes = step.duration_actual or 0.0
|
step.duration_running_minutes = step.duration_actual or 0.0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Sub 12d — Step Details Quick-Look modal
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Three computed/related fields that power the read-only manager
|
||||||
|
# quick-look modal. The modal is bound via context= on the parent
|
||||||
|
# job form's <field name="step_ids"/> — no TransientModel needed.
|
||||||
|
|
||||||
|
quick_look_instructions = fields.Html(
|
||||||
|
string='Operator Instructions',
|
||||||
|
related='recipe_node_id.description',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
quick_look_collect_master = fields.Boolean(
|
||||||
|
string='Collect Measurements',
|
||||||
|
related='recipe_node_id.collect_measurements',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
quick_look_prompt_ids = fields.Many2many(
|
||||||
|
'fusion.plating.process.node.input',
|
||||||
|
string='Prompts',
|
||||||
|
compute='_compute_quick_look_prompt_ids',
|
||||||
|
)
|
||||||
|
quick_look_recorded_value_ids = fields.Many2many(
|
||||||
|
'fp.job.step.move.input.value',
|
||||||
|
string='Recorded Values',
|
||||||
|
compute='_compute_quick_look_recorded_value_ids',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('recipe_node_id', 'recipe_node_id.input_ids')
|
||||||
|
def _compute_quick_look_prompt_ids(self):
|
||||||
|
for step in self:
|
||||||
|
node = step.recipe_node_id
|
||||||
|
if node:
|
||||||
|
step.quick_look_prompt_ids = node.input_ids.filtered(
|
||||||
|
lambda i: (i.kind or 'step_input') == 'step_input'
|
||||||
|
).sorted('sequence')
|
||||||
|
else:
|
||||||
|
step.quick_look_prompt_ids = False
|
||||||
|
|
||||||
|
def _compute_quick_look_recorded_value_ids(self):
|
||||||
|
Value = self.env['fp.job.step.move.input.value']
|
||||||
|
for step in self:
|
||||||
|
if not step.id:
|
||||||
|
step.quick_look_recorded_value_ids = False
|
||||||
|
continue
|
||||||
|
step.quick_look_recorded_value_ids = Value.search([
|
||||||
|
('move_id.from_step_id', '=', step.id),
|
||||||
|
], order='create_date desc')
|
||||||
|
|
||||||
|
def action_open_full_form(self):
|
||||||
|
"""From the quick-look modal, escape to the full editable form."""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'fp.job.step',
|
||||||
|
'res_id': self.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
'name': self.name,
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@
|
|||||||
sees on the tablet; Running Min ticks on every refresh
|
sees on the tablet; Running Min ticks on every refresh
|
||||||
for the active step. -->
|
for the active step. -->
|
||||||
<xpath expr="//page[@name='steps']/field[@name='step_ids']" position="replace">
|
<xpath expr="//page[@name='steps']/field[@name='step_ids']" position="replace">
|
||||||
<field name="step_ids" mode="list">
|
<field name="step_ids" mode="list"
|
||||||
|
context="{'form_view_ref': 'fusion_plating_jobs.view_fp_job_step_quick_look_form'}">
|
||||||
<list editable="bottom"
|
<list editable="bottom"
|
||||||
decoration-info="state in ('ready', 'in_progress')"
|
decoration-info="state in ('ready', 'in_progress')"
|
||||||
decoration-success="state == 'done'"
|
decoration-success="state == 'done'"
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
Read-only "Step Details" quick-look modal. Triggered by clicking a
|
||||||
|
step's name in the embedded step list inside fp.job's form. Bound
|
||||||
|
via context="{'form_view_ref': '...'}" on the parent — see
|
||||||
|
fp_job_form_inherit.xml. The standalone editable form view stays
|
||||||
|
registered for the Job Steps menu / direct navigation.
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_fp_job_step_quick_look_form" model="ir.ui.view">
|
||||||
|
<field name="name">fp.job.step.quick.look.form</field>
|
||||||
|
<field name="model">fp.job.step</field>
|
||||||
|
<field name="priority">50</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Step Details" edit="false" create="false" delete="false">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" readonly="1"/>
|
||||||
|
</h1>
|
||||||
|
<div class="text-muted">
|
||||||
|
<field name="sequence" readonly="1"/> ·
|
||||||
|
<field name="kind" readonly="1"/> ·
|
||||||
|
<field name="state" widget="badge"
|
||||||
|
decoration-info="state == 'in_progress'"
|
||||||
|
decoration-success="state == 'done'"
|
||||||
|
decoration-warning="state == 'paused'"
|
||||||
|
decoration-muted="state in ('skipped','cancelled')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Equipment">
|
||||||
|
<field name="work_centre_id" readonly="1"/>
|
||||||
|
<field name="tank_id" readonly="1"/>
|
||||||
|
<field name="bath_id" readonly="1"/>
|
||||||
|
<field name="rack_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Schedule">
|
||||||
|
<field name="duration_expected" readonly="1"/>
|
||||||
|
<field name="duration_actual" readonly="1"/>
|
||||||
|
<field name="assigned_user_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Master switch banner -->
|
||||||
|
<field name="quick_look_collect_master" invisible="1"/>
|
||||||
|
<div class="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
invisible="quick_look_collect_master or not recipe_node_id">
|
||||||
|
<i class="fa fa-exclamation-triangle"/>
|
||||||
|
<strong> Master switch off</strong> — no values will be collected at runtime for this step.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<separator string="Operator Instructions"/>
|
||||||
|
<div style="max-height: 40vh; overflow: auto; padding: 8px; background: #f8f9fa; border: 1px solid #d8dadd; border-radius: 4px;">
|
||||||
|
<field name="quick_look_instructions" nolabel="1" readonly="1"/>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small"
|
||||||
|
invisible="quick_look_instructions">
|
||||||
|
No instructions authored for this step.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<separator string="Measurement Prompts"/>
|
||||||
|
<field name="quick_look_prompt_ids" nolabel="1" readonly="1">
|
||||||
|
<list create="false" delete="false" edit="false">
|
||||||
|
<field name="collect" widget="boolean_toggle" readonly="1" string="Collect"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="input_type"/>
|
||||||
|
<field name="target_min" optional="show"/>
|
||||||
|
<field name="target_max" optional="show"/>
|
||||||
|
<field name="target_unit" optional="show"/>
|
||||||
|
<field name="required" widget="boolean_toggle" readonly="1"/>
|
||||||
|
<field name="hint" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<p class="text-muted small"
|
||||||
|
invisible="quick_look_prompt_ids">
|
||||||
|
No measurement prompts authored for this step.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<separator string="Values Recorded So Far"/>
|
||||||
|
<field name="quick_look_recorded_value_ids" nolabel="1" readonly="1">
|
||||||
|
<list create="false" delete="false" edit="false" default_order="create_date desc">
|
||||||
|
<field name="node_input_id" string="Prompt"/>
|
||||||
|
<field name="value_text" optional="show"/>
|
||||||
|
<field name="value_number" optional="show"/>
|
||||||
|
<field name="value_boolean" optional="hide"/>
|
||||||
|
<field name="value_date" optional="hide"/>
|
||||||
|
<field name="value_attachment_id" optional="hide"/>
|
||||||
|
<field name="create_uid" string="Recorded By"/>
|
||||||
|
<field name="create_date" string="When"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<p class="text-muted small"
|
||||||
|
invisible="quick_look_recorded_value_ids">
|
||||||
|
No values recorded yet.
|
||||||
|
</p>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button name="action_open_full_form" type="object"
|
||||||
|
string="Open Full Form" class="btn-primary"/>
|
||||||
|
<button string="Close" class="btn-secondary"
|
||||||
|
special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user