[IMP] point_of_sale: multi-attribute support
Prior to this PR, `product_configurator_popup.js` did not support attribute selection of type `multi`. In addition, the `pos_preparation_display` did not display any attributes. Now it's possible to select attributes of type `multi` in the Point of Sale, and the preparation display shows all available attributes. closes odoo/odoo#136441 Taskid: 3497560 Related: odoo/enterprise#47834 Signed-off-by: Adrien Guilliams (adgu) <adgu@odoo.com>
This commit is contained in:
@@ -1712,6 +1712,7 @@ export class Order extends PosModel {
|
||||
this.lastOrderPrepaChange[lineKey]["quantity"] = line.get_quantity();
|
||||
} else {
|
||||
this.lastOrderPrepaChange[lineKey] = {
|
||||
attribute_value_ids: line.attribute_value_ids,
|
||||
line_uuid: line.uuid,
|
||||
product_id: line.get_product().id,
|
||||
name: line.get_full_product_name(),
|
||||
@@ -1765,6 +1766,7 @@ export class Order extends PosModel {
|
||||
changes[productKey] = {
|
||||
name: orderline.get_full_product_name(),
|
||||
product_id: product.id,
|
||||
attribute_value_ids: orderline.attribute_value_ids,
|
||||
quantity: quantityDiff,
|
||||
note: note,
|
||||
};
|
||||
@@ -1790,6 +1792,7 @@ export class Order extends PosModel {
|
||||
product_id: lineResume["product_id"],
|
||||
name: lineResume["name"],
|
||||
note: lineResume["note"],
|
||||
attribute_value_ids: lineResume["attribute_value_ids"],
|
||||
quantity: -lineResume["quantity"],
|
||||
};
|
||||
} else {
|
||||
|
||||
+46
-14
@@ -9,24 +9,32 @@ export class BaseProductAttribute extends Component {
|
||||
this.attribute = this.props.attribute;
|
||||
this.values = this.attribute.values;
|
||||
this.state = useState({
|
||||
selected_value: parseFloat(this.values[0].id),
|
||||
attribute_value_ids: parseFloat(this.values[0].id),
|
||||
custom_value: "",
|
||||
});
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const selected_value = this.values.find(
|
||||
(val) => val.id === parseFloat(this.state.selected_value)
|
||||
);
|
||||
let value = selected_value.name;
|
||||
if (selected_value.is_custom && this.state.custom_value) {
|
||||
value += `: ${this.state.custom_value}`;
|
||||
}
|
||||
const attribute_value_ids =
|
||||
this.attribute.display_type === "multi"
|
||||
? this.values.filter((val) => this.state.attribute_value_ids[val.id])
|
||||
: [this.values.find((val) => val.id === parseInt(this.state.attribute_value_ids))];
|
||||
|
||||
const extra = attribute_value_ids.reduce((acc, val) => acc + val.price_extra, 0);
|
||||
const valueIds = attribute_value_ids.map((val) => val.id);
|
||||
const value = attribute_value_ids
|
||||
.map((val) => {
|
||||
if (val.is_custom && this.state.custom_value) {
|
||||
return `${val.name}: ${this.state.custom_value}`;
|
||||
}
|
||||
return val.name;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return {
|
||||
value,
|
||||
valueId: selected_value.id,
|
||||
extra: selected_value.price_extra,
|
||||
valueIds,
|
||||
extra,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -56,12 +64,35 @@ export class ColorProductAttribute extends BaseProductAttribute {
|
||||
static template = "point_of_sale.ColorProductAttribute";
|
||||
}
|
||||
|
||||
export class MultiProductAttribute extends BaseProductAttribute {
|
||||
static template = "point_of_sale.MultiProductAttribute";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
attribute_value_ids: [],
|
||||
custom_value: "",
|
||||
});
|
||||
|
||||
this.initAttribute();
|
||||
}
|
||||
|
||||
initAttribute() {
|
||||
const attribute = this.props.attribute;
|
||||
|
||||
for (const value of attribute.values) {
|
||||
this.state.attribute_value_ids[value.id] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
|
||||
static template = "point_of_sale.ProductConfiguratorPopup";
|
||||
static components = {
|
||||
RadioProductAttribute,
|
||||
SelectProductAttribute,
|
||||
ColorProductAttribute,
|
||||
MultiProductAttribute,
|
||||
};
|
||||
|
||||
setup() {
|
||||
@@ -74,18 +105,19 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
|
||||
}
|
||||
|
||||
getPayload() {
|
||||
var selected_attributes = [];
|
||||
const attribute_value_ids = [];
|
||||
const selected_attributes = [];
|
||||
let attribute_value_ids = [];
|
||||
var price_extra = 0.0;
|
||||
const quantity = this.state.quantity;
|
||||
|
||||
this.env.attribute_components.forEach((attribute_component) => {
|
||||
const { value, valueId, extra } = attribute_component.getValue();
|
||||
const { value, valueIds, extra } = attribute_component.getValue();
|
||||
selected_attributes.push(value);
|
||||
attribute_value_ids.push(valueId);
|
||||
attribute_value_ids.push(valueIds);
|
||||
price_extra += extra;
|
||||
});
|
||||
|
||||
attribute_value_ids = attribute_value_ids.flat();
|
||||
return {
|
||||
selected_attributes,
|
||||
attribute_value_ids,
|
||||
|
||||
+25
-10
@@ -6,7 +6,7 @@
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<t t-foreach="values" t-as="value" t-key="value.id">
|
||||
<div class="attribute-name-cell form-check">
|
||||
<input class="form-check-input" type="radio" t-model="state.selected_value" t-att-name="attribute.id"
|
||||
<input class="form-check-input" type="radio" t-model="state.attribute_value_ids" t-att-name="attribute.id"
|
||||
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
|
||||
<label class="form-check-label" t-att-name="value.name" t-attf-for="{{ attribute.id }}_{{ value.id }}">
|
||||
<span t-esc="value.name"/>
|
||||
@@ -17,7 +17,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="value.id == state.selected_value && value.is_custom" class="custom-value-cell">
|
||||
<div t-if="value.id == state.attribute_value_ids && value.is_custom" class="custom-value-cell">
|
||||
<input class="custom_value form-control form-control-lg mt-2" type="text" t-model="state.custom_value"/>
|
||||
</div>
|
||||
</t>
|
||||
@@ -29,9 +29,9 @@
|
||||
<div>
|
||||
<t t-set="is_custom" t-value="false"/>
|
||||
|
||||
<select class="configurator_select form-select form-select-lg" t-model="state.selected_value">
|
||||
<select class="configurator_select form-select form-select-lg" t-model="state.attribute_value_ids">
|
||||
<option t-foreach="values" t-as="value" t-key="value.id" t-att-value="value.id">
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.selected_value)"/>
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.attribute_value_ids)"/>
|
||||
<t t-esc="value.name"/>
|
||||
<t t-if="value.price_extra">
|
||||
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
|
||||
@@ -49,10 +49,10 @@
|
||||
|
||||
<ul class="color_attribute_list d-flex gap-3">
|
||||
<li t-foreach="values" t-as="value" t-key="value.id" class="color_attribute_list_item">
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.selected_value)"/>
|
||||
<label t-attf-class="configurator_color rounded border {{ value.id == state.selected_value ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
|
||||
<t t-set="is_custom" t-value="is_custom || (value.is_custom && value.id == state.attribute_value_ids)"/>
|
||||
<label t-attf-class="configurator_color rounded border {{ value.id == state.attribute_value_ids ? 'active border-3 border-primary' : 'border-3 border-secondary' }}"
|
||||
t-attf-style="background-color: {{ value.html_color }};" t-att-data-color="value.name">
|
||||
<input class="m-4 opacity-0" type="radio" t-model="state.selected_value" t-att-value="value.id" t-att-name="attribute.id"/>
|
||||
<input class="m-4 opacity-0" type="radio" t-model="state.attribute_value_ids" t-att-value="value.id" t-att-name="attribute.id"/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -61,22 +61,37 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="point_of_sale.MultiProductAttribute">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div t-foreach="values" t-as="value" t-key="value.id" class="form-check">
|
||||
<label class="form-check-label" t-attf-for="multi-{{value.id}}" t-esc="value.name" />
|
||||
<input class="form-check-input" type="checkbox" t-attf-name="multi-{{value.id}}" t-model="state.attribute_value_ids[value.id]" />
|
||||
<div t-if="value.price_extra" class="price-extra-cell d-inline-block ms-3">
|
||||
<span class="price_extra px-2 py-1 rounded-pill text-bg-info">
|
||||
+ <t t-esc="env.utils.formatCurrency(value.price_extra)"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="point_of_sale.ProductConfiguratorPopup">
|
||||
<t t-if="!ui.isSmall">
|
||||
<div class="popup popup-text popup-med product-configurator-popup">
|
||||
<div class="modal-header drag-handle">
|
||||
<h4 class="modal-title"><t t-esc="props.product.display_name" /></h4>
|
||||
</div>
|
||||
|
||||
|
||||
<main class="body modal-body product_configurator_attributes m-0 text-start">
|
||||
<div t-foreach="props.attributes" t-as="attribute" t-key="attribute.id" class="attribute mb-3">
|
||||
<div class="attribute_name mb-2 fw-bolder" t-esc="attribute.name"/>
|
||||
<RadioProductAttribute t-if="attribute.display_type === 'radio' or attribute.display_type === 'pills'" attribute="attribute"/>
|
||||
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
|
||||
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
|
||||
<MultiProductAttribute t-elif="attribute.display_type === 'multi'" attribute="attribute"/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<footer class="footer footer-flex modal-footer">
|
||||
<div class="button highlight confirm btn btn-lg btn-primary" t-on-click="confirm">
|
||||
Add
|
||||
@@ -104,7 +119,7 @@
|
||||
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<footer class="footer footer-flex mobile modal-footer">
|
||||
<div class="quantity-selector d-flex align-items-center justify-content-center w-100 py-2 fw-bolder">
|
||||
<button class="btn btn-lg btn-secondary" t-on-click="removeOneQuantity">
|
||||
|
||||
@@ -590,10 +590,8 @@ export class FloorScreen extends Component {
|
||||
}
|
||||
} else {
|
||||
await this.popup.add(ErrorPopup, {
|
||||
title: this.env._t("Delete Error"),
|
||||
body: this.env._t(
|
||||
"You cannot delete a table with orders still in draft for this table."
|
||||
),
|
||||
title: _t("Delete Error"),
|
||||
body: _t("You cannot delete a table with orders still in draft for this table."),
|
||||
});
|
||||
}
|
||||
// Value of an object can change inside async function call.
|
||||
|
||||
Reference in New Issue
Block a user