19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -0,0 +1,52 @@
import { useService } from "@web/core/utils/hooks";
import { ProductMatrixDialog } from "./product_matrix_dialog";
export function useMatrixConfigurator() {
const dialog = useService("dialog");
const openDialog = (rootRecord, jsonInfo, productTemplateId, editedCellAttributes) => {
const infos = JSON.parse(jsonInfo);
dialog.add(ProductMatrixDialog, {
header: infos.header,
rows: infos.matrix,
editedCellAttributes: editedCellAttributes.toString(),
product_template_id: productTemplateId,
record: rootRecord,
});
};
const open = async (record, edit) => {
const rootRecord = record.model.root;
// fetch matrix information from server;
await rootRecord.update({
grid_product_tmpl_id: record.data.product_template_id,
});
const updatedLineAttributes = [];
if (edit) {
// provide attributes of edited line to automatically focus on matching cell in the matrix
for (const ptnvav of record.data.product_no_variant_attribute_value_ids.records) {
updatedLineAttributes.push(ptnvav.resId);
}
for (const ptav of record.data.product_template_attribute_value_ids.records) {
updatedLineAttributes.push(ptav.resId);
}
updatedLineAttributes.sort((a, b) => a - b);
}
openDialog(
rootRecord,
rootRecord.data.grid,
record.data.product_template_id.id,
updatedLineAttributes
);
if (!edit) {
// remove new line used to open the matrix
rootRecord.data.order_line.delete(record);
}
};
return { open };
}

View file

@ -0,0 +1,91 @@
import { Dialog } from '@web/core/dialog/dialog';
import { formatMonetary } from "@web/views/fields/formatters";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { Component, onMounted, markup, useRef } from "@odoo/owl";
export class ProductMatrixDialog extends Component {
static template = "product_matrix.dialog";
static props = {
header: { type: Object },
rows: { type: Object },
editedCellAttributes: { type: String },
product_template_id: { type: Number },
record: { type: Object },
close: { type: Function },
};
static components = { Dialog };
setup() {
this.size = 'xl';
const productMatrixRef = useRef('productMatrix');
useHotkey("enter", () => this._onConfirm(), {
/***
* By default, Hotkeys don't work in input fields. As the matrix table is composed of
* input fields, the `bypassEditableProtection` param will allow Hotkeys to work from
* the input fields.
*
* To avoid triggering the confirmation when pressing 'enter' on the close or the
* discard button, we only set the hotkey area on the matrix table.
*/
bypassEditableProtection: true,
area: () => productMatrixRef.el,
});
onMounted(() => {
if(this.props.editedCellAttributes.length) {
const inputs = document.getElementsByClassName('o_matrix_input');
const relevantInput = Array.from(inputs).filter((matrixInput) =>
matrixInput.attributes.ptav_ids.nodeValue === this.props.editedCellAttributes
)[0];
if (relevantInput) {
relevantInput.select();
} else {
// Based on the record creation, it may ignore the 'no_variant' attributes
// (e.g. from a stock.move), thus finding no match in the matrix.
inputs[0].select();
}
} else {
document.getElementsByClassName('o_matrix_input')[0].select();
}
});
}
_format({price, currency_id}) {
if (!price) { return ""; }
const sign = price < 0 ? '-' : '+';
const formatted = formatMonetary(
Math.abs(price),
{
currencyId: currency_id,
},
);
return markup(`&nbsp;${sign}&nbsp;${formatted}&nbsp;`);
}
_onConfirm() {
const inputs = document.getElementsByClassName('o_matrix_input');
let matrixChanges = [];
for (let matrixInput of inputs) {
if (matrixInput.value && matrixInput.value !== matrixInput.attributes.value.nodeValue) {
matrixChanges.push({
qty: parseFloat(matrixInput.value),
ptav_ids: matrixInput.attributes.ptav_ids.nodeValue.split(",").map(
id => parseInt(id)
),
});
}
}
if (matrixChanges.length > 0) {
// NB: server also removes current line opening the matrix
this.props.record.update({
grid: JSON.stringify({
changes: matrixChanges,
product_template_id: this.props.product_template_id
}),
grid_update: true // to say that the changes to grid have to be applied to the SO.
});
}
this.props.close();
}
}

View file

@ -1,59 +1,35 @@
.o_web_client .o_matrix_input_table {
table {
margin-bottom: 0;
table-layout: fixed;
min-width: 100%;
width: auto;
max-width: none;
.o_matrix_input_table {
.o_matrix_ps {
padding-left: $modal-inner-padding;
}
th, td {
border: 0 !important;
vertical-align: middle;
width: 5em;
.o_matrix_pe {
padding-right: $modal-inner-padding;
}
.o_matrix_title_header {
width: 10em;
//removing input field=number arrows as their size might
//change depending on browser default styling and shift input's position
.o_matrix_input {
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&[type=number] {
-moz-appearance: textfield;
}
}
.o_matrix_input_td {
border: none;
&:focus-within {
--table-accent-bg: none;
background-color: $o-view-background-color;
border-bottom: $o-black 1px solid;
}
}
// Sticky header styles
thead {
color: $o-main-text-color;
background-color: $o-brand-lightsecondary;
th {
text-align: center;
white-space: pre-line;
}
}
tbody {
background-color: $o-view-background-color;
text-align: right;
tr {
border-top: 1px solid $o-form-lightsecondary;
border-bottom: 1px solid $o-form-lightsecondary;
}
.o_matrix_input {
text-align: right;
border: none;
}
}
.o_matrix_text_muted{
color: lighten($o-main-text-color, 15%);
font-style: italic;
}
// ensure white background completely surrounds nocontent bubble
.o_matrix_nocontent_container {
overflow: auto;
.oe_view_nocontent_img_link {
padding:10px;
}
}
}
.o_product_variant_matrix {
.form-control {
&:focus {
box-shadow: none;
border: 1px solid $gray-400;
}
position: sticky;
top: 0;
z-index: 10; // Ensure it stays on top of other content
background-color: $o-view-background-color; // Background for visibility
}
}

View file

@ -1,51 +1,67 @@
<template>
<div t-name="product_matrix.matrix">
<table class="o_matrix_input_table o_product_variant_matrix table table-sm table-striped table-bordered cursor-default">
<thead>
<tr>
<t t-foreach="header" t-as="column_header">
<th t-attf-class="o_matrix_title_header {{column_header_first?'text-start':'text-center'}}">
<span t-esc="column_header.name"/>
<t t-call="product_matrix.extra_price">
<t t-set="cell" t-value="column_header"/>
</t>
<div t-name="product_matrix.matrix" t-ref="productMatrix">
<table class="o_matrix_input_table table table-sm table-striped table-hover table-bordered cursor-default mb-0 h-100">
<thead class="border-0">
<tr class="border-0 h-100">
<t t-foreach="header" t-as="column_header" t-key="column_header_index">
<th class="border-0"
t-attf-class="{{column_header_first?'o_matrix_ps':''}} {{column_header_last?'o_matrix_pe':''}}">
<div class="d-flex flex-column justify-content-start h-100"
t-attf-class="{{column_header_first?'align-items-start':'align-items-end'}}">
<span t-esc="column_header.name"/>
<t t-call="product_matrix.extra_price">
<t t-set="cell" t-value="column_header"/>
</t>
</div>
</th>
</t>
</tr>
</thead>
<tbody>
<tr t-foreach="rows" t-as="row">
<t t-foreach="row" t-as="cell">
<th t-if="cell.name" class="text-start">
<strong t-esc="cell.name"/>
<t t-call="product_matrix.extra_price"/>
</th>
<td t-else="">
<div t-if="cell.is_possible_combination" class="input-group">
<input type="number"
class="o_matrix_input"
t-att-ptav_ids="cell.ptav_ids"
t-att-value="cell.qty"/>
<tr t-foreach="rows" t-as="row" t-key="row_index" class="border-1 border-end-0 border-start-0">
<t t-foreach="row" t-as="cell" t-key="cell_index">
<th t-if="cell.name"
class="border-0"
t-attf-class="{{cell_first?'o_matrix_ps o_matrix_pe':''}} {{cell_last?'o_matrix_pe':''}}">
<div class="d-flex align-items-center justify-content-between">
<strong t-esc="cell.name" class="pe-2"/>
<t t-call="product_matrix.extra_price"/>
</div>
<span t-else="" class="o_matrix_cell o_matrix_text_muted o_matrix_nocontent_container"> Not available </span>
</th>
<td t-else=""
class="o_matrix_input_td text-end"
t-attf-class="{{cell_last?'o_matrix_pe':''}}">
<div t-if="cell.is_possible_combination" class="input-group">
<input type="number"
class="o_input o_field_number o_matrix_input border-0 text-end"
t-att="{'ptav_ids': cell.ptav_ids,'value': cell.qty}"
onClick="this.select();"/>
</div>
<span t-else=""
class="text-muted overflow-auto">
Not available
</span>
</td>
</t>
</tr>
</tbody>
</table>
</div>
<span t-name="product_matrix.extra_price" t-if="cell.price" class="badge rounded-pill text-bg-secondary">
<!--
price_extra is displayed as catalog price instead of
price after pricelist because it is impossible to
compute. Indeed, the pricelist rule might depend on the
selected variant, so the price_extra will be different
depending on the selected combination. The price of an
attribute is therefore variable and it's not very
accurate to display it.
-->
<span class="variant_price_extra" style="white-space: nowrap;">
<t t-out="format(cell)"/>
</span>
<span t-name="product_matrix.extra_price"
t-if="cell.price"
class="badge rounded-pill text-bg-secondary"
>
<!--
price_extra is displayed as catalog price instead of
price after pricelist because it is impossible to
compute. Indeed, the pricelist rule might depend on the
selected variant, so the price_extra will be different
depending on the selected combination. The price of an
attribute is therefore variable and it's not very
accurate to display it.
-->
<span class="variant_price_extra" style="white-space: nowrap;">
<t t-out="format(cell)"/>
</span>
</span>
</template>

View file

@ -0,0 +1,17 @@
<templates>
<t t-name="product_matrix.dialog">
<Dialog size="size" title.translate="Choose Product Variants" withBodyPadding="false">
<t t-call="product_matrix.matrix">
<t t-set="header" t-value="props.header"/>
<t t-set="rows" t-value="props.rows"/>
<t t-set="format" t-value="_format"/>
</t>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="_onConfirm">Confirm</button>
<button class="btn btn-secondary" t-on-click="() => this.props.close()">
Discard
</button>
</t>
</Dialog>
</t>
</templates>