[IMP] point_of_sale, pos_self_order: save custom product attribute
Previously, the custom value of an attribute was not saved in the database when selected via the point of sale or Self Order. The only way to see this value was via the product name. Now, this custom value is saved via the `product.attribute.custom.value` template, in order to follow the behavior of the Sale module. Others fix in this PR: - Prevent to pass to payment page when we are in mobile mode. Only kiosk mode is allowed to do so. - Hide "Modify" button in the cart page when the line is already sent to the server. - Make the language popup behaviour available in mobile mode too. closes odoo/odoo#136612 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
@@ -719,8 +719,11 @@ class PosConfig(models.Model):
|
||||
"""
|
||||
self.env.cr.execute(query, params + [20000])
|
||||
product_ids = self.env.cr.fetchall()
|
||||
products = self.env['product.product'].search_read([('id', 'in', product_ids)], fields=fields)
|
||||
return products
|
||||
products = self.env['product.product'].search([('id', 'in', product_ids)])
|
||||
product_combo = products.filtered(lambda p: p['detailed_type'] == 'combo')
|
||||
product_in_combo = product_combo.combo_ids.combo_line_ids.product_id
|
||||
products_available = products | product_in_combo
|
||||
return products_available.read(fields)
|
||||
|
||||
def get_limited_partners_loading(self):
|
||||
self.env.cr.execute("""
|
||||
|
||||
@@ -1179,6 +1179,10 @@ class PosOrderLine(models.Model):
|
||||
notice = fields.Char(string='Discount Notice')
|
||||
product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], required=True, change_default=True)
|
||||
attribute_value_ids = fields.Many2many('product.template.attribute.value', string="Selected Attributes")
|
||||
custom_attribute_value_ids = fields.One2many(
|
||||
comodel_name='product.attribute.custom.value', inverse_name='pos_order_line_id',
|
||||
string="Custom Values",
|
||||
store=True, readonly=False)
|
||||
price_unit = fields.Float(string='Unit Price', digits=0)
|
||||
qty = fields.Float('Quantity', digits='Product Unit of Measure', default=1)
|
||||
price_subtotal = fields.Float(string='Subtotal w/o Tax', digits=0,
|
||||
@@ -1313,6 +1317,8 @@ class PosOrderLine(models.Model):
|
||||
return {
|
||||
'id': orderline.id,
|
||||
'qty': orderline.qty,
|
||||
'attribute_value_ids': orderline.attribute_value_ids.ids,
|
||||
'custom_attribute_value_ids': orderline.custom_attribute_value_ids.read(['id', 'name', 'custom_product_template_attribute_value_id', 'custom_value'], load=False),
|
||||
'price_unit': orderline.price_unit,
|
||||
'skip_change': orderline.skip_change,
|
||||
'uuid': orderline.uuid,
|
||||
|
||||
@@ -117,6 +117,10 @@ class ProductProduct(models.Model):
|
||||
'variants': variant_list
|
||||
}
|
||||
|
||||
class ProductAttributeCustomValue(models.Model):
|
||||
_inherit = "product.attribute.custom.value"
|
||||
|
||||
pos_order_line_id = fields.Many2one('pos.order.line', string="PoS Order Line", ondelete='cascade')
|
||||
|
||||
class UomCateg(models.Model):
|
||||
_inherit = 'uom.category'
|
||||
|
||||
@@ -98,7 +98,6 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
|
||||
this.numberBuffer.sendKey(buttonValue);
|
||||
}
|
||||
|
||||
|
||||
selectLine(orderline) {
|
||||
this.numberBuffer.reset();
|
||||
this.currentOrder.select_orderline(orderline);
|
||||
@@ -211,7 +210,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
|
||||
const { product_id = [], packaging = [] } = await this.orm.silent.call(
|
||||
"pos.session",
|
||||
"find_product_by_barcode",
|
||||
[odoo.pos_session_id, code.base_code],
|
||||
[odoo.pos_session_id, code.base_code]
|
||||
);
|
||||
if (product_id.length) {
|
||||
await this.pos._addProducts(product_id, false);
|
||||
|
||||
@@ -35,6 +35,9 @@ export class PosDB {
|
||||
this.product_by_category_id = {};
|
||||
this.product_packaging_by_barcode = {};
|
||||
|
||||
this.attribute_by_id = {};
|
||||
this.attribute_value_by_id = {};
|
||||
|
||||
this.partner_sorted = [];
|
||||
this.partner_by_id = {};
|
||||
this.partner_by_barcode = {};
|
||||
@@ -52,6 +55,15 @@ export class PosDB {
|
||||
this.combo_line_by_id = {};
|
||||
}
|
||||
|
||||
add_attributes(attribute) {
|
||||
for (const attr of Object.values(attribute)) {
|
||||
this.attribute_by_id[attr.id] = attr;
|
||||
for (const val of attr.values) {
|
||||
val.attribute_id = attr.id;
|
||||
this.attribute_value_by_id[val.id] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* sets an uuid to prevent conflict in locally stored data between multiple PoS Configs. By
|
||||
* using the uuid of the config the local storage from other configs will not get effected nor
|
||||
@@ -340,7 +352,7 @@ export class PosDB {
|
||||
}
|
||||
}
|
||||
add_packagings(productPackagings) {
|
||||
productPackagings?.forEach(productPackaging => {
|
||||
productPackagings?.forEach((productPackaging) => {
|
||||
if (productPackaging.product_id[0] in this.product_by_id) {
|
||||
this.product_packaging_by_barcode[productPackaging.barcode] = productPackaging;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ComboConfiguratorPopup } from "./combo_configurator_popup/combo_configu
|
||||
import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { renderToElement } from "@web/core/utils/render";
|
||||
import { ProductCustomAttribute } from "./models/product_custom_attribute";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
@@ -143,9 +144,10 @@ export class Product extends PosModel {
|
||||
}
|
||||
async getAddProductOptions(code) {
|
||||
let price_extra = 0.0;
|
||||
let draftPackLotLines, description, packLotLinesToEdit, attribute_value_ids;
|
||||
let draftPackLotLines, packLotLinesToEdit, attribute_value_ids;
|
||||
let quantity = 1;
|
||||
let comboLines = [];
|
||||
let attribute_custom_values = {};
|
||||
|
||||
if (code && this.pos.db.product_packaging_by_barcode[code.code]) {
|
||||
quantity = this.pos.db.product_packaging_by_barcode[code.code].qty;
|
||||
@@ -165,8 +167,8 @@ export class Product extends PosModel {
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
description = payload.selected_attributes.join(", ");
|
||||
attribute_value_ids = payload.attribute_value_ids;
|
||||
attribute_custom_values = payload.attribute_custom_values;
|
||||
price_extra += payload.price_extra;
|
||||
quantity = payload.quantity;
|
||||
} else {
|
||||
@@ -238,7 +240,7 @@ export class Product extends PosModel {
|
||||
return {
|
||||
draftPackLotLines,
|
||||
quantity,
|
||||
description,
|
||||
attribute_custom_values,
|
||||
price_extra,
|
||||
comboLines,
|
||||
attribute_value_ids,
|
||||
@@ -380,13 +382,13 @@ export class Orderline extends PosModel {
|
||||
options.quantity ? this.set_quantity(options.quantity) : this.set_quantity(1);
|
||||
this.discount = 0;
|
||||
this.note = "";
|
||||
this.custom_attribute_value_ids = [];
|
||||
this.hasChange = false;
|
||||
this.skipChange = false;
|
||||
this.discountStr = "0";
|
||||
this.selected = false;
|
||||
this.description = "";
|
||||
this.price_extra = 0;
|
||||
this.full_product_name = options.description || "";
|
||||
this.full_product_name = "";
|
||||
this.id = orderline_id++;
|
||||
this.customerNote = this.customerNote || "";
|
||||
this.saved_quantity = 0;
|
||||
@@ -404,10 +406,15 @@ export class Orderline extends PosModel {
|
||||
this.price_type = json.price_type;
|
||||
this.set_discount(json.discount);
|
||||
this.set_quantity(json.qty, "do not recompute unit price");
|
||||
this.set_description(json.description);
|
||||
this.attribute_value_ids = json.attribute_value_ids || [];
|
||||
this.set_price_extra(json.price_extra);
|
||||
this.set_full_product_name(json.full_product_name);
|
||||
this.custom_attribute_value_ids = json.custom_attribute_value_ids.map((attr) => {
|
||||
if (attr.length > 0) {
|
||||
attr = attr[2];
|
||||
}
|
||||
return new ProductCustomAttribute(attr);
|
||||
});
|
||||
this.set_full_product_name();
|
||||
this.id = json.server_id || json.id || orderline_id++;
|
||||
orderline_id = Math.max(this.id + 1, orderline_id);
|
||||
var pack_lot_lines = json.pack_lot_ids;
|
||||
@@ -440,6 +447,7 @@ export class Orderline extends PosModel {
|
||||
}
|
||||
);
|
||||
orderline.order = null;
|
||||
orderline.custom_attribute_value_ids = this.custom_attribute_value_ids;
|
||||
orderline.quantity = this.quantity;
|
||||
orderline.quantityStr = this.quantityStr;
|
||||
orderline.discount = this.discount;
|
||||
@@ -555,14 +563,31 @@ export class Orderline extends PosModel {
|
||||
get_discount_str() {
|
||||
return this.discountStr;
|
||||
}
|
||||
set_description(description) {
|
||||
this.description = description || "";
|
||||
}
|
||||
set_price_extra(price_extra) {
|
||||
this.price_extra = parseFloat(price_extra) || 0.0;
|
||||
}
|
||||
set_full_product_name(full_product_name) {
|
||||
this.full_product_name = full_product_name || "";
|
||||
set_full_product_name() {
|
||||
let attributeString = "";
|
||||
|
||||
if (this.attribute_value_ids && this.attribute_value_ids.length > 0) {
|
||||
for (const valId of this.attribute_value_ids) {
|
||||
const value = this.pos.db.attribute_value_by_id[valId];
|
||||
if (value.is_custom) {
|
||||
const customValue = this.custom_attribute_value_ids.find(
|
||||
(cus) => cus.custom_product_template_attribute_value_id == parseInt(valId)
|
||||
);
|
||||
attributeString += customValue
|
||||
? `${value.name}: ${customValue.custom_value}, `
|
||||
: `${value.name}, `;
|
||||
} else {
|
||||
attributeString += `${value.name}, `;
|
||||
}
|
||||
}
|
||||
attributeString = attributeString.slice(0, -2);
|
||||
attributeString = `(${attributeString})`;
|
||||
}
|
||||
|
||||
this.full_product_name = `${this.product.display_name} ${attributeString}`;
|
||||
}
|
||||
get_price_extra() {
|
||||
return this.price_extra;
|
||||
@@ -696,14 +721,7 @@ export class Orderline extends PosModel {
|
||||
return this.product;
|
||||
}
|
||||
get_full_product_name() {
|
||||
if (this.full_product_name) {
|
||||
return this.full_product_name;
|
||||
}
|
||||
var full_name = this.product.display_name;
|
||||
if (this.description) {
|
||||
full_name += ` (${this.description})`;
|
||||
}
|
||||
return full_name;
|
||||
return this.full_product_name || this.product.display_name;
|
||||
}
|
||||
// selects or deselects this orderline
|
||||
set_selected(selected) {
|
||||
@@ -746,7 +764,7 @@ export class Orderline extends PosModel {
|
||||
this.product.tracking === "lot" &&
|
||||
(this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots)
|
||||
) &&
|
||||
this.description === orderline.description &&
|
||||
this.full_product_name === orderline.full_product_name &&
|
||||
orderline.get_customer_note() === this.get_customer_note() &&
|
||||
!this.refunded_orderline_id &&
|
||||
!this.isPartOfCombo() &&
|
||||
@@ -770,6 +788,7 @@ export class Orderline extends PosModel {
|
||||
return {
|
||||
uuid: this.uuid,
|
||||
skip_change: this.skipChange,
|
||||
custom_attribute_value_ids: this.custom_attribute_value_ids.map((attr) => [0, 0, attr]),
|
||||
qty: this.get_quantity(),
|
||||
price_unit: this.get_unit_price(),
|
||||
price_subtotal: this.get_price_without_tax(),
|
||||
@@ -779,8 +798,7 @@ export class Orderline extends PosModel {
|
||||
tax_ids: [[6, false, this.get_applicable_taxes().map((tax) => tax.id)]],
|
||||
id: this.id,
|
||||
pack_lot_ids: pack_lot_ids,
|
||||
description: this.description,
|
||||
attribute_value_ids: this.attribute_value_ids,
|
||||
attribute_value_ids: this.attribute_value_ids || [],
|
||||
full_product_name: this.get_full_product_name(),
|
||||
price_extra: this.get_price_extra(),
|
||||
customer_note: this.get_customer_note(),
|
||||
@@ -818,6 +836,7 @@ export class Orderline extends PosModel {
|
||||
customer_note: this.get_customer_note(),
|
||||
taxed_lst_unit_price: this.get_taxed_lst_unit_price(),
|
||||
isPartOfCombo: this.isPartOfCombo(),
|
||||
custom_attribute_value_ids: this.custom_attribute_value_ids,
|
||||
unitDisplayPriceBeforeDiscount: this.getUnitDisplayPriceBeforeDiscount(),
|
||||
};
|
||||
}
|
||||
@@ -1084,7 +1103,7 @@ export class Orderline extends PosModel {
|
||||
return !selectedLine ? false : last_id === selectedLine.cid;
|
||||
}
|
||||
set_customer_note(note) {
|
||||
this.customerNote = note;
|
||||
this.customerNote = note || "";
|
||||
}
|
||||
get_customer_note() {
|
||||
return this.customerNote;
|
||||
@@ -2106,7 +2125,7 @@ export class Order extends PosModel {
|
||||
this.fix_tax_included_price(line);
|
||||
|
||||
this.set_orderline_options(line, options);
|
||||
|
||||
line.set_full_product_name();
|
||||
var to_merge_orderline;
|
||||
for (var i = 0; i < this.orderlines.length; i++) {
|
||||
if (this.orderlines.at(i).can_be_merged_with(line) && options.merge !== false) {
|
||||
@@ -2195,9 +2214,29 @@ export class Order extends PosModel {
|
||||
orderline.set_discount(options.discount);
|
||||
}
|
||||
|
||||
if (options.description !== undefined) {
|
||||
orderline.description += options.description;
|
||||
orderline.attribute_value_ids = options.attribute_value_ids;
|
||||
if (options.attribute_value_ids) {
|
||||
orderline.attribute_value_ids = options.attribute_value_ids || [];
|
||||
}
|
||||
|
||||
if (
|
||||
options.attribute_custom_values &&
|
||||
Object.keys(options.attribute_custom_values).length > 0
|
||||
) {
|
||||
const customAttributeValues = [];
|
||||
for (const [id, value] of Object.entries(options.attribute_custom_values)) {
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
customAttributeValues.push(
|
||||
new ProductCustomAttribute({
|
||||
custom_product_template_attribute_value_id: parseInt(id),
|
||||
custom_value: value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
orderline.custom_attribute_value_ids = customAttributeValues;
|
||||
}
|
||||
|
||||
if (options.extras !== undefined) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module */
|
||||
import { Reactive } from "@web/core/utils/reactive";
|
||||
|
||||
export class ProductCustomAttribute extends Reactive {
|
||||
constructor() {
|
||||
super();
|
||||
this.setup(...arguments);
|
||||
}
|
||||
|
||||
setup({ id, name, custom_value, custom_product_template_attribute_value_id }) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.custom_value = custom_value;
|
||||
this.custom_product_template_attribute_value_id =
|
||||
custom_product_template_attribute_value_id;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.custom_value;
|
||||
}
|
||||
}
|
||||
@@ -263,6 +263,7 @@ export class PosStore extends Reactive {
|
||||
this.pricelists = loadedData["product.pricelist"];
|
||||
this.default_pricelist = loadedData["default_pricelist"];
|
||||
this.currency = loadedData["res.currency"];
|
||||
this.db.add_attributes(loadedData["attributes_by_ptal_id"]);
|
||||
this.db.add_categories(loadedData["pos.category"]);
|
||||
this.db.add_combos(loadedData["pos.combo"]);
|
||||
this.db.add_combo_lines(loadedData["pos.combo.line"]);
|
||||
|
||||
+10
-4
@@ -34,6 +34,7 @@ export class BaseProductAttribute extends Component {
|
||||
return {
|
||||
value,
|
||||
valueIds,
|
||||
custom_value: this.state.custom_value,
|
||||
extra,
|
||||
};
|
||||
}
|
||||
@@ -105,22 +106,27 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup {
|
||||
}
|
||||
|
||||
getPayload() {
|
||||
const selected_attributes = [];
|
||||
const attribute_custom_values = [];
|
||||
let attribute_value_ids = [];
|
||||
var price_extra = 0.0;
|
||||
const quantity = this.state.quantity;
|
||||
|
||||
this.env.attribute_components.forEach((attribute_component) => {
|
||||
const { value, valueIds, extra } = attribute_component.getValue();
|
||||
selected_attributes.push(value);
|
||||
const { valueIds, extra, custom_value } = attribute_component.getValue();
|
||||
attribute_value_ids.push(valueIds);
|
||||
|
||||
if (custom_value) {
|
||||
// for custom values, it will never be a multiple attribute
|
||||
attribute_custom_values[valueIds[0]] = custom_value;
|
||||
}
|
||||
|
||||
price_extra += extra;
|
||||
});
|
||||
|
||||
attribute_value_ids = attribute_value_ids.flat();
|
||||
return {
|
||||
selected_attributes,
|
||||
attribute_value_ids,
|
||||
attribute_custom_values,
|
||||
price_extra,
|
||||
quantity,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ registry.category("mock_server").add("pos.session/load_pos_data", async function
|
||||
"pos.combo.line": [...(this.models["pos.combo.line"]?.records || [])],
|
||||
"product.pricelist": [...(this.models["product.pricelist"]?.records || [])],
|
||||
"pos.payment.method": [...(this.models["pos.payment.method"]?.records || [])],
|
||||
attributes_by_ptal_id: {},
|
||||
"res.currency": {
|
||||
id: 1,
|
||||
name: "USD",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"web/static/lib/bootstrap/js/dist/carousel.js",
|
||||
'web/static/lib/bootstrap/js/dist/scrollspy.js',
|
||||
"pos_self_order/static/src/app/**/*",
|
||||
"point_of_sale/static/src/app/store/models/product_custom_attribute.js",
|
||||
],
|
||||
# Assets tests
|
||||
"pos_self_order.assets_tests": [
|
||||
|
||||
@@ -241,6 +241,7 @@ class PosSelfOrderController(http.Controller):
|
||||
'price_unit': child_price_unit,
|
||||
'price_subtotal': child_tax_results.get('total_excluded'),
|
||||
'price_subtotal_incl': child_tax_results.get('total_included'),
|
||||
'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in child.get('custom_attribute_value_ids')] if child.get('custom_attribute_value_ids') else [],
|
||||
'id': child.get('id'),
|
||||
'order_id': pos_order_id,
|
||||
'tax_ids': product.taxes_id,
|
||||
@@ -248,7 +249,7 @@ class PosSelfOrderController(http.Controller):
|
||||
'product_id': child.get('product_id'),
|
||||
'qty': child.get('qty'),
|
||||
'customer_note': child.get('customer_note'),
|
||||
'attribute_value_ids': child.get('selected_attributes') or [],
|
||||
'attribute_value_ids': child.get('attribute_value_ids') or [],
|
||||
'full_product_name': child.get('full_product_name'),
|
||||
'combo_parent_uuid': child.get('combo_parent_uuid'),
|
||||
'combo_id': child.get('combo_id'),
|
||||
@@ -274,7 +275,8 @@ class PosSelfOrderController(http.Controller):
|
||||
'product_id': line.get('product_id'),
|
||||
'qty': line.get('qty'),
|
||||
'customer_note': line.get('customer_note'),
|
||||
'attribute_value_ids': line.get('selected_attributes') or [],
|
||||
'attribute_value_ids': line.get('attribute_value_ids') or [],
|
||||
'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in line.get('custom_attribute_value_ids')] if line.get('custom_attribute_value_ids') else [],
|
||||
'full_product_name': line.get('full_product_name'),
|
||||
'combo_parent_uuid': line.get('combo_parent_uuid'),
|
||||
'combo_id': line.get('combo_id'),
|
||||
|
||||
@@ -103,7 +103,8 @@ class PosOrder(models.Model):
|
||||
"price_subtotal": line.price_subtotal,
|
||||
"price_subtotal_incl": line.price_subtotal_incl,
|
||||
"product_id": line.product_id.id,
|
||||
"selected_attributes": line.attribute_value_ids.ids,
|
||||
"attribute_value_ids": line.attribute_value_ids.ids,
|
||||
"custom_attribute_value_ids": line.custom_attribute_value_ids.read(['id', 'name', 'custom_product_template_attribute_value_id', 'custom_value'], load=False),
|
||||
"uuid": line.uuid,
|
||||
"qty": line.qty,
|
||||
"customer_note": line.customer_note,
|
||||
|
||||
+26
-6
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { ProductCustomAttribute } from "@point_of_sale/app/store/models/product_custom_attribute";
|
||||
import { useSelfOrder } from "@pos_self_order/app/self_order_service";
|
||||
import { attributeFlatter, attributeFormatter } from "@pos_self_order/app/utils";
|
||||
|
||||
@@ -18,7 +19,7 @@ export class AttributeSelection extends Component {
|
||||
editMode: false,
|
||||
showResume: false,
|
||||
showNext: false,
|
||||
showCustomInput: false, // FIXME: need to implement this correctly here and in the PoS, maybe add a field ?
|
||||
showCustomInput: false,
|
||||
});
|
||||
|
||||
this.selectedValues = useState(this.env.selectedValues);
|
||||
@@ -35,9 +36,16 @@ export class AttributeSelection extends Component {
|
||||
return this.props.product.attributes[this.state.currentAttribute];
|
||||
}
|
||||
|
||||
get availableAttributeValue() {
|
||||
return this.selfOrder.config.self_ordering_mode === "kiosk"
|
||||
? this.attribute.values.filter((a) => !a.is_custom)
|
||||
: this.attribute.values;
|
||||
}
|
||||
|
||||
get attributeSelected() {
|
||||
const flatAttribute = attributeFlatter(this.selectedValues);
|
||||
return attributeFormatter(this.selfOrder.attributeById, flatAttribute);
|
||||
const customAttribute = this.env.customValues;
|
||||
return attributeFormatter(this.selfOrder.attributeById, flatAttribute, customAttribute);
|
||||
}
|
||||
|
||||
initAttribute() {
|
||||
@@ -53,12 +61,18 @@ export class AttributeSelection extends Component {
|
||||
}
|
||||
|
||||
for (const attribute of attributeSingle) {
|
||||
if (this.selfOrder.editedLine) {
|
||||
for (const value of attribute.values) {
|
||||
if (this.selfOrder.editedLine.selected_attributes.includes(value.id)) {
|
||||
for (const value of attribute.values) {
|
||||
if (this.selfOrder.editedLine) {
|
||||
if (this.selfOrder.editedLine.attribute_value_ids.includes(value.id)) {
|
||||
this.selectedValues[attribute.id] = value.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (value.is_custom) {
|
||||
this.env.customValues[value.id] = new ProductCustomAttribute({
|
||||
custom_product_template_attribute_value_id: value.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,12 +81,18 @@ export class AttributeSelection extends Component {
|
||||
|
||||
for (const value of attrMulti.values) {
|
||||
if (this.selfOrder.editedLine) {
|
||||
if (this.selfOrder.editedLine.selected_attributes.includes(value.id)) {
|
||||
if (this.selfOrder.editedLine.attribute_value_ids.includes(value.id)) {
|
||||
this.selectedValues[attrMulti.id][value.id] = true;
|
||||
}
|
||||
} else {
|
||||
this.selectedValues[attrMulti.id][value.id] = false;
|
||||
}
|
||||
|
||||
if (value.is_custom) {
|
||||
this.env.customValues[value.id] = new ProductCustomAttribute({
|
||||
custom_product_template_attribute_value_id: value.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div t-if="!state.showResume" class="attribute-selection-content align-items-center justify-content-start">
|
||||
<div class="row row-cols-3 g-3">
|
||||
<t t-foreach="attribute.values" t-as="value" t-key="value.id">
|
||||
<t t-foreach="availableAttributeValue" t-as="value" t-key="value.id">
|
||||
<div class="col">
|
||||
<label t-attf-for="{{ attribute.id }}_{{ value.id }}" t-attf-class="{{ this.isChecked(attribute, value) ? 'border-2 border-primary' : '' }} d-flex btn btn-light flex-column align-items-center justify-content-center p-0 rounded border overflow-hidden">
|
||||
<div class="name position-relative d-flex flex-column justify-content-center align-items-center flex-grow-1 w-100 py-2 text-center">
|
||||
@@ -43,6 +43,7 @@
|
||||
t-model="this.selectedValues[attribute.id][value.id]"
|
||||
t-on-click="() => this.attributeClicked(value.id)"
|
||||
t-attf-id="{{ attribute.id }}_{{ value.id }}" />
|
||||
<input t-if="this.isChecked(attribute, value) and value.is_custom" type="text" t-model="this.env.customValues[value.id].custom_value" class="form-control" placeholder="Enter your custom value" />
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="attribute.display_type === 'multi' || this.state.showNext" class="page-buttons position-absolute bottom-0 end-0 w-100 py-3 px-2 d-flex flex-row w-100 gap-3 shadow-sm z-index-1">
|
||||
|
||||
@@ -13,7 +13,7 @@ export class LanguagePopup extends Component {
|
||||
}
|
||||
|
||||
get languages() {
|
||||
return this.selfOrder.kiosk_available_languages;
|
||||
return this.selfOrder.config.self_ordering_available_language_ids;
|
||||
}
|
||||
|
||||
get currentLanguage() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
import { Reactive } from "@web/core/utils/reactive";
|
||||
import { uuidv4 } from "@point_of_sale/utils";
|
||||
import { ProductCustomAttribute } from "@point_of_sale/app/store/models/product_custom_attribute";
|
||||
|
||||
export class Line extends Reactive {
|
||||
constructor({
|
||||
@@ -13,7 +14,8 @@ export class Line extends Reactive {
|
||||
price_unit,
|
||||
price_subtotal_incl,
|
||||
price_subtotal,
|
||||
selected_attributes,
|
||||
attribute_value_ids,
|
||||
custom_attribute_value_ids,
|
||||
combo_parent_uuid,
|
||||
combo_id,
|
||||
child_lines,
|
||||
@@ -33,10 +35,19 @@ export class Line extends Reactive {
|
||||
this.price_unit = line.price_unit || 0;
|
||||
this.price_subtotal_incl = line.price_subtotal_incl || 0;
|
||||
this.price_subtotal = line.price_subtotal || 0;
|
||||
this.selected_attributes = line.selected_attributes || [];
|
||||
this.attribute_value_ids = line.attribute_value_ids || [];
|
||||
this.custom_attribute_value_ids = line.custom_attribute_value_ids || [];
|
||||
this.combo_parent_uuid = line.combo_parent_uuid || null;
|
||||
this.combo_id = line.combo_id || null;
|
||||
this.child_lines = line.child_lines || [];
|
||||
|
||||
this.initCustomAttribute();
|
||||
}
|
||||
|
||||
initCustomAttribute() {
|
||||
this.custom_attribute_value_ids = this.custom_attribute_value_ids.map(
|
||||
(customAttribute) => new ProductCustomAttribute(customAttribute)
|
||||
);
|
||||
}
|
||||
|
||||
isChange(lastChange) {
|
||||
@@ -50,7 +61,7 @@ export class Line extends Reactive {
|
||||
}
|
||||
|
||||
get attributes() {
|
||||
return Object.entries(this.selected_attributes).map(([key, value]) => {
|
||||
return Object.entries(this.attribute_value_ids).map(([key, value]) => {
|
||||
return { name: key, value };
|
||||
});
|
||||
}
|
||||
@@ -63,7 +74,7 @@ export class Line extends Reactive {
|
||||
updateDataFromServer(data) {
|
||||
for (const key in data) {
|
||||
let updatedValue = data[key];
|
||||
if (key === "selected_attributes") {
|
||||
if (key === "attribute_value_ids") {
|
||||
updatedValue ||= {};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class Order extends Reactive {
|
||||
this.lastChangesSent = this.lines.reduce((acc, line) => {
|
||||
acc[line.uuid] = {
|
||||
qty: line.qty,
|
||||
selected_attributes: [...line.selected_attributes],
|
||||
attribute_value_ids: [...line.attribute_value_ids],
|
||||
customer_note: line.customer_note,
|
||||
};
|
||||
return acc;
|
||||
|
||||
@@ -59,7 +59,10 @@ export class CartPage extends Component {
|
||||
this.sendInProgress = true;
|
||||
await this.selfOrder.sendDraftOrderToServer();
|
||||
this.sendInProgress = false;
|
||||
return;
|
||||
|
||||
if (type !== "kiosk") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (orderingMode === "table" && !takeAway) {
|
||||
@@ -176,7 +179,9 @@ export class CartPage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedAttributes(attrs) {
|
||||
return attributeFormatter(this.selfOrder.attributeById, attrs);
|
||||
getSelectedAttributes(line) {
|
||||
const attributeValues = line.attribute_value_ids;
|
||||
const customAttr = line.custom_attribute_value_ids;
|
||||
return attributeFormatter(this.selfOrder.attributeById, attributeValues, customAttr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,19 +41,19 @@
|
||||
<div t-foreach="childLines" t-as="cline" t-key="cline.uuid" class="text-muted">
|
||||
- <span t-esc="cline.full_product_name" /> <span t-if="cline.price_subtotal_incl">(<t t-esc="selfOrder.formatMonetary(cline.price_subtotal_incl)"/>)</span>
|
||||
<div class="product-info d-flex flex-column flex-grow-1 ms-3">
|
||||
<div t-foreach="getSelectedAttributes(cline.selected_attributes)" t-as="attribute" t-key="attribute.name">
|
||||
<div t-foreach="getSelectedAttributes(cline)" t-as="attribute" t-key="attribute.name">
|
||||
- <span t-esc="attribute.name" /> : <span t-esc="attribute.value" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-foreach="getSelectedAttributes(line.selected_attributes)" t-as="attribute" t-key="attribute.name" class="text-muted">
|
||||
<div t-foreach="getSelectedAttributes(line)" t-as="attribute" t-key="attribute.name" class="text-muted">
|
||||
- <span t-esc="attribute.name" /> : <span t-esc="attribute.value" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-price d-flex flex-column align-items-end justify-content-between h-auto w-25">
|
||||
<span class="fw-bolder" t-esc="selfOrder.formatMonetary(getPrice(line))"/>
|
||||
<button t-if="this.selfOrder.productByIds[line.product_id].attributes.length > 0 || line.child_lines.length > 0"
|
||||
<button t-if="!selfOrder.currentOrder.isSavedOnServer and this.selfOrder.productByIds[line.product_id].attributes.length > 0 || line.child_lines.length > 0"
|
||||
type="button"
|
||||
t-on-click="() => this.clickOnLine(line)"
|
||||
class="btn btn-secondary rounded" >Modify</button>
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ComboPage extends Component {
|
||||
this.selfOrder = useSelfOrder();
|
||||
this.selfOrder.lastEditedProductId = this.props.product.id;
|
||||
this.router = useService("router");
|
||||
useSubEnv({ selectedValues: {}, editable: this.editableProductLine });
|
||||
useSubEnv({ selectedValues: {}, customValues: {}, editable: this.editableProductLine });
|
||||
|
||||
if (!this.props.product) {
|
||||
this.router.navigate("productList");
|
||||
@@ -55,9 +55,10 @@ export class ComboPage extends Component {
|
||||
return this.selfOrder.comboByIds[this.currentComboId];
|
||||
}
|
||||
|
||||
getAttributeSelected(values) {
|
||||
const flatAttribute = attributeFlatter(values);
|
||||
return attributeFormatter(this.selfOrder.attributeById, flatAttribute);
|
||||
getAttributeSelected(combo) {
|
||||
const flatAttribute = attributeFlatter(combo.variants);
|
||||
const customAttribute = combo.customValues;
|
||||
return attributeFormatter(this.selfOrder.attributeById, flatAttribute, customAttribute);
|
||||
}
|
||||
|
||||
resetState() {
|
||||
@@ -68,6 +69,10 @@ export class ComboPage extends Component {
|
||||
for (const key in this.env.selectedValues) {
|
||||
delete this.env.selectedValues[key];
|
||||
}
|
||||
|
||||
for (const key in this.env.customValues) {
|
||||
delete this.env.customValues[key];
|
||||
}
|
||||
}
|
||||
|
||||
next() {
|
||||
@@ -81,6 +86,7 @@ export class ComboPage extends Component {
|
||||
id: this.state.selectedProduct.id,
|
||||
name: this.state.selectedProduct.name,
|
||||
variants: { ...this.env.selectedValues },
|
||||
customValues: { ...this.env.customValues },
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -91,6 +97,7 @@ export class ComboPage extends Component {
|
||||
id: this.state.selectedProduct.id,
|
||||
name: this.state.selectedProduct.name,
|
||||
variants: { ...this.env.selectedValues },
|
||||
customValues: { ...this.env.customValues },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -132,7 +139,8 @@ export class ComboPage extends Component {
|
||||
price_unit: this.props.product.price_info.price_without_tax,
|
||||
product_id: this.props.product.id,
|
||||
full_product_name: this.props.product.name,
|
||||
selected_attributes: [],
|
||||
attribute_value_ids: [],
|
||||
custom_attribute_value_ids: [],
|
||||
combo_parent_uuid: null,
|
||||
combo_id: null,
|
||||
});
|
||||
@@ -144,7 +152,8 @@ export class ComboPage extends Component {
|
||||
qty: this.state.qty,
|
||||
product_id: combo.product.id,
|
||||
full_product_name: combo.product.name,
|
||||
selected_attributes: attributeFlatter(combo.product.variants),
|
||||
attribute_value_ids: attributeFlatter(combo.product.variants),
|
||||
custom_attribute_value_ids: Object.values(combo.product.customValues),
|
||||
combo_parent_uuid: parent_line.uuid,
|
||||
combo_id: combo.id,
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
onerror="this.remove()" />
|
||||
<div class="name d-flex flex-column justify-content-center align-items-start flex-grow-1 h-auto w-100 text-center">
|
||||
<h2 class="m-0"><t t-esc="combo.product.name"/></h2>
|
||||
<div t-foreach="getAttributeSelected(combo.product.variants)" t-as="attribute" t-key="attribute.id">
|
||||
<div t-foreach="getAttributeSelected(combo.product)" t-as="attribute" t-key="attribute.id">
|
||||
- <span t-esc="attribute.name" /> : <span t-esc="attribute.value" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ProductPage extends Component {
|
||||
setup() {
|
||||
this.selfOrder = useSelfOrder();
|
||||
this.router = useService("router");
|
||||
useSubEnv({ selectedValues: {}, editable: this.editableProductLine });
|
||||
useSubEnv({ selectedValues: {}, customValues: {}, editable: this.editableProductLine });
|
||||
|
||||
if (!this.props.product) {
|
||||
this.router.navigate("productList");
|
||||
@@ -95,7 +95,7 @@ export class ProductPage extends Component {
|
||||
|
||||
const line = this.selfOrder.currentOrder.lines.find(
|
||||
(l) =>
|
||||
JSON.stringify(l.selected_attributes.sort()) ===
|
||||
JSON.stringify(l.attribute_value_ids.sort()) ===
|
||||
JSON.stringify(attributeFlatter(this.env.selectedValues).sort()) &&
|
||||
l.customer_note === this.state.customer_note &&
|
||||
l.product_id === this.product.id
|
||||
@@ -109,7 +109,7 @@ export class ProductPage extends Component {
|
||||
const lineToMerge = this.orderlineCanBeMerged();
|
||||
|
||||
if (lineToMerge) {
|
||||
lineToMerge.selected_attributes = attributeFlatter(this.env.selectedValues);
|
||||
lineToMerge.attribute_value_ids = attributeFlatter(this.env.selectedValues);
|
||||
lineToMerge.customer_note = this.state.customer_note;
|
||||
lineToMerge.full_product_name = this.product.name;
|
||||
|
||||
@@ -126,7 +126,8 @@ export class ProductPage extends Component {
|
||||
product_id: this.product.id,
|
||||
full_product_name: this.product.name,
|
||||
customer_note: this.state.customer_note,
|
||||
selected_attributes: attributeFlatter(this.env.selectedValues),
|
||||
custom_attribute_value_ids: Object.values(this.env.customValues),
|
||||
attribute_value_ids: attributeFlatter(this.env.selectedValues),
|
||||
});
|
||||
|
||||
lines.push(mainLine);
|
||||
|
||||
@@ -57,8 +57,6 @@ export class SelfOrder extends Reactive {
|
||||
}
|
||||
}
|
||||
|
||||
// In case of self_order_mode === "meal", we keep the same order until the user pays
|
||||
// In case of self_order_mode === "each", we create a new order each time the user orders somethings
|
||||
get currentOrder() {
|
||||
if (this.editedOrder) {
|
||||
return this.editedOrder;
|
||||
@@ -81,6 +79,16 @@ export class SelfOrder extends Reactive {
|
||||
}
|
||||
|
||||
initData() {
|
||||
this.currentLanguage = this.config.self_ordering_available_language_ids.find(
|
||||
(l) => l.code === this.cookie.current.frontend_lang
|
||||
);
|
||||
|
||||
if (this.config.self_ordering_default_language_id && !this.currentLanguage) {
|
||||
this.currentLanguage = this.config.self_ordering_default_language_id;
|
||||
}
|
||||
|
||||
this.cookie.setCookie("frontend_lang", this.currentLanguage.code);
|
||||
|
||||
this.products = this.products.map((p) => {
|
||||
const product = new Product(p, this.config.iface_tax_included);
|
||||
this.productByIds[product.id] = product;
|
||||
@@ -124,15 +132,6 @@ export class SelfOrder extends Reactive {
|
||||
|
||||
initKioskData() {
|
||||
this.ordering = true;
|
||||
this.currentLanguage = this.config.self_ordering_available_language_ids.find(
|
||||
(l) => l.code === this.cookie.current.frontend_lang
|
||||
);
|
||||
|
||||
if (this.config.self_ordering_default_language_id && !this.currentLanguage) {
|
||||
this.currentLanguage = this.config.self_ordering_default_language_id;
|
||||
}
|
||||
|
||||
this.cookie.setCookie("frontend_lang", this.currentLanguage.code);
|
||||
}
|
||||
|
||||
async initMobileData() {
|
||||
@@ -179,7 +178,7 @@ export class SelfOrder extends Reactive {
|
||||
if (change) {
|
||||
line.qty = change.qty;
|
||||
line.customer_note = change.customer_note;
|
||||
line.selected_attributes = change.selected_attributes;
|
||||
line.attribute_value_ids = change.attribute_value_ids;
|
||||
keptLines.push(line);
|
||||
}
|
||||
}
|
||||
@@ -213,11 +212,13 @@ export class SelfOrder extends Reactive {
|
||||
this.updateOrdersFromServer([order], [order.access_token]);
|
||||
this.editedOrder.updateLastChanges();
|
||||
|
||||
if (this.self_order_mode === "each") {
|
||||
if (this.config.self_ordering_pay_after === "each") {
|
||||
this.editedOrder = null;
|
||||
}
|
||||
|
||||
this.notification.add(_t("Your order has been placed!"), { type: "success" });
|
||||
if (this.config.self_ordering_mode !== "kiosk") {
|
||||
this.notification.add(_t("Your order has been placed!"), { type: "success" });
|
||||
}
|
||||
|
||||
return order;
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const categorySorter = (a, b, start_categ_id) => {
|
||||
return a.id - b.id; // sort by id if sequences are the same
|
||||
};
|
||||
|
||||
export const attributeFormatter = (attrById, values) => {
|
||||
export const attributeFormatter = (attrById, values, customValues = []) => {
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
@@ -28,17 +28,25 @@ export const attributeFormatter = (attrById, values) => {
|
||||
const selectedValue = Object.values(attrVals)
|
||||
.filter((attr) => values.includes(attr.id))
|
||||
.reduce((acc, val) => {
|
||||
let description = "";
|
||||
const attribute = attrById[val.attribute_id];
|
||||
const isCustomValue = Object.values(customValues).find(
|
||||
(cus) => cus.custom_product_template_attribute_value_id === val.id
|
||||
);
|
||||
|
||||
if (isCustomValue && val.is_custom) {
|
||||
description = `: ${isCustomValue.custom_value}`;
|
||||
}
|
||||
|
||||
if (!acc[attribute.id]) {
|
||||
acc[attribute.id] = {
|
||||
id: attribute.id,
|
||||
name: attribute.name,
|
||||
value: val.name,
|
||||
value: val.name + description,
|
||||
valueIds: [val.id],
|
||||
};
|
||||
} else {
|
||||
acc[attribute.id].value += `, ${val.name}`;
|
||||
acc[attribute.id].value += `, ${val.name}${description}`;
|
||||
acc[attribute.id].valueIds.push(val.id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user