[IMP] pos_restaurant, *: default settings and data

*: point_of_sale, pos_self_order

Changes:
 - Adds a default restaurant with some default usual settings (with tips after payment if the company is in the United States) but no floor in the Point of Sale app. Makes pos_restaurant module auto installed with point_of_sale.
 - Makes some restaurant settings enabled by default (only for restaurants).
 - Adds loadable default demonstration data for onboarding (only when no real product is available in POS), specific to a restaurant. The default demonstration data for onboarding of point_of_sale and pos_restaurant are loaded together (if pos_restaurant is installed) to make it so an user that clicks to add demo data will have demo data for the default point of sale and also for the default restaurant (and the current restaurant if it is different than the default one). Makes the load demonstration data button visible when there is no product with a positive price available, because without that condition the tips (in restaurant) and discount were preventing the display of the button.
 - Hides the Preparation Printers menu item if there is no POS config using the printers.
 - Renames some labels.

Fixes:
 - Adds checks to always set set_tip_after_payment to False if a point of sale is no longer a restaurant or if the tips are disabled. This is needed because the code of pos_restaurant seems to be written considering that set_tip_after_payment is True only when the point of sale is a restaurant with the tips enabled (which wasn't guaranteed). Without that checks, some tests like ReceiptScreenTour fail. Another solution to make the tests successful without that checks would be to add !this.env.pos.config.module_pos_restaurant in pos_restaurant/static/src/js/Screens/PaymentScreen.js:16. But because other parts of the code only check the value of set_tip_after_payment, I think it is better to opt for the additionnal checks.
 - Fixes duplication of displayed product categories when loading demonstration data.
 - Fixes products before loading demonstration data that were kept and displayed after (discount for example).
 - Fixes categories and therefore communication with the preparation display after loading demo data in a restaurant.
 - Adds a check on the server side to prevent demonstration data loading when there is at least one valid product for POS.
 - Fixes order count display when "NaN" (in floors and tables view).
 - Fixes arguments of orm call for set_not_tip (model and method names were not given in the right order).

closes odoo/odoo#117941

Task-id: 2484291
Related: odoo/enterprise#39450
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
Theo VINCENT (thvi)
2023-06-26 20:00:35 +02:00
parent e02f9804c7
commit 3236c1cb50
25 changed files with 402 additions and 59 deletions
+12
View File
@@ -395,6 +395,7 @@ class PosConfig(models.Model):
pos_configs = super().create(vals_list)
pos_configs.sudo()._check_modules_to_install()
pos_configs.sudo()._check_groups_implied()
pos_configs._update_preparation_printers_menuitem_visibility()
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
return pos_configs
@@ -406,16 +407,25 @@ class PosConfig(models.Model):
else:
raise UserError(_('The default tip product is missing. Please manually specify the tip product. (See Tips field.)'))
def _update_preparation_printers_menuitem_visibility(self):
prepa_printers_menuitem = self.sudo().env.ref('point_of_sale.menu_pos_preparation_printer', raise_if_not_found=False)
if prepa_printers_menuitem:
prepa_printers_menuitem.active = self.sudo().env['pos.config'].search_count([('is_order_printer', '=', True)], limit=1) > 0
def write(self, vals):
self._reset_default_on_vals(vals)
if ('is_order_printer' in vals and not vals['is_order_printer']):
vals['printer_ids'] = [fields.Command.clear()]
bypass_categories_forbidden_change = self.env.context.get('bypass_categories_forbidden_change', False)
opened_session = self.mapped('session_ids').filtered(lambda s: s.state != 'closed')
if opened_session:
forbidden_fields = []
for key in self._get_forbidden_change_fields():
if key in vals.keys():
if bypass_categories_forbidden_change and key in ('limit_categories', 'iface_available_categ_ids'):
continue
if key == 'use_pricelist' and vals[key]:
continue
if key == 'available_pricelist_ids':
@@ -434,6 +444,8 @@ class PosConfig(models.Model):
self.sudo()._set_fiscal_position()
self.sudo()._check_modules_to_install()
self.sudo()._check_groups_implied()
if 'is_order_printer' in vals:
self._update_preparation_printers_menuitem_visibility()
return result
def _get_forbidden_change_fields(self):
@@ -38,7 +38,7 @@ class PosPaymentMethod(models.Model):
default=False,
help='Forces to set a customer when using this payment method and splits the journal entries for each customer. It could slow down the closing process.')
open_session_ids = fields.Many2many('pos.session', string='Pos Sessions', compute='_compute_open_session_ids', help='Open PoS sessions that are using this payment method.')
config_ids = fields.Many2many('pos.config', string='Point of Sale Configurations')
config_ids = fields.Many2many('pos.config', string='Point of Sale')
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
use_payment_terminal = fields.Selection(selection=lambda self: self._get_payment_terminal_selection(), string='Use a Payment Terminal', help='Record payments with a terminal on this journal.')
# used to hide use_payment_terminal when no payment interfaces are installed
+17 -5
View File
@@ -1563,8 +1563,8 @@ class PosSession(models.Model):
def get_onboarding_data(self):
return {
"categories": self._load_model('pos.category'),
"products": self._load_model('product.product'),
'pos.category': self._load_model('pos.category'),
'product.product': self._load_model('product.product'),
}
def _load_model(self, model):
@@ -1636,6 +1636,7 @@ class PosSession(models.Model):
loaded_data['attributes_by_ptal_id'] = self._get_attributes_by_ptal_id()
loaded_data['base_url'] = self.get_base_url()
loaded_data['pos_has_valid_product'] = self._pos_has_valid_product()
@api.model
def _pos_ui_models_to_load(self):
@@ -2094,10 +2095,21 @@ class PosSession(models.Model):
self.message_post(body=body, author_id=partner_id)
def _pos_has_valid_product(self):
return self.env['product.product'].sudo().search_count(['&', ('available_in_pos', '=', True), ('list_price', '>', 0)], limit=1) > 0
def _load_onboarding_data(self):
convert.convert_file(self.env, 'point_of_sale', 'data/point_of_sale_onboarding.xml', None, mode='init', kind='data')
def load_product_frontend(self):
convert.convert_file(self.env, 'point_of_sale', 'data/point_of_sale_onboarding.xml', None, mode='init',
kind='data')
return self.get_onboarding_data()
allowed = not self._pos_has_valid_product()
if allowed:
self._load_onboarding_data()
return {
'models_data': self.get_onboarding_data(),
'successful': allowed,
}
class ProcurementGroup(models.Model):
@@ -4,18 +4,20 @@
<t t-name="point_of_sale.ProductsWidgetControlPanel" owl="1">
<div class="products-widget-control" t-ref="products-widget-control">
<div class="rightpane-header">
<div class="search-bar-container sb-product">
<span t-if="state.isMobile" class="oe_status oe_icon icon" t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</span>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="pos-search-bar">
<i class="fa fa-search"/>
<input t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times" t-on-click="_clearSearch"/>
</div>
</t>
</div>
<t t-if="this.pos.posHasValidProduct()">
<div class="search-bar-container sb-product">
<span t-if="state.isMobile" class="oe_status oe_icon icon" t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</span>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="pos-search-bar">
<i class="fa fa-search"/>
<input t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times" t-on-click="_clearSearch"/>
</div>
</t>
</div>
</t>
<t t-if="!props.hasNoCategories">
<div class="categories-header">
<!-- Breadcrumbs -->
@@ -10,6 +10,7 @@ import { ProductsWidgetControlPanel } from "@point_of_sale/app/screens/product_s
import { Component, useState } from "@odoo/owl";
import { sprintf } from "@web/core/utils/strings";
import { OfflineErrorPopup } from "@point_of_sale/app/errors/popups/offline_error_popup";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
export class ProductsWidget extends Component {
static components = { ProductItem, ProductsWidgetControlPanel };
@@ -30,8 +31,8 @@ export class ProductsWidget extends Component {
this.notification = useService("pos_notification");
this.orm = useService("orm");
}
get hasProducts() {
return Object.keys(this.pos.db.product_by_id).length > 0;
get posHasValidProduct() {
return this.pos.posHasValidProduct();
}
get selectedCategoryId() {
return this.pos.selectedCategoryId;
@@ -166,17 +167,42 @@ export class ProductsWidget extends Component {
}
}
async loadDemoDataProducts() {
const { products, categories } = await this.orm.call(
const { models_data, successful } = await this.orm.call(
"pos.session",
"load_product_frontend",
[this.pos.pos_session.id]
);
this.pos.db.add_categories(categories);
this.pos._loadProductProduct(products);
if (!successful) {
this.popup.add(ErrorPopup, {
title: this.env._t("Demo products are no longer available"),
body: this.env._t(
"A valid product already exists for Point of Sale. Therefore, demonstration products cannot be loaded."
),
});
// But the received models_data is still used to update the current session.
}
if (!models_data) {
this._showLoadDemoDataMissingDataError("models_data");
return;
}
for (const dataName of ["pos.category", "product.product"]) {
if (!models_data[dataName]) {
this._showLoadDemoDataMissingDataError(dataName);
return;
}
}
this.pos.updateModelsData(models_data);
}
_showLoadDemoDataMissingDataError(missingData) {
console.error(
"Missing '",
missingData,
"' in pos.session:load_product_frontend server answer."
);
}
createNewProducts() {
window.open("/web#action=point_of_sale.action_client_product_menu", "_blank");
window.open("/web#action=point_of_sale.action_client_product_menu", "_self");
this.state.showReloadMessage = true;
}
}
@@ -13,26 +13,30 @@
hasNoCategories="hasNoCategories"
loadProductFromServer.bind="onPressEnterKey" />
<div class="product-list-container">
<div t-if="productsToDisplay.length != 0" class="product-list">
<div t-if="productsToDisplay.length != 0 and posHasValidProduct" class="product-list">
<t t-foreach="productsToDisplay" t-as="product" t-key="product.id">
<ProductItem
product="product" />
</t>
</div>
<div t-else="" class="product-list-empty no-results-message">
<p t-if="searchWord">
No product found for <b>"<t t-esc="searchWord"/>"</b> in this category.
</p>
<p t-else="">
There are no products in this category.
</p>
<div t-if="!hasProducts">
<button class="button-no-demo" t-on-click="loadDemoDataProducts">Generate demo data</button>
<button class="button-no-demo" t-on-click="createNewProducts">Create new products</button>
<div t-if="state.showReloadMessage" class="info-load-product-message">
Please reload this page once products have been created
</div>
</div>
<t t-if="searchWord">
<p>No products found for <b>"<t t-esc="searchWord"/>"</b> in this category.</p>
</t>
<t t-else="">
<t t-if="posHasValidProduct">
<p>There are no products in this category.</p>
</t>
<t t-else="">
<div>
<p>No products here, but don't worry, we've got your back.</p>
<p>Play with <a role="button" class="button-no-demo" t-on-click="loadDemoDataProducts">our demo products</a> or <a role="button" class="button-no-demo" t-on-click="createNewProducts">create your own products</a></p>
<div t-if="state.showReloadMessage" class="info-load-product-message">
Please reload this page once products have been created
</div>
</div>
</t>
</t>
</div>
<div t-if="searchWord" class="search-more-button">
<div class="button" t-on-click="onPressEnterKey">Search more</div>
+106 -1
View File
@@ -118,7 +118,10 @@ export class PosDB {
if (!self.category_childs[parent_id]) {
self.category_childs[parent_id] = [];
}
self.category_childs[parent_id].push(cat.id);
if (!self.category_childs[parent_id].includes(cat.id)) {
self.category_childs[parent_id].push(cat.id);
}
});
function make_ancestors(cat_id, ancestors) {
self.category_ancestors[cat_id] = ancestors;
@@ -133,7 +136,43 @@ export class PosDB {
}
make_ancestors(this.root_category_id, []);
}
/**
* Removes all categories specified by their id.
* @param {integer[]} categories_id
*/
remove_categories(categories_id) {
categories_id = categories_id.filter((cat_id) => cat_id !== this.root_category_id);
categories_id.forEach((cat_id) => {
if (cat_id === undefined || cat_id === null) {
return;
}
const cat = this.category_by_id[cat_id];
if (!cat) {
return;
}
const parent_id = this.category_parent[cat.id];
if (this.category_childs[parent_id]) {
this._remove_all_occurrences_from_array(this.category_childs[parent_id], cat.id);
if (this.category_childs[parent_id].length === 0) {
delete this.category_childs[parent_id];
}
}
delete this.category_parent[cat.id];
delete this.category_by_id[cat_id];
});
const removed_categories_id = new Set(categories_id);
for (const cat_id_str in this.category_ancestors) {
if (removed_categories_id.has(Number(cat_id_str))) {
delete this.category_ancestors[cat_id_str];
} else {
this.category_ancestors[cat_id_str] = this.category_ancestors[cat_id_str].filter(
(anc_id_str) => !removed_categories_id.has(Number(anc_id_str))
);
}
}
}
/* loads a record store from the database. returns default if nothing is found */
load(store, deft) {
if (CACHE[store] !== undefined) {
@@ -220,6 +259,72 @@ export class PosDB {
}
}
}
/**
* Removes all products specified by their id.
* @param {integer[]} products_id
*/
remove_products(products_id) {
if (!(products_id instanceof Array)) {
products_id = [products_id];
}
products_id.forEach((product_id) => {
if (product_id === undefined || product_id === null) {
return;
}
const product = this.product_by_id[product_id];
if (!product) {
return;
}
if (product.available_in_pos) {
const product_search_string = unaccent(this._product_search_string(product));
const all_categ_ids = product.pos_categ_ids.length
? product.pos_categ_ids
: [this.root_category_id];
for (const categ_id of all_categ_ids) {
this.remove_product_from_category(product.id, product_search_string, categ_id);
const categ_ancestors_id = this.get_category_ancestors_ids(categ_id) || [];
categ_ancestors_id.forEach((ancestor_id) => {
this.remove_product_from_category(
product.id,
product_search_string,
ancestor_id
);
});
}
}
delete this.product_by_id[product.id];
if (product.barcode) {
delete this.product_by_barcode[product.barcode];
}
});
}
remove_product_from_category(product_id, product_search_string, categ_id) {
const stored_categories = this.product_by_category_id;
if (stored_categories[categ_id]) {
this._remove_all_occurrences_from_array(stored_categories[categ_id], product_id);
if (stored_categories[categ_id].length === 0) {
delete stored_categories[categ_id];
}
}
if (this.category_search_string[categ_id]) {
this.category_search_string[categ_id] = this.category_search_string[categ_id].replace(
product_search_string,
""
);
}
if (!this.category_search_string[categ_id]) {
delete this.category_search_string[categ_id];
}
}
_remove_all_occurrences_from_array(array, element) {
for (let i = array.length - 1; i >= 0; i--) {
if (array[i] === element) {
array.splice(i, 1);
}
}
}
add_packagings(product_packagings) {
var self = this;
Object.values(product_packagings || {}).map((product_packaging) => {
@@ -243,6 +243,7 @@ export class PosStore extends Reactive {
this._loadPosPaymentMethod();
this.fiscal_positions = loadedData["account.fiscal.position"];
this.base_url = loadedData["base_url"];
this.pos_has_valid_product = loadedData["pos_has_valid_product"];
await this._loadPictures();
await this._loadPosPrinters(loadedData["pos.printer"]);
}
@@ -393,6 +394,53 @@ export class PosStore extends Reactive {
return this.addPartners(partners);
}
async updateModelsData(models_data) {
const products = models_data["product.product"];
const categories = models_data["pos.category"];
let removed_categories_id;
if (categories) {
const previous_categories_id = Object.values(this.db.category_by_id).map((c) => c.id);
const received_categories_id = new Set(categories.map((c) => c.id));
this.db.add_categories(categories);
removed_categories_id = previous_categories_id.filter(
(p) => !received_categories_id.has(p)
);
}
if (products) {
const previous_products_id = Object.values(this.db.product_by_id).map((p) => p.id);
const received_products_id = new Set(products.map((p) => p.id));
this._loadProductProduct(products);
const removed_products_id = previous_products_id.filter(
(p) => !received_products_id.has(p)
);
this.db.remove_products(removed_products_id);
if (
Object.values(this.db.product_by_id).some(
(p) => p.available_in_pos && p.lst_price > 0
)
) {
this.pos_has_valid_product = true;
}
}
if (categories) {
this.db.remove_categories(removed_categories_id);
}
}
/**
* @returns true if the POS app (not only this POS config) has at least one valid product.
*/
posHasValidProduct() {
return (
this.pos_has_valid_product ||
Object.values(this.db.product_by_id).some((p) => p.available_in_pos && p.lst_price > 0)
);
}
setSelectedCategoryId(categoryId) {
this.selectedCategoryId = categoryId;
}
@@ -485,7 +485,7 @@ td {
justify-content: center;
flex-direction: column;
border: none;
h1 {
font-size: 20px;
margin: 0;
@@ -889,7 +889,7 @@ td {
color: white;
border: solid 1px $primary;
}
}
.pos .product-list-scroller{
@@ -2945,7 +2945,7 @@ td {
font-size: 12px;
font-weight: bold;
margin: 5px;
div {
margin: 0;
}
@@ -3719,6 +3719,8 @@ td {
padding: 10px 0;
cursor: pointer;
font-size: large;
color: #017e84;
font-weight: bold;
}
.info-load-product-message {
@@ -3901,7 +3903,7 @@ td {
.menu-item {
padding: 10px;
span, i {
color: black;
inline-size: 196px;
+2
View File
@@ -17,12 +17,14 @@ This module adds several features to the Point of Sale that are specific to rest
""",
'depends': ['point_of_sale'],
'auto_install': True,
'website': 'https://www.odoo.com/app/point-of-sale-restaurant',
'data': [
'security/ir.model.access.csv',
'views/pos_order_views.xml',
'views/pos_restaurant_views.xml',
'views/res_config_settings_views.xml',
'data/pos_restaurant_data.xml',
],
'demo': [
'data/pos_restaurant_demo.xml',
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="pos_config_main_restaurant" model="pos.config">
<field name="name">Restaurant</field>
<field name="module_pos_restaurant">True</field>
<field name="iface_splitbill">True</field>
<field name="iface_printbill">False</field>
<field name="iface_orderline_notes">True</field>
<field name="iface_tipproduct">False</field>
<field name="start_category">False</field>
<field name="limit_categories">False</field>
</record>
<function model="pos.config" name="_set_tips_after_payment_if_country_custom">
<value eval="[ref('pos_config_main_restaurant')]"/>
</function>
</data>
</odoo>
@@ -0,0 +1,55 @@
<?xml version="1.0"?>
<odoo>
<record id="onboarding_drinks_category" model="pos.category">
<field name="name">Drinks</field>
</record>
<record id="onboarding_food_category" model="pos.category">
<field name="name">Food</field>
</record>
<record id="product_category_pos_restaurant_onboarding_food" model="product.category">
<field name="parent_id" ref="product.product_category_1" />
<field name="name">Restaurant food</field>
</record>
<!-- Food -->
<record id="pos_food_margherita" model="product.product">
<field name="available_in_pos">True</field>
<field name="list_price">7.0</field>
<field name="name">Margherita</field>
<field name="weight">0.01</field>
<field name="pos_categ_ids" eval="[(6, 0, [ref('onboarding_food_category')])]" />
<field name="categ_id" ref="pos_restaurant.product_category_pos_restaurant_onboarding_food" />
<field name="image_1920" type="base64" file="pos_restaurant/static/img/th-pizza.png" />
</record>
<record id="pos_food_cheeseburger" model="product.product">
<field name="available_in_pos">True</field>
<field name="list_price">7.0</field>
<field name="name">Cheese Burger</field>
<field name="weight">0.01</field>
<field name="pos_categ_ids" eval="[(6, 0, [ref('onboarding_food_category')])]" />
<field name="categ_id" ref="pos_restaurant.product_category_pos_restaurant_onboarding_food" />
<field name="image_1920" type="base64" file="pos_restaurant/static/img/th-cheeseburger.jpg" />
</record>
<!-- Drinks -->
<record id="coke" model="product.product">
<field name="available_in_pos">True</field>
<field name="list_price">2.20</field>
<field name="name">Coca-Cola</field>
<field name="weight">0.01</field>
<field name="pos_categ_ids" eval="[(6, 0, [ref('onboarding_drinks_category')])]" />
<field name="categ_id" ref="point_of_sale.product_category_pos" />
<field name="image_1920" type="base64" file="pos_restaurant/static/img/th-coke.jpg" />
</record>
<record id="water" model="product.product">
<field name="available_in_pos">True</field>
<field name="list_price">2.20</field>
<field name="name">Water</field>
<field name="weight">0.01</field>
<field name="pos_categ_ids" eval="[(6, 0, [ref('onboarding_drinks_category')])]" />
<field name="categ_id" ref="point_of_sale.product_category_pos" />
<field name="image_1920" type="base64" file="pos_restaurant/static/img/th-water.jpg" />
</record>
</odoo>
+24 -1
View File
@@ -59,10 +59,33 @@ class PosConfig(models.Model):
forbidden_keys.append('floor_ids')
return forbidden_keys
def _set_tips_after_payment_if_country_custom(self):
self.ensure_one()
company = self.company_id or self.env.company or self.env['res.company']._get_main_company()
if company and company.country_id and company.country_id.code == 'US':
self.update({
'iface_tipproduct': True,
'set_tip_after_payment': True,
})
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
is_restaurant = 'module_pos_restaurant' not in vals or vals['module_pos_restaurant']
if is_restaurant and 'iface_splitbill' not in vals:
vals['iface_splitbill'] = True
if not is_restaurant or not vals.get('iface_tipproduct', False):
vals['set_tip_after_payment'] = False
return super(PosConfig, self).create(vals_list)
def write(self, vals):
if ('module_pos_restaurant' in vals and vals['module_pos_restaurant'] is False):
vals['floor_ids'] = [(5, 0, 0)]
return super(PosConfig, self).write(vals)
if ('module_pos_restaurant' in vals and not vals['module_pos_restaurant']) or ('iface_tipproduct' in vals and not vals['iface_tipproduct']):
vals['set_tip_after_payment'] = False
return super().write(vals)
@api.model
def add_cash_payment_method(self):
+11 -1
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import models, Command
from odoo.tools import convert
from itertools import groupby
from odoo.osv.expression import AND
@@ -52,3 +53,12 @@ class PosSession(models.Model):
def get_pos_ui_restaurant_floor(self):
return self._get_pos_ui_restaurant_floor(self._loader_params_restaurant_floor())
def _load_onboarding_data(self):
super()._load_onboarding_data()
convert.convert_file(self.env, 'pos_restaurant', 'data/pos_restaurant_onboarding.xml', None, mode='init', kind='data')
configs = self.config_id.filtered('module_pos_restaurant').union(self.env.ref('pos_restaurant.pos_config_main_restaurant', raise_if_not_found=False))
configs.with_context(bypass_categories_forbidden_change=True).write({
'limit_categories': True,
'iface_available_categ_ids': [Command.link(self.env.ref('pos_restaurant.onboarding_drinks_category').id), Command.link(self.env.ref('pos_restaurant.onboarding_food_category').id)]
})
@@ -6,7 +6,7 @@
<span class="edit-button first-button" t-if="!ui.isSmall"></span>
<span class="edit-button" t-on-click.stop="props.createTable">
<i class="fa fa-plus icon-button" role="img" aria-label="Add" title="Add"></i>
<span class="text-button">ADD</span>
<span class="text-button">TABLE</span>
</span>
<span class="edit-button" t-att-class="{ disabled: props.selectedTables.length == 0 }" t-on-click.stop="props.changeSeatsNum">
<i class="fa fa-user icon-button" role="img" aria-label="Seats" title="Seats"></i>
@@ -63,7 +63,7 @@
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setTableColor('#ACADAD')" />
<span class="color" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setTableColor('#4ED2BE')" />
</div>
<div t-if="props.isColorPicker and props.selectedTables.length == 0" class="color-picker">
<div t-if="props.isColorPicker and props.selectedTables.length == 0" class="color-picker">
<span class="color" style="background-color:rgb(255, 255, 255)" role="img" aria-label="White" title="White" t-on-click.stop="() => props.setFloorColor('rgb(255, 255, 255)')" />
<span class="color" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setFloorColor('rgb(244, 149, 149)')" />
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setFloorColor('rgb(130, 233, 171)')" />
@@ -363,7 +363,8 @@ export class FloorScreen extends Component {
}
async addFloor() {
const { confirmed, payload: newName } = await this.popup.add(TextInputPopup, {
title: this.env._t("Floor Name"),
title: this.env._t("New Floor"),
placeholder: this.env._t("Floor name"),
});
if (!confirmed) {
return;
@@ -3,10 +3,10 @@
<t t-name="pos_restaurant.FloorScreen" owl="1">
<div class="floor-screen screen">
<EditBar t-if="pos.isEditMode" selectedTables="selectedTables" nbrFloors="nbrFloors"
<EditBar t-if="pos.isEditMode" selectedTables="selectedTables" nbrFloors="nbrFloors"
floorMapScrollTop="state.floorMapScrollTop" isColorPicker="state.isColorPicker" toggleColorPicker.bind="toggleColorPicker"
createTable.bind="createTable" duplicateTableOrFloor.bind="duplicateTableOrFloor" renameTable.bind="renameTable"
changeSeatsNum.bind="changeSeatsNum" changeToCircle.bind="changeToCircle" changeToSquare.bind="changeToSquare"
changeSeatsNum.bind="changeSeatsNum" changeToCircle.bind="changeToCircle" changeToSquare.bind="changeToSquare"
setTableColor.bind="setTableColor" setFloorColor.bind="setFloorColor" deleteFloorOrTable.bind="deleteFloorOrTable"
toggleEditMode.bind="toggleEditMode"
/>
@@ -31,7 +31,7 @@
>
<t t-if="pos.floors.length > 0">
<div t-if="isFloorEmpty" class="empty-floor" t-ref="map">
<span>No tables available,<br/>ADD your first table</span>
<span>Oops! No tables available.<br/>Add a new table to get started.</span>
</div>
<div t-else="" t-ref="map">
<t t-foreach="activeTables" t-as="table" t-key="table.id">
@@ -42,7 +42,7 @@
</t>
<t t-else="">
<div class="empty-floor" t-ref="map">
<span>No floors available,<br/>ADD a floor to start</span>
<span>Oops! No floors available.<br/>Add a new floor to get started.</span>
</div>
</t>
</div>
@@ -99,12 +99,15 @@ export class Table extends Component {
// do not count the orders that are already finalized
!o.finalized
);
let result;
if (table.changes_count > 0) {
return table.changes_count;
result = table.changes_count;
} else if (table.skip_changes > 0) {
return table.skip_changes;
result = table.skip_changes;
} else {
result = table.order_count + unsynced_orders.length;
}
return table.order_count + unsynced_orders.length;
return !Number.isNaN(result) ? result : 0;
}
get orderCountClass() {
const countClass = { "order-count": true };
@@ -117,7 +117,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
}
},
async setNoTip(serverId) {
await this.orm.call("set_no_tip", "pos.order", [serverId]);
await this.orm.call("pos.order", "set_no_tip", [serverId]);
},
_getOrderStates() {
const result = this._super(...arguments);
@@ -49,6 +49,7 @@ class TestFrontend(odoo.tests.HttpCase):
'start_category': True,
'is_order_printer': True,
'printer_ids': [(4, printer.id)],
'iface_tipproduct': False,
})
main_floor = self.env['restaurant.floor'].create({
@@ -33,7 +33,7 @@
<div id="tip_product" position="after">
<div attrs="{'invisible': ['|', ('pos_module_pos_restaurant', '=', False), ('pos_iface_tipproduct', '=', False)]}">
<field name="pos_set_tip_after_payment" class="oe_inline"/>
<label class="fw-normal" for="pos_set_tip_after_payment" string="Add tip after payment (North America specific)"/>
<label class="fw-normal" for="pos_set_tip_after_payment" string="Add tip after payment"/>
</div>
</div>
</field>
+1
View File
@@ -19,6 +19,7 @@
"data/restaurant_table_data.xml",
"views/res_config_settings_views.xml",
"views/point_of_sale_dashboard.xml",
"data/pos_restaurant_data.xml",
],
"assets": {
"pos_self_order.assets_self_order": [
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="pos_restaurant.pos_config_main_restaurant" model="pos.config">
<field name="self_order_view_mode">True</field>
<field name="self_order_table_mode">True</field>
<field name="self_order_image"
model="pos.config"
eval="obj().env.ref('pos_restaurant.pos_config_main_restaurant')._self_order_default_image()"
/>
<field name="self_order_image_name"
model="pos.config"
eval="obj().env.ref('pos_restaurant.pos_config_main_restaurant')._self_order_default_image_name()"
/>
</record>
</data>
</odoo>
+1 -1
View File
@@ -36,7 +36,7 @@ class PosConfig(models.Model):
self_order_pay_after = fields.Selection(
[("each", "Each Order"), ("meal", "Meal")],
string="Pay After:",
default="each",
default="meal",
help="Choose when the customer will pay",
required=True,
)
@@ -16,7 +16,7 @@ class ResConfigSettings(models.TransientModel):
pos_self_order_pay_after = fields.Selection(
[("each", "Each Order"), ("meal", "Meal")],
string="Pay After:",
default="each",
default="meal",
help="Choose when the customer will pay",
readonly=False,
required=True,