diff --git a/fusion_plating/fusion_plating_configurator/__manifest__.py b/fusion_plating/fusion_plating_configurator/__manifest__.py
index 502600c1..ae1be85c 100644
--- a/fusion_plating/fusion_plating_configurator/__manifest__.py
+++ b/fusion_plating/fusion_plating_configurator/__manifest__.py
@@ -79,10 +79,13 @@ Provides:
'fusion_plating_configurator/static/src/scss/_express_tokens.scss',
'fusion_plating_configurator/static/src/scss/express_order.scss',
# OWL widgets — multi-row Part cell + click-to-edit Bake pill
+ # + stacked DWG/OPEN action buttons
'fusion_plating_configurator/static/src/js/express_part_cell.js',
'fusion_plating_configurator/static/src/js/express_bake_pill.js',
+ 'fusion_plating_configurator/static/src/js/express_action_btns.js',
'fusion_plating_configurator/static/src/xml/express_part_cell.xml',
'fusion_plating_configurator/static/src/xml/express_bake_pill.xml',
+ 'fusion_plating_configurator/static/src/xml/express_action_btns.xml',
],
# Register colour-aware SCSS in both bundles so the
# `@if $o-webclient-color-scheme == dark` branch compiles for
@@ -94,8 +97,10 @@ Provides:
'fusion_plating_configurator/static/src/scss/express_order.scss',
'fusion_plating_configurator/static/src/js/express_part_cell.js',
'fusion_plating_configurator/static/src/js/express_bake_pill.js',
+ 'fusion_plating_configurator/static/src/js/express_action_btns.js',
'fusion_plating_configurator/static/src/xml/express_part_cell.xml',
'fusion_plating_configurator/static/src/xml/express_bake_pill.xml',
+ 'fusion_plating_configurator/static/src/xml/express_action_btns.xml',
],
},
'installable': True,
diff --git a/fusion_plating/fusion_plating_configurator/static/src/js/express_action_btns.js b/fusion_plating/fusion_plating_configurator/static/src/js/express_action_btns.js
new file mode 100644
index 00000000..c300fd0d
--- /dev/null
+++ b/fusion_plating/fusion_plating_configurator/static/src/js/express_action_btns.js
@@ -0,0 +1,101 @@
+/** @odoo-module **/
+
+// Express Orders — stacked DWG / OPEN action buttons widget (2026-05-26)
+//
+// Renders BOTH the upload-drawing and open-part buttons stacked
+// vertically in one list cell, saving horizontal width. The widget
+// binds to part_catalog_id (read-only — picker is owned by the
+// FpExpressPartCell widget on the same field; this widget is
+// declared on a separate dummy column).
+
+import { Component } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { standardFieldProps } from "@web/views/fields/standard_field_props";
+
+
+export class FpExpressActionBtns extends Component {
+ static template = "fusion_plating_configurator.FpExpressActionBtns";
+ static props = { ...standardFieldProps };
+
+ setup() {
+ this.action = useService("action");
+ this.orm = useService("orm");
+ this.notification = useService("notification");
+ }
+
+ get hasPart() {
+ return !!this.props.record.data.part_catalog_id;
+ }
+
+ async _ensureSaved() {
+ if (!this.props.record.resId) {
+ await this.props.record.save();
+ }
+ return !!this.props.record.resId;
+ }
+
+ async onUpload(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ if (!this.hasPart) {
+ this.notification.add("Pick a part first.", { type: "warning" });
+ return;
+ }
+ // Trigger native file picker via hidden input
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = ".pdf,.dwg,.dxf,.png,.jpg,.jpeg,application/pdf,image/*";
+ input.onchange = async () => {
+ const file = input.files && input.files[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = async () => {
+ const base64 = reader.result.split(",")[1];
+ if (!(await this._ensureSaved())) return;
+ try {
+ await this.orm.call(
+ this.props.record.resModel,
+ "action_upload_drawing",
+ [[this.props.record.resId]],
+ {
+ context: {
+ fp_drawing_file: base64,
+ fp_drawing_filename: file.name,
+ },
+ },
+ );
+ this.notification.add(`Drawing "${file.name}" uploaded.`, { type: "success" });
+ await this.props.record.load();
+ } catch (e) {
+ this.notification.add(`Upload failed: ${e.message || e}`, { type: "danger" });
+ }
+ };
+ reader.readAsDataURL(file);
+ };
+ input.click();
+ }
+
+ async onOpen(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ if (!this.hasPart) {
+ this.notification.add("Pick a part first.", { type: "warning" });
+ return;
+ }
+ if (!(await this._ensureSaved())) return;
+ const action = await this.orm.call(
+ this.props.record.resModel,
+ "action_open_part",
+ [[this.props.record.resId]],
+ );
+ if (action) await this.action.doAction(action);
+ }
+}
+
+export const fpExpressActionBtns = {
+ component: FpExpressActionBtns,
+ supportedTypes: ["many2one"],
+};
+
+registry.category("fields").add("fp_express_action_btns", fpExpressActionBtns);
diff --git a/fusion_plating/fusion_plating_configurator/static/src/scss/express_order.scss b/fusion_plating/fusion_plating_configurator/static/src/scss/express_order.scss
index 571ff76e..64394411 100644
--- a/fusion_plating/fusion_plating_configurator/static/src/scss/express_order.scss
+++ b/fusion_plating/fusion_plating_configurator/static/src/scss/express_order.scss
@@ -33,6 +33,7 @@
flex-direction: column;
gap: 2px;
min-width: 0; // allow Many2One to shrink within grid track
+ cursor: text; // hint that clicking the cell focuses the input
> label {
font-size: 11px;
@@ -41,36 +42,80 @@
letter-spacing: 0.3px;
font-weight: 500;
margin-bottom: 0;
+ cursor: pointer; // label is explicitly clickable to focus input
}
- // The actual field input — kill Odoo's default block-level chrome
+ // The actual field input — kill Odoo's default block-level chrome.
+ // Aggressive width 100% on all known wrappers so the click target
+ // matches the cell's visible width.
> .o_field_widget,
> .o_field_widget > div,
- > .o_field_widget input,
- > .o_field_widget select,
+ > .o_field_widget > .o-dropdown,
+ > .o_field_widget > .o-autocomplete,
> .o_field_many2one,
> .o_field_char,
- > .o_field_date {
+ > .o_field_date,
+ > .o_field_text,
+ > .o_field_integer,
+ > .o_field_selection {
width: 100%;
+ display: block;
+ }
+ .o-dropdown, .o-autocomplete {
+ width: 100%;
+ display: block;
}
// Field input visual — underline style like the mockup
- .o_input, .o_field_widget input {
+ .o_input, .o_field_widget input,
+ .o_field_widget select {
border: none;
border-bottom: 1px solid $xpr-border-strong;
background: transparent;
- padding: 4px 0;
+ padding: 5px 4px;
border-radius: 0;
font-size: 14px;
color: $xpr-text;
+ width: 100%;
+ min-height: 30px;
+ box-sizing: border-box;
+ cursor: text;
&:focus {
border-bottom-color: $xpr-accent;
border-bottom-width: 2px;
outline: none;
box-shadow: none;
+ background: $xpr-cell-focus;
}
}
+ .o_field_widget select { cursor: pointer; }
+
+ // Ensure Boolean/toggle aligns visually with the underline
+ .o_field_boolean,
+ .o_field_boolean_toggle {
+ padding: 5px 0;
+ min-height: 30px;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ // Inline pair (Blanket SO toggle + Block partial checkbox)
+ .o_fp_xpr_inline_pair {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+ padding: 5px 0;
+ min-height: 30px;
+
+ .o_field_widget { width: auto !important; }
+ }
+ .o_fp_xpr_inline_help {
+ font-size: 11px;
+ color: $xpr-text-muted;
+ font-style: italic;
}
.o_fp_xpr_cell.span-2 { grid-column: span 2; }
@@ -168,6 +213,50 @@
.o_fp_xpr_lines .o_list_view table {
border-collapse: collapse;
border: 1px solid $xpr-border-table;
+ table-layout: auto;
+ }
+
+ // Column widths — give Part Number + Specification room,
+ // tighten Qty/Price/Subtotal/Mask
+ .o_fp_xpr_lines .o_list_view {
+ th[data-name="part_catalog_id"],
+ td[name="part_catalog_id"] {
+ min-width: 230px;
+ width: 230px;
+ }
+ th[data-name="line_description"],
+ td[name="line_description"] {
+ min-width: 220px;
+ }
+ th[data-name="customer_line_ref"],
+ td[name="customer_line_ref"] { width: 75px; }
+ th[data-name="thickness_range"],
+ td[name="thickness_range"] { width: 110px; }
+ th[data-name="masking_enabled"],
+ td[name="masking_enabled"] {
+ width: 55px;
+ text-align: center;
+ }
+ th[data-name="bake_instructions"],
+ td[name="bake_instructions"] { width: 130px; }
+ th[data-name="internal_description"],
+ td[name="internal_description"] { min-width: 140px; }
+ th[data-name="quantity"],
+ td[name="quantity"] { width: 60px; }
+ th[data-name="unit_price"],
+ td[name="unit_price"] { width: 80px; }
+ th[data-name="line_subtotal"],
+ td[name="line_subtotal"] {
+ width: 90px;
+ font-weight: 600;
+ }
+ // Stacked DWG / OPEN action column
+ th[data-name="action_btns_anchor"],
+ td[name="action_btns_anchor"] {
+ width: 60px;
+ text-align: center;
+ padding: 2px !important;
+ }
}
.o_fp_xpr_lines .o_list_view thead th {
background: $xpr-table-head;
@@ -244,6 +333,38 @@
}
}
+ // Stacked DWG / OPEN buttons widget
+ .o_fp_xpr_action_stack {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ align-items: stretch;
+
+ .o_fp_xpr_action_stack_btn {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ padding: 2px 6px;
+ border: 1px solid $xpr-border-strong;
+ border-radius: 3px;
+ color: $xpr-text-muted;
+ background: transparent;
+ cursor: pointer;
+ line-height: 1.3;
+
+ &:hover:not(:disabled) {
+ color: $xpr-accent;
+ border-color: $xpr-accent;
+ background: $xpr-accent-bg;
+ }
+ &:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+ }
+ }
+ }
+
// ============================================================
// FOOTER — Notes/Terms left + Totals right (CSS Grid)
// ============================================================
diff --git a/fusion_plating/fusion_plating_configurator/static/src/xml/express_action_btns.xml b/fusion_plating/fusion_plating_configurator/static/src/xml/express_action_btns.xml
new file mode 100644
index 00000000..bfbd448c
--- /dev/null
+++ b/fusion_plating/fusion_plating_configurator/static/src/xml/express_action_btns.xml
@@ -0,0 +1,22 @@
+
+