[IMP] pos*: Remove info button and add self ordering availability

pos*: point_of_sale, pos_sale_product_configurator, pos_self_order

This commit removes the product info button and put a button on each
product card to open the product info popup.

The commit also adds a new field on the product template model to
track the availability of a product to the self ordering mode. This
field can be changed from the product form view or from the product
info popup directly in the POS. If the field is set to false,
the product will not be shown in the self ordering mode.

The commit also adds new filter and search fields on the product
list view in the backend.

closes odoo/odoo#136467

Task-id: 3515881
Signed-off-by: David Monnom (moda) <moda@odoo.com>
This commit is contained in:
Adrien Guilliams (adgu)
2023-10-03 19:20:08 +00:00
parent adba68440b
commit 707d76eec1
35 changed files with 291 additions and 87 deletions
@@ -10,7 +10,9 @@ export class ProductCard extends Component {
productId: Number,
price: String,
imageUrl: String,
productInfo: { Boolean, optional: true },
onClick: { type: Function, optional: true },
onProductInfoClick: { type: Function, optional: true },
};
static defaultProps = {
onClick: () => {},
@@ -24,3 +24,22 @@
box-shadow: 0 0 0 2px $primary;
}
}
.product-information-tag {
width: 0;
height: 0;
border-style: solid;
border-width: 0 30px 30px 0;
border-color: transparent #9a9ea180 transparent transparent;
position: absolute;
top: 0;
right: 0;
color: white;
text-align: center;
}
.product-information-tag-logo {
position: absolute;
left: 19px;
top: 3px;
}
@@ -8,6 +8,9 @@
t-on-click="props.onClick"
t-att-data-product-id="props.productId"
t-attf-aria-labelledby="article_product_{{props.productId}}">
<div t-if="props.productInfo" class="product-information-tag" t-on-click.stop="props.onProductInfoClick">
<i class="product-information-tag-logo fa fa-info" role="img" aria-label="Product Information" title="Product Information" />
</div>
<div t-if="props.imageUrl" class="product-img">
<img class="w-100" t-att-src="props.imageUrl" t-att-alt="props.name" />
</div>
@@ -1,31 +0,0 @@
/** @odoo-module */
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { useService } from "@web/core/utils/hooks";
import { ProductInfoPopup } from "@point_of_sale/app/screens/product_screen/product_info_popup/product_info_popup";
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/store/pos_hook";
export class ProductInfoButton extends Component {
static template = "point_of_sale.ProductInfoButton";
setup() {
this.pos = usePos();
this.popup = useService("popup");
this.pos = usePos();
}
async click() {
const orderline = this.pos.get_order().get_selected_orderline();
if (orderline) {
const product = orderline.get_product();
const quantity = orderline.get_quantity();
const info = await this.pos.getProductInfo(product, quantity);
this.popup.add(ProductInfoPopup, { info: info, product: product });
}
}
}
ProductScreen.addControlButton({
component: ProductInfoButton,
position: ["before", "SetFiscalPositionButton"],
});
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductInfoButton">
<button class="control-button btn btn-light rounded-0 fw-bolder" t-on-click="() => this.click()">
<i class="fa fa-info-circle me-1" role="img" aria-label="Info" title="Info"/>
Info
</button>
</t>
</templates>
@@ -4,9 +4,12 @@
<div class="popup product-info-popup">
<div class="modal-header">
<h4 class="modal-title">Product information</h4>
<div class="btn" t-on-click="cancel">
<i class="fa fa-times" aria-hidden="true"></i>
</div>
</div>
<main class="body modal-body overflow-auto">
<div class="section-product-info-title d-flex flex-column flex-sm-row text-start gap-3 bg-info text-info bg-opacity-25 mx-n3 mt-n3 p-3">
<div class="section-product-info-title d-flex flex-column flex-sm-row text-start gap-3 text-info bg-opacity-25 mx-n3 mt-n3 p-3 bg-info">
<div class="d-flex flex-column flex-grow-1 text-start w-100 w-sm-25">
<span t-esc="props.product.display_name" class="global-info-title fs-2 fw-bolder text-truncate"/>
<span class="fs-3"><t t-if="props.product.default_code" t-esc="props.product.default_code"/> <t t-if="props.product.default_code and props.product.barcode"> - </t> <t t-if="props.product.barcode" t-esc="props.product.barcode"/></span>
@@ -132,11 +135,6 @@
</div>
</div>
</main>
<footer class="footer modal-footer">
<div class="button cancel btn btn-lg btn-primary" t-on-click="cancel">
Close
</div>
</footer>
</div>
</t>
</templates>
@@ -10,6 +10,7 @@ import { ProductCard } from "@point_of_sale/app/generic_components/product_card/
import { Component, useState } from "@odoo/owl";
import { OfflineErrorPopup } from "@point_of_sale/app/errors/popups/offline_error_popup";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { ProductInfoPopup } from "@point_of_sale/app/screens/product_screen/product_info_popup/product_info_popup";
import { CategorySelector } from "@point_of_sale/app/generic_components/category_selector/category_selector";
import { Input } from "@point_of_sale/app/generic_components/input/input";
@@ -212,4 +213,8 @@ export class ProductsWidget extends Component {
createNewProducts() {
window.open("/web#action=point_of_sale.action_client_product_menu", "_self");
}
async onProductInfoClick(product) {
const info = await this.pos.getProductInfo(product, 1);
this.popup.add(ProductInfoPopup, { info: info, product: product });
}
}
@@ -25,6 +25,8 @@
price="product.getFormattedUnitPrice()"
imageUrl="product.getImageUrl()"
onClick="() =>this.pos.addProductToCurrentOrder(product)"
productInfo="true"
onProductInfoClick="() => this.onProductInfoClick(product)"
/>
</t>
</div>
@@ -5,6 +5,9 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
<field name="categ_id" position="after">
<field string="POS Product Category" name="pos_categ_ids" filter_domain="[('pos_categ_ids', 'child_of', raw_value)]"/>
</field>
<filter name="filter_to_sell" position="before">
<filter name="filter_to_availabe_pos" string="Available in POS" domain="[('available_in_pos', '=', True)]"/>
</filter>
@@ -173,6 +176,9 @@
<field name="categ_id" position="before">
<field name="pos_categ_ids" widget="many2many_tags" groups="point_of_sale.group_pos_user" optional="hide" string="POS Product Category"/>
</field>
<field name="pos_categ_ids" position="after">
<field name="available_in_pos" groups="point_of_sale.group_pos_user" optional="hide"/>
</field>
</field>
</record>
@@ -1,17 +0,0 @@
/** @odoo-module */
import { ProductInfoButton } from "@point_of_sale/app/screens/product_screen/control_buttons/product_info_button/product_info_button";
import { patch } from "@web/core/utils/patch";
patch(ProductInfoButton.prototype, {
hasOptionalProduct() {
const orderline = this.pos.get_order().get_selected_orderline();
if (orderline) {
const optionalProductIds = orderline.product.optional_product_ids;
return Object.values(this.pos.db.product_by_id).find((p) =>
optionalProductIds.includes(p.product_tmpl_id)
);
}
return false;
},
});
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_sale_product_configurator.ProductInfoButton" t-inherit="point_of_sale.ProductInfoButton" t-inherit-mode="extension">
<xpath expr="//button[hasclass('control-button')]" position="attributes">
<attribute name="t-att-class">{ 'highlight text-bg-primary': this.hasOptionalProduct() }</attribute>
</xpath>
</t>
</templates>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_sale_product_configurator.ProductInfoPopup" t-inherit="point_of_sale.ProductInfoPopup" t-inherit-mode="extension">
<xpath expr="//div[hasclass('extra')]" position="inside">
<div class="section-optional-product" t-if="productInfo.optional_products.length > 0">
<span class="section-title">Optional Products:</span>
<xpath expr="//div[hasclass('extra')]" position="after">
<div class="section-optional-product mt-3 mb-4 pb-4 border-bottom text-start" t-if="productInfo.optional_products.length > 0">
<h3 class="section-title">Optional Products:</h3>
<div class="section-optional-product-body" t-foreach="productInfo.optional_products" t-as="optional" t-key="optional.name">
<span>
<a class="text-primary text-decoration-underline" t-esc="optional.name" t-on-click="() => this.searchProduct(optional.name)"/>
+4
View File
@@ -18,6 +18,7 @@
"views/pos_config_view.xml",
"views/pos_session_view.xml",
"views/custom_link_views.xml",
"views/product_views.xml",
"data/init_access.xml",
"views/res_config_settings_views.xml",
"views/point_of_sale_dashboard.xml",
@@ -25,6 +26,9 @@
],
"assets": {
# Assets
'point_of_sale._assets_pos': [
'pos_self_order/static/src/overrides/**/*',
],
'web.assets_backend': [
"pos_self_order/static/src/upgrade_selection_field.js",
],
@@ -261,6 +261,7 @@ class PosConfig(models.Model):
[
("id", "not in", product_ids.ids),
("available_in_pos", "=", True),
("self_order_available", "=", True),
*(
self.limit_categories
and self.iface_available_categ_ids
@@ -40,3 +40,8 @@ class PosSession(models.Model):
})
return sessions
def _loader_params_product_product(self):
res = super()._loader_params_product_product()
res['search_params']['fields'].append('self_order_available')
return res
@@ -2,11 +2,33 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from typing import List, Dict, Optional
from odoo import models
from odoo import api, models, fields
from odoo.addons.point_of_sale.models.pos_config import PosConfig
class ProductTemplate(models.Model):
_inherit = 'product.template'
self_order_available = fields.Boolean(
string="Available in Self Order",
help="If this product is available in the Self Order screens",
default=True,
)
@api.constrains('available_in_pos')
def _check_combo_inclusions(self):
super()._check_combo_inclusions()
self.self_order_available = False
def write(self, vals_list):
res = super().write(vals_list)
if 'self_order_available' in vals_list:
for record in self:
for product in record.product_variant_ids:
product._send_availability_status()
return res
class ProductProduct(models.Model):
_inherit = "product.product"
@@ -100,6 +122,7 @@ class ProductProduct(models.Model):
"pos_combo_ids": self.combo_ids.mapped("id") or False,
"is_pos_groupable": self.uom_id.is_pos_groupable,
"write_date": self.write_date.timestamp(),
"self_order_available": self.self_order_available,
}
def _get_self_order_data(self, pos_config: PosConfig) -> List[Dict]:
@@ -107,3 +130,18 @@ class ProductProduct(models.Model):
product._get_product_for_ui(pos_config)
for product in self
]
def write(self, vals_list):
res = super().write(vals_list)
if 'self_order_available' in vals_list:
for record in self:
record._send_availability_status()
return res
def _send_availability_status(self):
config_self = self.env['pos.config'].sudo().search([('self_ordering_mode', '!=', 'nothing')])
for config in config_self:
if config.current_session_id and config.access_token:
self.env['bus.bus']._sendone(f'pos_config-{config.access_token}', 'PRODUCT_CHANGED', {
'product': self._get_product_for_ui(config)
})
@@ -16,6 +16,9 @@ export class ComboSelection extends Component {
productClicked(lineId) {
const comboLine = this.props.combo.combo_line_ids.find((line) => line.id == lineId);
const productSelected = this.selfOrder.productByIds[comboLine.product_id[0]];
if (!productSelected.self_order_available) {
return;
}
this.props.comboState.selectedProduct = productSelected;
if (productSelected.attributes.length === 0) {
this.props.next();
@@ -26,6 +26,9 @@
<span t-if="line.combo_price" class="position-absolute top-0 end-0 m-1 px-2 rounded border border-white text-bg-primary bg-opacity-75 fs-4">
+ <t t-esc="selfOrder.formatMonetary(line.combo_price)" />
</span>
<div class="text-center ms-2 fs-lighter" t-if="!this.selfOrder.productByIds[line.product_id[0]].self_order_available">
<div class="fs-lighter bg-secondary rounded">Out of stock</div>
</div>
</div>
</div>
</div>
@@ -74,12 +74,12 @@ export class ProductCard extends Component {
}
async selectProduct() {
if (!this.selfOrder.ordering) {
const product = this.props.product;
if (!this.selfOrder.ordering || !product.self_order_available) {
return;
}
const product = this.props.product;
if (product.isCombo) {
this.router.navigate("combo_selection", { id: product.id });
} else if (product.attributes.length > 0) {
@@ -12,8 +12,13 @@
loading="lazy"
onerror="this.remove()" />
<div class="product-infos d-flex flex-column justify-content-between text-start flex-grow-1 px-2 mt-3 mb-1">
<span t-esc="props.product.name" class="fs-5 fw-bolder lh-sm" />
<span t-esc="selfOrder.formatMonetary(props.product.prices)" class="py-2 text-muted flex-grow-1" />
<span t-esc="props.product.name" class="fs-3 fw-bolder flex-grow-1 lh-sm" />
<div class="d-flex justify-content-between align-items-end gap-3">
<span t-esc="selfOrder.formatMonetary(props.product.prices)" class="fs-4 text-muted flex-grow-1" />
<div class="text-center ms-2 fs-lighter">
<div t-if="!props.product.self_order_available" class="fs-lighter bg-secondary rounded">Out of stock</div>
</div>
</div>
</div>
</button>
</div>
@@ -14,6 +14,7 @@ export class Product extends Reactive {
pos_combo_ids,
is_pos_groupable,
write_date,
self_order_available,
},
showPriceTaxIncluded
) {
@@ -33,6 +34,7 @@ export class Product extends Reactive {
this.pos_combo_ids = product.pos_combo_ids;
this.is_pos_groupable = product.is_pos_groupable;
this.write_date = product.write_date;
this.self_order_available = product.self_order_available;
// data
this.showPriceTaxIncluded = showPriceTaxIncluded;
@@ -55,6 +55,10 @@ export class CartPage extends Component {
const mode = this.selfOrder.config.self_ordering_pay_after;
const order = this.selfOrder.currentOrder;
if (!this.selfOrder.verifyCart()) {
return;
}
if (mode === "meal" && !order.isSavedOnServer) {
this.sendInProgress = true;
await this.selfOrder.sendDraftOrderToServer();
@@ -53,7 +53,7 @@
</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="!selfOrder.currentOrder.isSavedOnServer and 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>
@@ -173,4 +173,8 @@ export class ComboPage extends Component {
this.state.editMode = true;
this.state.showQtyButtons = false;
}
get showQtyButtons() {
return this.state.showQtyButtons && this.props.product.self_order_available;
}
}
@@ -80,7 +80,7 @@
</div>
<div 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">
<button class="btn btn-secondary h-100 w-100 p-0 m-0 fs-2 flex-grow-1 text-uppercase" t-on-click="back">Discard</button>
<button t-if="!state.showResume and state.showQtyButtons" class="btn btn-primary h-100 w-100 p-0 m-0 fs-2 text-uppercase" t-on-click="next">Next</button>
<button t-if="!state.showResume and showQtyButtons" class="btn btn-primary h-100 w-100 p-0 m-0 fs-2 text-uppercase" t-on-click="next">Next</button>
<button t-if="state.showResume" class="btn btn-primary h-100 w-100 p-0 m-0 fs-2 text-uppercase" t-on-click="addToCart">Add to cart</button>
</div>
</t>
@@ -143,4 +143,8 @@ export class ProductPage extends Component {
this.router.back();
}
}
get showQtyButtons() {
return this.state.showQtyButtons && this.props.product.self_order_available;
}
}
@@ -20,7 +20,7 @@
</div>
</div>
<div t-attf-class="{{ state.showQtyButtons ? 'h-50' : 'h-75' }}">
<div t-attf-class="{{ showQtyButtons ? 'h-50' : 'h-75' }}">
<div class="h-100 px-4 overflow-y-auto">
<AttributeSelection
t-if="this.product.attributes.length"
@@ -28,7 +28,7 @@
product="product"/>
</div>
</div>
<div t-if="state.showQtyButtons" class="d-flex flex-column justify-content-center align-items-center w-100 h-25 p-5 bg-view border-top" style="z-index:1">
<div t-if="showQtyButtons" class="d-flex flex-column justify-content-center align-items-center w-100 h-25 p-5 bg-view border-top" style="z-index:1">
<div class="o_self_order_incr_button d-flex flex-row text-center border-light mb-1" role="group" aria-label="Quantity select">
<button type="button"
t-on-click = "() => this.changeQuantity(false)"
@@ -42,7 +42,7 @@
</div>
<div 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">
<button class="btn btn-secondary w-100 h-100 p-0 m-0 fs-2 flex-grow-1 text-uppercase" t-on-click="back">Discard</button>
<button t-if="state.showQtyButtons and !props.onValidate" class="btn btn-primary w-100 h-100 p-0 m-0 fs-2 text-uppercase" t-on-click="addToCart">Add to cart</button>
<button t-if="showQtyButtons and !props.onValidate" class="btn btn-primary w-100 h-100 p-0 m-0 fs-2 text-uppercase" t-on-click="addToCart">Add to cart</button>
</div>
</t>
</templates>
@@ -45,6 +45,8 @@ export class SelfOrderBus {
this.ws_status(message);
} else if (message.type === "PAYMENT_STATUS" && mode === "kiosk") {
this.ws_paymentStatus(message);
} else if (message.type === "PRODUCT_CHANGED") {
this.ws_productChanged(message);
}
}
@@ -80,6 +82,10 @@ export class SelfOrderBus {
this.selfOrder.isSession();
}
ws_productChanged(message) {
this.selfOrder.handleProductChanges(message.payload);
}
}
export const SelfOrderBusService = {
@@ -382,6 +382,58 @@ export class SelfOrder extends Reactive {
formatMonetary(price) {
return formatMonetary(price, { currencyId: this.currency_id });
}
handleProductChanges(payload) {
const product = new Product(payload.product, this.show_prices_with_tax_included);
this.productByIds[payload.product.id] = product;
for (const categ_name of payload.product.pos_categ_ids) {
if (!this.pos_category.map((c) => c.name).includes(categ_name)) {
continue;
}
const index = this.productsGroupedByCategory[categ_name].findIndex(
(p) => p.id === product.id
);
if (index >= 0) {
this.productsGroupedByCategory[categ_name][index] = product;
} else {
this.productsGroupedByCategory[categ_name].push(product);
}
}
}
verifyCart() {
let result = true;
for (const line of this.currentOrder.hasNotAllLinesSent()) {
if (line.combo_parent_uuid) {
continue;
}
const alreadySent = this.currentOrder.lastChangesSent
? this.currentOrder.lastChangesSent[line.uuid]
: false;
const wrongChild = line.child_lines.find(
(l) => !this.productByIds[l.product_id]?.self_order_available
);
if (wrongChild || !this.productByIds[line.product_id]?.self_order_available) {
if (alreadySent) {
line.qty = alreadySent.qty;
line.customer_note = alreadySent.customer_note;
line.selected_attributes = alreadySent.selected_attributes;
} else {
this.currentOrder.removeLine(line.uuid);
}
this.notification.add(
_t(
"%s is not available anymore, it has thus been removed from your order. Please review your order and validate it again.",
line.full_product_name
),
{ type: "danger" }
);
result = false;
}
}
return result;
}
}
export const selfOrderService = {
@@ -0,0 +1,15 @@
/** @odoo-module */
import { ProductCard } from "@point_of_sale/app/generic_components/product_card/product_card";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { patch } from "@web/core/utils/patch";
patch(ProductCard.prototype, {
setup() {
super.setup();
this.pos = usePos();
},
isSelfOrderAvailable(product_id) {
return this.pos.db.product_by_id[product_id].self_order_available;
},
});
@@ -0,0 +1,3 @@
.red-tag {
border-color: transparent #e75b5b63 transparent transparent;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_self_order.ProductCard" t-inherit="point_of_sale.ProductCard" t-inherit-mode="extension">
<xpath expr="//div[hasclass('product-information-tag')]" position="attributes">
<attribute name="t-att-class">{'red-tag' : !isSelfOrderAvailable(props.productId)}</attribute>
</xpath>
</t>
</templates>
@@ -0,0 +1,18 @@
/** @odoo-module */
import { ProductInfoPopup } from "@point_of_sale/app/screens/product_screen/product_info_popup/product_info_popup";
import { useService } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
patch(ProductInfoPopup.prototype, {
setup() {
super.setup();
this.orm = useService("orm");
},
async switchSelfAvailability() {
await this.orm.write("product.product", [this.props.product.id], {
self_order_available: !this.props.product.self_order_available,
});
this.props.product.self_order_available = !this.props.product.self_order_available;
},
});
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_self_order.ProductInfoPopup" t-inherit="point_of_sale.ProductInfoPopup" t-inherit-mode="extension">
<xpath expr="//div[hasclass('section-product-info-title')]" position="attributes">
<attribute name="class" remove="bg-info" separator=" "/>
<attribute name="t-attf-class">{{ props.product.self_order_available ? 'bg-success' : 'bg-danger' }}</attribute>
</xpath>
<xpath expr="//div[hasclass('section-product-info-title')]" position="after">
<div class="section-self-order-availability mt-3 mb-4 pb-4 border-bottom text-start">
<h3 class="section-title">Self-ordering availability:</h3>
<div class="section-self-order-availability-body">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" t-att-checked="props.product.self_order_available" t-on-click="() => this.switchSelfAvailability()" />
</div>
<span>
<t t-if="props.product.self_order_available">Available</t>
<t t-else="">Not available</t>
</span>
</div>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_search_view_pos" model="ir.ui.view">
<field name="name">product.template.search.pos.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="point_of_sale.product_template_search_view_pos"/>
<field name="arch" type="xml">
<filter name="filter_to_availabe_pos" position="after">
<filter name="filter_to_self_order" string="Available in Self" domain="[('self_order_available', '=', True)]"/>
</filter>
<filter name="filter_to_self_order" position="after">
<filter name="filter_to_not_available_pos" string="Not available in Self" domain="[('self_order_available', '=', False)]"/>
</filter>
</field>
</record>
<record id="product_template_form_view" model="ir.ui.view">
<field name="name">product.template.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="point_of_sale.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='pos']" position="inside">
<field name="self_order_available" invisible="not available_in_pos"/>
</xpath>
</field>
</record>
<record id="product_template_tree_view" model="ir.ui.view">
<field name="name">product.template.product.tree.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="point_of_sale.product_template_tree_view"/>
<field name="arch" type="xml">
<field name="available_in_pos" position="after">
<field name="self_order_available" groups="point_of_sale.group_pos_user" optional="hide"/>
</field>
</field>
</record>
</odoo>