fusion_plating: add opt_in_out field + time tracking display (v19.0.2.0.4)

New opt_in_out selection field (disabled/opt-in/opt-out) matching
Steelhead's Configure OPT IN/OUT feature. Shown in both the form
view and the tree editor side panel.

Time tracking: form view now shows Created, Created By, Last Updated,
Updated By fields. Tree editor side panel shows relative timestamps
down to the second (e.g. "46w 3d 4h 17m 21s ago by Brett Kinzett").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 15:05:59 -04:00
parent 3316b5d519
commit e4b41828a3
6 changed files with 87 additions and 4 deletions

View File

@@ -85,7 +85,7 @@ class FpRecipeController(http.Controller):
'description', 'notes',
'estimated_duration',
'auto_complete', 'customer_visible', 'is_manual',
'requires_signoff', 'sequence', 'version',
'requires_signoff', 'opt_in_out', 'sequence', 'version',
}
safe_vals = {k: v for k, v in vals.items() if k in allowed}
if not safe_vals:

View File

@@ -143,6 +143,17 @@ class FpProcessNode(models.Model):
default=False,
help='Quality hold point — requires operator sign-off.',
)
opt_in_out = fields.Selection(
[
('disabled', 'Disabled'),
('opt_in', 'Opt-In'),
('opt_out', 'Opt-Out'),
],
string='Opt In/Out',
default='disabled',
help='Controls whether this step is optional for a given job.',
tracking=True,
)
# ---- Lifecycle -----------------------------------------------------------
@@ -274,7 +285,12 @@ class FpProcessNode(models.Model):
'requires_signoff': self.requires_signoff,
'version': self.version,
'child_count': len(children),
'opt_in_out': self.opt_in_out or 'disabled',
'input_count': len(self.input_ids),
'create_date': self.create_date.isoformat() if self.create_date else '',
'create_uid_name': self.create_uid.name if self.create_uid else '',
'write_date': self.write_date.isoformat() if self.write_date else '',
'write_uid_name': self.write_uid.name if self.write_uid else '',
'children': children,
}

View File

@@ -453,6 +453,29 @@ export class RecipeTreeEditor extends Component {
return ICON_OPTIONS;
}
formatTimeAgo(isoStr) {
if (!isoStr) return "";
const date = new Date(isoStr);
const now = new Date();
let diff = Math.floor((now - date) / 1000); // seconds
if (diff < 0) diff = 0;
const parts = [];
const weeks = Math.floor(diff / 604800);
diff %= 604800;
const days = Math.floor(diff / 86400);
diff %= 86400;
const hours = Math.floor(diff / 3600);
diff %= 3600;
const minutes = Math.floor(diff / 60);
const seconds = diff % 60;
if (weeks) parts.push(`${weeks}w`);
if (days) parts.push(`${days}d`);
if (hours) parts.push(`${hours}h`);
if (minutes) parts.push(`${minutes}m`);
parts.push(`${seconds}s`);
return parts.join(" ") + " ago";
}
formatDuration(minutes) {
if (!minutes) return "";
if (minutes < 60) return `${Math.round(minutes)}m`;

View File

@@ -372,6 +372,12 @@
}
}
// ---- Tracking section -------------------------------------------------------
.o_fp_recipe_tracking {
border-top: 1px solid $border-color;
}
// ---- Icon picker ------------------------------------------------------------
.o_fp_recipe_icon_picker {

View File

@@ -144,20 +144,49 @@
<label class="form-check-label" for="fp_chk_visible">Customer visible</label>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Opt In/Out</label>
<select class="form-select"
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
<option value="disabled"
t-att-selected="state.selectedNode.opt_in_out === 'disabled'">Disabled</option>
<option value="opt_in"
t-att-selected="state.selectedNode.opt_in_out === 'opt_in'">Opt-In</option>
<option value="opt_out"
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
</select>
</div>
<!-- Info -->
<div class="text-muted small mb-3" t-if="state.selectedNode.work_center">
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
<i class="fa fa-building me-1"/>
<t t-esc="state.selectedNode.work_center"/>
</div>
<div class="text-muted small mb-3" t-if="state.selectedNode.process_type">
<div class="text-muted small mb-2" t-if="state.selectedNode.process_type">
<i class="fa fa-tag me-1"/>
<t t-esc="state.selectedNode.process_type"/>
</div>
<div class="text-muted small mb-3"
<div class="text-muted small mb-2"
t-if="state.selectedNode.input_count">
<i class="fa fa-keyboard-o me-1"/>
<t t-esc="state.selectedNode.input_count"/> operator input(s)
</div>
<!-- Tracking -->
<div class="o_fp_recipe_tracking mt-3 pt-3" t-if="state.selectedNode.create_date">
<div class="text-muted small mb-1">
<i class="fa fa-calendar-plus-o me-1"/>
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
<t t-if="state.selectedNode.create_uid_name">
by <strong t-esc="state.selectedNode.create_uid_name"/>
</t>
</div>
<div class="text-muted small" t-if="state.selectedNode.write_date">
<i class="fa fa-pencil me-1"/>
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
<t t-if="state.selectedNode.write_uid_name">
by <strong t-esc="state.selectedNode.write_uid_name"/>
</t>
</div>
</div>
<!-- Actions -->
<div class="d-flex gap-2 mt-4">
<button class="btn btn-primary flex-fill"

View File

@@ -74,10 +74,19 @@
<field name="customer_visible"/>
<field name="is_manual"/>
<field name="requires_signoff"/>
<field name="opt_in_out"/>
<field name="version"/>
<field name="active" invisible="True"/>
</group>
</group>
<group>
<group string="Tracking">
<field name="create_date" string="Created"/>
<field name="create_uid" string="Created By"/>
<field name="write_date" string="Last Updated"/>
<field name="write_uid" string="Updated By"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<field name="description" widget="html"/>