[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:
David Monnom (moda)
2023-09-27 14:57:15 +00:00
parent 1d70293093
commit 69e2d08d4e
4 changed files with 76 additions and 28 deletions
@@ -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 {
@@ -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,
@@ -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 &amp;&amp; value.is_custom" class="custom-value-cell">
<div t-if="value.id == state.attribute_value_ids &amp;&amp; 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 &amp;&amp; value.id == state.selected_value)"/>
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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.