fix(simple-editor): HTML in chatter + library form + expand per-step inline edit
Three fixes from user feedback:
1. Chatter posting raw HTML
_AUDIT_BODY in migration 19.0.18.8.0 was a plain str with <p>
tags. message_post escaped it for safety, so the chatter pill
rendered '<p><strong>...</strong></p>' literally to the recipe
author. Wrapped in markupsafe.Markup so Odoo recognises it as
safe HTML. Going forward: ANY message_post body containing HTML
tags MUST be wrapped in Markup() — most callers already do this,
the migration script was the outlier.
2. Library template editor showed raw <p> tags
onOpenLibraryEdit was JSON-cloning the payload directly without
running description through the existing _htmlToText helper that
the per-step editor uses. Added the conversion. Save path
(onSaveLibraryEditor + library_save) already wraps via
_textToHtml so storage stays HTML-compatible.
3. Per-step inline form was missing critical fields — user had to
delete + re-add a step to change Type/workflow trigger/parallel/signoff
onToggleEdit now also captures default_kind, triggers_workflow_state_id,
parallel_start, requires_signoff into the edit state. onSaveStep
sends them in the write vals. Added _fpResetStepEdit helper to
keep open/cancel/save reset paths in sync.
New per-step form has:
* Step Type (Default Kind) dropdown — drives workflow milestone
triggers + step-kind routing (e.g. contract_review opens QA-005)
* Triggers Workflow State dropdown (Sub 14) — per-step override
* Parallel Start checkbox (Sub 13)
* Require QA Sign-off checkbox
step_write controller endpoint also gained a field whitelist —
was previously accepting any vals dict from the client (security
hole + opaque to maintainers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.18.12.3',
|
||||
'version': '19.0.18.12.4',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -102,6 +102,15 @@ class SimpleRecipeController(http.Controller):
|
||||
'work_center_id': step.work_center_id.id if step.work_center_id else False,
|
||||
'source_template_id': step.source_template_id.id or False,
|
||||
'collect_measurements': bool(step.collect_measurements),
|
||||
# Sub 13 — per-step opt-out of the sequential gate
|
||||
'parallel_start': bool(step.parallel_start),
|
||||
# Sub 14 — workflow milestone trigger override
|
||||
'triggers_workflow_state_id': (
|
||||
step.triggers_workflow_state_id.id
|
||||
if 'triggers_workflow_state_id' in step._fields
|
||||
and step.triggers_workflow_state_id
|
||||
else False
|
||||
),
|
||||
'measurements_badge_text': badge_text,
|
||||
'measurements_badge_class': badge_class,
|
||||
'inputs': [
|
||||
@@ -437,8 +446,29 @@ class SimpleRecipeController(http.Controller):
|
||||
|
||||
@http.route('/fp/simple_recipe/step/write', type='jsonrpc', auth='user')
|
||||
def step_write(self, node_id, vals):
|
||||
node = request.env['fusion.plating.process.node'].browse(node_id)
|
||||
node.write(vals)
|
||||
"""Update fields on an existing recipe step (operation node).
|
||||
|
||||
Whitelisted to the fields the inline edit panel actually surfaces
|
||||
— never trust client-provided node_type / parent_id / etc.
|
||||
"""
|
||||
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
||||
if not node.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
node.check_access('write')
|
||||
allowed = {
|
||||
'name', 'description', 'icon',
|
||||
'default_kind',
|
||||
'requires_signoff', 'requires_predecessor_done',
|
||||
'parallel_start', # Sub 13
|
||||
'triggers_workflow_state_id', # Sub 14
|
||||
'requires_rack_assignment',
|
||||
'requires_transition_form',
|
||||
'estimated_duration',
|
||||
'collect_measurements',
|
||||
}
|
||||
clean = {k: v for k, v in (vals or {}).items() if k in allowed}
|
||||
if clean:
|
||||
node.write(clean)
|
||||
return {'ok': True}
|
||||
|
||||
@http.route('/fp/simple_recipe/step/remove', type='jsonrpc', auth='user')
|
||||
|
||||
@@ -27,10 +27,15 @@ have a paper trail.
|
||||
|
||||
import logging
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_AUDIT_BODY = (
|
||||
# Wrapped in Markup so Odoo's message_post recognises it as safe HTML
|
||||
# instead of escaping the tags. Without Markup, the chatter pill renders
|
||||
# the raw "<p><strong>...</strong></p>" string literally to the operator.
|
||||
_AUDIT_BODY = Markup(
|
||||
'<p><strong>Recipe migrated to v19.0.18.8.0 step layout.</strong></p>'
|
||||
'<p>Step nodes that were direct children of this recipe (Simple '
|
||||
'Editor authoring) have been promoted to operation nodes so they '
|
||||
|
||||
@@ -44,6 +44,12 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
editingStepId: null,
|
||||
editName: "",
|
||||
editInstructions: "",
|
||||
// Sub 14 + Sub 13 — additional per-step settings that the
|
||||
// user can change inline without delete + re-add.
|
||||
editDefaultKind: "",
|
||||
editTriggersWorkflowStateId: false,
|
||||
editParallelStart: false,
|
||||
editRequiresSignoff: false,
|
||||
// Inline library form — open when authoring or editing a
|
||||
// library template directly from the right pane. null =
|
||||
// closed; otherwise carries the template payload.
|
||||
@@ -287,6 +293,13 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
// we want to be able to mutate this.state.libraryEditor.* in
|
||||
// place without triggering library list re-renders.
|
||||
this.state.libraryEditor = JSON.parse(JSON.stringify(data.template));
|
||||
// description is fields.Html on the server → strip <p> tags
|
||||
// for clean display in the textarea (operators don't want
|
||||
// to see raw HTML markup). Saved back via _textToHtml on
|
||||
// onSaveLibraryEditor below.
|
||||
this.state.libraryEditor.description = this._htmlToText(
|
||||
this.state.libraryEditor.description || ""
|
||||
);
|
||||
} else {
|
||||
this.notification.add(
|
||||
_t("Could not load library template — it may have been deleted."),
|
||||
@@ -560,18 +573,35 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
* Save discards changes — operator-style "I clicked the wrong row"
|
||||
* shouldn't write garbage to the recipe.
|
||||
*/
|
||||
onToggleEdit(stepId) {
|
||||
async onToggleEdit(stepId) {
|
||||
if (this.state.editingStepId === stepId) {
|
||||
this.state.editingStepId = null;
|
||||
this.state.editName = "";
|
||||
this.state.editInstructions = "";
|
||||
this._fpResetStepEdit();
|
||||
return;
|
||||
}
|
||||
const step = this.state.steps.find((s) => s.id === stepId);
|
||||
if (!step) return;
|
||||
// Sub 14 — make sure the workflow-state catalog is cached so
|
||||
// the dropdown in the inline form has options to render.
|
||||
await this._fpEnsureWorkflowStatesLoaded();
|
||||
this.state.editingStepId = stepId;
|
||||
this.state.editName = step.name || "";
|
||||
this.state.editInstructions = this._htmlToText(step.description || "");
|
||||
// Settings the user can now change WITHOUT delete + re-add.
|
||||
this.state.editDefaultKind = step.default_kind || "";
|
||||
this.state.editTriggersWorkflowStateId =
|
||||
step.triggers_workflow_state_id || false;
|
||||
this.state.editParallelStart = !!step.parallel_start;
|
||||
this.state.editRequiresSignoff = !!step.requires_signoff;
|
||||
}
|
||||
|
||||
_fpResetStepEdit() {
|
||||
this.state.editingStepId = null;
|
||||
this.state.editName = "";
|
||||
this.state.editInstructions = "";
|
||||
this.state.editDefaultKind = "";
|
||||
this.state.editTriggersWorkflowStateId = false;
|
||||
this.state.editParallelStart = false;
|
||||
this.state.editRequiresSignoff = false;
|
||||
}
|
||||
|
||||
async onSaveStep() {
|
||||
@@ -580,22 +610,25 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
const vals = {
|
||||
name: this.state.editName || _t("Untitled Step"),
|
||||
description: this._textToHtml(this.state.editInstructions),
|
||||
// New per-step settings — user can flip these without
|
||||
// deleting and re-adding the step.
|
||||
default_kind: this.state.editDefaultKind || false,
|
||||
triggers_workflow_state_id:
|
||||
this.state.editTriggersWorkflowStateId || false,
|
||||
parallel_start: !!this.state.editParallelStart,
|
||||
requires_signoff: !!this.state.editRequiresSignoff,
|
||||
};
|
||||
await rpc("/fp/simple_recipe/step/write", {
|
||||
node_id: stepId,
|
||||
vals: vals,
|
||||
});
|
||||
this.state.editingStepId = null;
|
||||
this.state.editName = "";
|
||||
this.state.editInstructions = "";
|
||||
this._fpResetStepEdit();
|
||||
await this.loadAll();
|
||||
this.notification.add(_t("Step updated"), { type: "success" });
|
||||
}
|
||||
|
||||
onCancelEdit() {
|
||||
this.state.editingStepId = null;
|
||||
this.state.editName = "";
|
||||
this.state.editInstructions = "";
|
||||
this._fpResetStepEdit();
|
||||
}
|
||||
|
||||
// -------------------- Sub 12d — measurements config --------------------
|
||||
|
||||
@@ -116,6 +116,83 @@
|
||||
Shown to operators when running this step at the tank. Use line breaks for separate points.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Sub 14 + Sub 13 — settings the user can change
|
||||
without delete + re-add. Step Type drives the
|
||||
default-kind workflow trigger; the dropdown
|
||||
below it lets them override per-step. -->
|
||||
<div class="o_fp_edit_row" style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<div class="o_fp_edit_field" style="flex: 1; min-width: 240px;">
|
||||
<label>Step Type (Default Kind)</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.editDefaultKind = ev.target.value; }">
|
||||
<option value="" t-att-selected="!state.editDefaultKind">— Generic —</option>
|
||||
<option value="receiving" t-att-selected="state.editDefaultKind === 'receiving'">Receiving / Incoming Inspection</option>
|
||||
<option value="contract_review" t-att-selected="state.editDefaultKind === 'contract_review'">Contract Review (QA-005)</option>
|
||||
<option value="racking" t-att-selected="state.editDefaultKind === 'racking'">Racking</option>
|
||||
<option value="mask" t-att-selected="state.editDefaultKind === 'mask'">Masking</option>
|
||||
<option value="cleaning" t-att-selected="state.editDefaultKind === 'cleaning'">Cleaning</option>
|
||||
<option value="electroclean" t-att-selected="state.editDefaultKind === 'electroclean'">Electroclean</option>
|
||||
<option value="etch" t-att-selected="state.editDefaultKind === 'etch'">Etch / Activation</option>
|
||||
<option value="rinse" t-att-selected="state.editDefaultKind === 'rinse'">Rinse</option>
|
||||
<option value="strike" t-att-selected="state.editDefaultKind === 'strike'">Strike</option>
|
||||
<option value="plate" t-att-selected="state.editDefaultKind === 'plate'">Plating</option>
|
||||
<option value="replenishment" t-att-selected="state.editDefaultKind === 'replenishment'">Tank Replenishment</option>
|
||||
<option value="wbf_test" t-att-selected="state.editDefaultKind === 'wbf_test'">Water Break Free Test</option>
|
||||
<option value="dry" t-att-selected="state.editDefaultKind === 'dry'">Drying</option>
|
||||
<option value="bake" t-att-selected="state.editDefaultKind === 'bake'">Bake</option>
|
||||
<option value="demask" t-att-selected="state.editDefaultKind === 'demask'">De-Masking</option>
|
||||
<option value="derack" t-att-selected="state.editDefaultKind === 'derack'">De-Racking</option>
|
||||
<option value="inspect" t-att-selected="state.editDefaultKind === 'inspect'">Inspection</option>
|
||||
<option value="final_inspect" t-att-selected="state.editDefaultKind === 'final_inspect'">Final Inspection</option>
|
||||
<option value="ship" t-att-selected="state.editDefaultKind === 'ship'">Shipping</option>
|
||||
</select>
|
||||
<p class="o_fp_edit_hint">
|
||||
Drives workflow milestone triggers (e.g. <code>final_inspect</code> fires
|
||||
the Inspected status) and routing (e.g. <code>contract_review</code> opens
|
||||
QA-005 instead of the input wizard).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_edit_field"
|
||||
style="flex: 1; min-width: 240px;"
|
||||
t-if="state.workflowStates and state.workflowStates.length">
|
||||
<label>Triggers Workflow State</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.editTriggersWorkflowStateId = ev.target.value ? parseInt(ev.target.value, 10) : false; }">
|
||||
<option value="" t-att-selected="!state.editTriggersWorkflowStateId">— None (use Step Type) —</option>
|
||||
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
||||
<option t-att-value="ws.id"
|
||||
t-att-selected="state.editTriggersWorkflowStateId === ws.id"
|
||||
t-esc="ws.name"/>
|
||||
</t>
|
||||
</select>
|
||||
<p class="o_fp_edit_hint">
|
||||
Override the default-kind matching. Wins over Step Type when set.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_edit_field">
|
||||
<label class="me-3">
|
||||
<input type="checkbox"
|
||||
t-model="state.editParallelStart"/>
|
||||
<strong> Parallel Start</strong>
|
||||
<small class="text-muted ms-2">
|
||||
Lets the step start while earlier-sequence steps are still in progress
|
||||
(overrides the recipe's sequential gate for this step only).
|
||||
</small>
|
||||
</label>
|
||||
<br/>
|
||||
<label class="me-3">
|
||||
<input type="checkbox"
|
||||
t-model="state.editRequiresSignoff"/>
|
||||
<strong> Require QA Sign-off</strong>
|
||||
<small class="text-muted ms-2">
|
||||
Marks this step as needing a QA signature before it can be considered done.
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<!-- Sub 12d — Measurements config -->
|
||||
<div class="o_fp_edit_field o_fp_measurements_config">
|
||||
<label>
|
||||
|
||||
Reference in New Issue
Block a user