feat(configurator): Express form polish — 4 fixes per user review
1. Blanket Sales Order — match legacy field shape. Renamed label from
'Blanket SO' to 'Blanket Sales Order' (matches legacy view), removed
the boolean_toggle widget (defaults to checkbox), and added the
sibling 'block_partial_shipments' field inline (only visible when
blanket is checked, with 'Block partial shipments' helper text).
2. Column widths — give roomier columns where data needs space, tighten
numeric columns. Part Number 230px, Specification min 220px,
Internal Notes min 140px, Qty 60px, Price 80px, Subtotal 90px,
Mask 55px, Bake 130px, Action stack 60px.
3. Stacked DWG / OPEN buttons — new OWL widget FpExpressActionBtns
(express_action_btns.js + .xml) renders both buttons vertically in
ONE column to save horizontal space. Widget binds to a new
action_btns_anchor field (related from part_catalog_id) on the
line. Each button shows tooltip + disabled state when no part is
picked; DWG triggers the native file picker, OPEN navigates to the
part record.
4. Field activation — clicking the cell anywhere now focuses the
input, not just clicking the label. Achieved via:
- cursor: text on .o_fp_xpr_cell
- cursor: pointer on labels
- min-height: 30px on all inputs (larger click target)
- width: 100% propagated through Many2One wrappers (.o-dropdown,
.o-autocomplete) so the input genuinely fills the cell
- box-sizing: border-box so widths are predictable
- Background tint on focus for visual feedback
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Express Orders — stacked DWG / OPEN action buttons -->
|
||||
<t t-name="fusion_plating_configurator.FpExpressActionBtns">
|
||||
<div class="o_fp_xpr_action_stack">
|
||||
<button class="o_fp_xpr_action_stack_btn"
|
||||
t-on-click="onUpload"
|
||||
t-att-disabled="!hasPart"
|
||||
title="Upload a drawing for this part (saves to part record)">
|
||||
DWG
|
||||
</button>
|
||||
<button class="o_fp_xpr_action_stack_btn"
|
||||
t-on-click="onOpen"
|
||||
t-att-disabled="!hasPart"
|
||||
title="Open the part record in a modal">
|
||||
OPEN
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -159,11 +159,16 @@
|
||||
<field name="delivery_method" nolabel="1"/>
|
||||
</div>
|
||||
|
||||
<!-- ROW 4 — Blanket + Currency + Quote Valid + Invoice -->
|
||||
<!-- ROW 4 — Blanket (with block partial) + Currency + Quote Valid + Invoice -->
|
||||
<div class="o_fp_xpr_cell">
|
||||
<label for="is_blanket_order">Blanket SO</label>
|
||||
<field name="is_blanket_order" nolabel="1"
|
||||
widget="boolean_toggle"/>
|
||||
<label for="is_blanket_order">Blanket Sales Order</label>
|
||||
<div class="o_fp_xpr_inline_pair">
|
||||
<field name="is_blanket_order" nolabel="1"/>
|
||||
<field name="block_partial_shipments" nolabel="1"
|
||||
invisible="not is_blanket_order"/>
|
||||
<span class="o_fp_xpr_inline_help"
|
||||
invisible="not is_blanket_order">Block partial shipments</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_fp_xpr_cell">
|
||||
<label for="pricelist_id">Currency / Pricelist</label>
|
||||
@@ -250,16 +255,10 @@
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"
|
||||
sum="Total"/>
|
||||
<button name="action_upload_drawing" type="object"
|
||||
string="DWG"
|
||||
class="btn-link btn-sm o_fp_xpr_inline_btn"
|
||||
title="Upload a drawing for this part"
|
||||
invisible="not part_catalog_id"/>
|
||||
<button name="action_open_part" type="object"
|
||||
string="OPEN"
|
||||
class="btn-link btn-sm o_fp_xpr_inline_btn"
|
||||
title="Open part record"
|
||||
invisible="not part_catalog_id"/>
|
||||
<!-- Stacked DWG / OPEN buttons in ONE column -->
|
||||
<field name="action_btns_anchor"
|
||||
string=" "
|
||||
widget="fp_express_action_btns"/>
|
||||
<field name="process_variant_id"
|
||||
string="Process / Recipe"
|
||||
options="{'no_quick_create': True}"
|
||||
|
||||
@@ -478,6 +478,14 @@ class FpDirectOrderLine(models.Model):
|
||||
string='Part Name (display)',
|
||||
readonly=True,
|
||||
)
|
||||
# Anchor field for the FpExpressActionBtns widget — renders the
|
||||
# stacked DWG / OPEN buttons in one list column. The widget reads
|
||||
# part_catalog_id from the line; this field's value is unused.
|
||||
action_btns_anchor = fields.Many2one(
|
||||
related='part_catalog_id',
|
||||
string='Actions',
|
||||
readonly=True,
|
||||
)
|
||||
customer_line_ref = fields.Char(
|
||||
string='Customer Line Job #',
|
||||
help='Per-line customer sub-reference (e.g. ABC, DEF). Distinct from '
|
||||
|
||||
Reference in New Issue
Block a user