summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/point_of_sale/static/src/js')
-rw-r--r--addons/point_of_sale/static/src/js/Chrome.js8
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js56
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js41
-rw-r--r--addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js15
-rw-r--r--addons/point_of_sale/static/src/js/Misc/SearchBar.js115
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js4
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js181
-rw-r--r--addons/point_of_sale/static/src/js/main.js2
-rw-r--r--addons/point_of_sale/static/src/js/models.js44
-rw-r--r--addons/point_of_sale/static/src/js/utils.js4
12 files changed, 391 insertions, 85 deletions
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
index 7f950cfab71..e01a434d9a8 100644
--- a/addons/point_of_sale/static/src/js/Chrome.js
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -197,7 +197,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
this.showScreen(name, props);
}
_getSavedScreen(order) {
- return order.get_screen_data('screen') || { name: 'ProductScreen' };
+ return order.get_screen_data();
}
__showTempScreen(event) {
const { name, props, resolve } = event.detail;
@@ -236,7 +236,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
_setScreenData(name, props) {
const order = this.env.pos.get_order();
if (order) {
- order.set_screen_data('screen', { name, props });
+ order.set_screen_data({ name, props });
}
}
async _closePos() {
@@ -325,6 +325,10 @@ odoo.define('point_of_sale.Chrome', function(require) {
}
}
+ get isTicketScreenShown() {
+ return this.mainScreen.name === 'TicketScreen';
+ }
+
// MISC METHODS //
async _loadDemoData() {
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
deleted file mode 100644
index ad2d8265417..00000000000
--- a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
+++ /dev/null
@@ -1,56 +0,0 @@
-odoo.define('point_of_sale.OrderSelector', function(require) {
- 'use strict';
-
- const { useContext } = owl.hooks;
- const PosComponent = require('point_of_sale.PosComponent');
- const Registries = require('point_of_sale.Registries');
- const contexts = require('point_of_sale.PosContext');
-
- // Previously OrderSelectorWidget
- class OrderSelector extends PosComponent {
- constructor() {
- super(...arguments);
- // this is used in its template
- this.chromeContext = useContext(contexts.chrome);
- }
- mounted() {
- this.env.pos.get('orders').on('add remove change', () => this.render(), this);
- this.env.pos.on('change:selectedOrder', () => this.render(), this);
- }
- willUnmount() {
- this.env.pos.get('orders').off('add remove change', null, this);
- this.env.pos.off('change:selectedOrder', null, this);
- }
- selectOrder(order) {
- this.env.pos.set_order(order);
- }
- addNewOrder() {
- this.env.pos.add_new_order();
- }
- get orderList() {
- return this.env.pos.get_order_list();
- }
- async deleteCurrentOrder() {
- const order = this.env.pos.get_order();
- if (!order) {
- return;
- } else if (!order.is_empty()) {
- const { confirmed } = await this.showPopup('ConfirmPopup', {
- title: this.env._t('Destroy Current Order ?'),
- body: this.env._t('You will lose any data associated with the current order'),
- });
- if (confirmed) this.env.pos.delete_current_order();
- } else {
- this.env.pos.delete_current_order();
- }
- }
- creationTime(order) {
- return moment(order.creation_date).format('hh:mm');
- }
- }
- OrderSelector.template = 'OrderSelector';
-
- Registries.Component.add(OrderSelector);
-
- return OrderSelector;
-});
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js
new file mode 100644
index 00000000000..d142bbdeac0
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js
@@ -0,0 +1,41 @@
+odoo.define('point_of_sale.TicketButton', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { posbus } = require('point_of_sale.utils');
+
+ class TicketButton extends PosComponent {
+ onClick() {
+ if (this.props.isTicketScreenShown) {
+ posbus.trigger('ticket-button-clicked');
+ } else {
+ this.showScreen('TicketScreen');
+ }
+ }
+ willPatch() {
+ posbus.off('order-deleted', this);
+ }
+ patched() {
+ posbus.on('order-deleted', this, this.render);
+ }
+ mounted() {
+ posbus.on('order-deleted', this, this.render);
+ }
+ willUnmount() {
+ posbus.off('order-deleted', this);
+ }
+ get count() {
+ if (this.env.pos) {
+ return this.env.pos.get_order_list().length;
+ } else {
+ return 0;
+ }
+ }
+ }
+ TicketButton.template = 'TicketButton';
+
+ Registries.Component.add(TicketButton);
+
+ return TicketButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
index 939f0daa43f..e2f2148b0c7 100644
--- a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
+++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
@@ -4,13 +4,18 @@ odoo.define('point_of_sale.IndependentToOrderScreen', function (require) {
const PosComponent = require('point_of_sale.PosComponent');
class IndependentToOrderScreen extends PosComponent {
+ /**
+ * Alias the forceTriggerSelectedOrder method as it also
+ * means 'closing' this screen.
+ */
close() {
- // To close this order-indenpendent screen, we forcefully trigger change
+ this.forceTriggerSelectedOrder();
+ }
+ forceTriggerSelectedOrder() {
+ // Calling this method forcefully trigger change
// on the selectedOrder attribute, which then shows the screen of the
- // current order.
- this.env.pos.trigger('change:selectedOrder', this.env.pos, this.env.pos.get_order(), {
- silent: true,
- });
+ // current order, essentially closing this screen.
+ this.env.pos.trigger('change:selectedOrder', this.env.pos, this.env.pos.get_order());
}
}
diff --git a/addons/point_of_sale/static/src/js/Misc/SearchBar.js b/addons/point_of_sale/static/src/js/Misc/SearchBar.js
new file mode 100644
index 00000000000..d9bcfe879b1
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Misc/SearchBar.js
@@ -0,0 +1,115 @@
+odoo.define('point_of_sale.SearchBar', function (require) {
+ 'use strict';
+
+ const { useState, useExternalListener } = owl.hooks;
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * This is a simple configurable search bar component. It has search fields
+ * and selection filter. Search fields allow the users to specify the type
+ * of their searches. The filter is a dropdown menu for selection. Depending on
+ * user's action, this component emits corresponding event with the action
+ * information (payload).
+ *
+ * TODO: This component can be made more generic and be able to replace
+ * all the search bars across pos ui.
+ *
+ * @prop {{
+ * config: {
+ * searchFields: string[],
+ * filter: { show: boolean, options: string[] }
+ * },
+ * placeholder: string,
+ * }}
+ * @emits search @payload { fieldValue: string, searchTerm: '' }
+ * @emits filter-selected @payload { filter: string }
+ *
+ * NOTE: The payload of the emitted event is accessible via the `detail`
+ * field of the event.
+ */
+ class SearchBar extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.config = this.props.config;
+ this.state = useState({
+ searchInput: '',
+ selectedFieldId: this.config.searchFields.length ? 0 : null,
+ showSearchFields: false,
+ showFilterOptions: false,
+ selectedFilter: this.config.filter.options[0] || 'Select',
+ });
+ useExternalListener(window, 'click', this._hideOptions);
+ }
+ selectFilter(option) {
+ this.state.selectedFilter = option;
+ this.trigger('filter-selected', { filter: this.state.selectedFilter });
+ }
+ get placeholder() {
+ return this.props.placeholder;
+ }
+ /**
+ * When vertical arrow keys are pressed, select fields for searching.
+ * When enter key is pressed, trigger search event if there is searchInput.
+ */
+ onKeydown(event) {
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
+ event.preventDefault();
+ this.state.selectedFieldId = this._fieldIdToSelect(event.key);
+ } else if (event.key === 'Enter') {
+ this.trigger('search', {
+ fieldValue: this.config.searchFields[this.state.selectedFieldId],
+ searchTerm: this.state.searchInput,
+ });
+ this.state.showSearchFields = false;
+ } else {
+ if (this.state.selectedFieldId === null && this.config.searchFields.length) {
+ this.state.selectedFieldId = 0;
+ }
+ this.state.showSearchFields = true;
+ }
+ }
+ /**
+ * Called when a search field is clicked.
+ */
+ onClickSearchField(id) {
+ this.state.showSearchFields = false;
+ this.trigger('search', {
+ fieldValue: this.config.searchFields[id],
+ searchTerm: this.state.searchInput,
+ });
+ }
+ /**
+ * Given an arrow key, return the next selectedFieldId.
+ * E.g. If the selectedFieldId is 1 and ArrowDown is pressed, return 2.
+ *
+ * @param {string} key vertical arrow key
+ */
+ _fieldIdToSelect(key) {
+ const length = this.config.searchFields.length;
+ if (!length) return null;
+ if (this.state.selectedFieldId === null) return 0;
+ const current = this.state.selectedFieldId || length;
+ return (current + (key === 'ArrowDown' ? 1 : -1)) % length;
+ }
+ _hideOptions() {
+ this.state.showFilterOptions = false;
+ this.state.showSearchFields = false;
+ }
+ }
+ SearchBar.template = 'point_of_sale.SearchBar';
+ SearchBar.defaultProps = {
+ config: {
+ searchFields: [],
+ filter: {
+ show: false,
+ options: [],
+ },
+ },
+ placeholder: 'Search ...',
+ };
+
+ Registries.Component.add(SearchBar);
+
+ return SearchBar;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
index f4b80c12190..dcde9739f60 100644
--- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -86,10 +86,9 @@ odoo.define('point_of_sale.OrderManagementScreen', function (require) {
* @param {models.Order} order
*/
_setOrder(order) {
+ this.env.pos.set_order(order);
if (order === this.env.pos.get_order()) {
this.close();
- } else {
- this.env.pos.set_order(order);
}
}
}
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index 641fb621f4b..2d552e4a1d4 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -23,8 +23,6 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
useListener('send-payment-cancel', this._sendPaymentCancel);
useListener('send-payment-reverse', this._sendPaymentReverse);
useListener('send-force-done', this._sendForceDone);
- onChangeOrder(this._onPrevOrder, this._onNewOrder);
- useErrorHandlers();
NumberBuffer.use({
// The numberBuffer listens to this event to update its state.
// Basically means 'update the buffer when this event is triggered'
@@ -33,6 +31,8 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
// Note that the component listens to it.
triggerAtInput: 'update-selected-paymentline',
});
+ onChangeOrder(this._onPrevOrder, this._onNewOrder);
+ useErrorHandlers();
this.payment_interface = null;
}
get currentOrder() {
diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
index 5bb49de5f07..8475697926d 100644
--- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
+++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
@@ -54,6 +54,9 @@ odoo.define('point_of_sale.ProductsWidget', function(require) {
this.selectedCategoryId,
].map(id => this.env.pos.db.get_category_by_id(id));
}
+ get hasNoCategories() {
+ return this.env.pos.db.get_category_childs_ids(0).length === 0;
+ }
_switchCategory(event) {
this.env.pos.set('selectedCategoryId', event.detail);
}
diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js
new file mode 100644
index 00000000000..724766e93da
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js
@@ -0,0 +1,181 @@
+odoo.define('point_of_sale.TicketScreen', function (require) {
+ 'use strict';
+
+ const Registries = require('point_of_sale.Registries');
+ const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
+ const { useListener } = require('web.custom_hooks');
+ const { posbus } = require('point_of_sale.utils');
+
+ class TicketScreen extends IndependentToOrderScreen {
+ constructor() {
+ super(...arguments);
+ useListener('close-screen', this.close);
+ useListener('filter-selected', this._onFilterSelected);
+ useListener('search', this._onSearch);
+ this.searchDetails = {};
+ this.filter = null;
+ this._initializeSearchFieldConstants();
+ }
+ mounted() {
+ posbus.on('ticket-button-clicked', this, this.close);
+ this.env.pos.get('orders').on('add remove change', () => this.render(), this);
+ this.env.pos.on('change:selectedOrder', () => this.render(), this);
+ }
+ willUnmount() {
+ posbus.off('ticket-button-clicked', this);
+ this.env.pos.get('orders').off('add remove change', null, this);
+ this.env.pos.off('change:selectedOrder', null, this);
+ }
+ _onFilterSelected(event) {
+ this.filter = event.detail.filter;
+ this.render();
+ }
+ _onSearch(event) {
+ const searchDetails = event.detail;
+ Object.assign(this.searchDetails, searchDetails);
+ this.render();
+ }
+ /**
+ * Override to conditionally show the new ticket button.
+ */
+ get showNewTicketButton() {
+ return true;
+ }
+ get orderList() {
+ return this.env.pos.get_order_list();
+ }
+ get filteredOrderList() {
+ const filterCheck = (order) => {
+ if (this.filter && this.filter !== 'All Tickets') {
+ const screen = order.get_screen_data();
+ return this.filter === this.constants.screenToStatusMap[screen.name];
+ }
+ return true;
+ };
+ const { fieldValue, searchTerm } = this.searchDetails;
+ const fieldAccessor = this._searchFields[fieldValue];
+ const searchCheck = (order) => {
+ if (!fieldAccessor) return true;
+ const fieldValue = fieldAccessor(order);
+ if (fieldValue === null) return true;
+ if (!searchTerm) return true;
+ return fieldValue && fieldValue.toString().toLowerCase().includes(searchTerm.toLowerCase());
+ };
+ const predicate = (order) => {
+ return filterCheck(order) && searchCheck(order);
+ };
+ return this.orderList.filter(predicate);
+ }
+ selectOrder(order) {
+ this._setOrder(order);
+ if (order === this.env.pos.get_order()) {
+ this.close();
+ }
+ }
+ _setOrder(order) {
+ this.env.pos.set_order(order);
+ }
+ createNewOrder() {
+ this.env.pos.add_new_order();
+ }
+ async deleteOrder(order) {
+ const screen = order.get_screen_data();
+ if (['ProductScreen', 'PaymentScreen'].includes(screen.name) && order.get_orderlines().length > 0) {
+ const { confirmed } = await this.showPopup('ConfirmPopup', {
+ title: 'Existing orderlines',
+ body: `${order.name} has total amount of ${this.getTotal(
+ order
+ )}, are you sure you want delete this order?`,
+ });
+ if (!confirmed) return;
+ }
+ if (order) {
+ order.destroy({ reason: 'abandon' });
+ }
+ posbus.trigger('order-deleted');
+ }
+ getDate(order) {
+ return moment(order.creation_date).format('YYYY-MM-DD hh:mm A');
+ }
+ getTotal(order) {
+ return this.env.pos.format_currency(order.get_total_with_tax());
+ }
+ getCustomer(order) {
+ return order.get_client_name();
+ }
+ getEmployee(order) {
+ return order.employee.name;
+ }
+ getStatus(order) {
+ const screen = order.get_screen_data();
+ return this.constants.screenToStatusMap[screen.name];
+ }
+ /**
+ * Hide the delete button if one of the payments is a 'done' electronic payment.
+ */
+ hideDeleteButton(order) {
+ return order
+ .get_paymentlines()
+ .some((payment) => payment.is_electronic() && payment.get_payment_status() === 'done');
+ }
+ get searchBarConfig() {
+ return {
+ searchFields: this.constants.searchFieldNames,
+ filter: { show: true, options: ['All Tickets', 'Ongoing', 'Payment', 'Receipt'] },
+ };
+ }
+ /**
+ * An object with keys containing the search field names which map to functions.
+ * The mapped functions will be used to generate representative string for the order
+ * to match the search term when searching.
+ * E.g. Given 2 orders, search those with `Receipt Number` containing `1111`.
+ * ```
+ * orders = [{
+ * name: '000-1111-222'
+ * total: 10,
+ * }, {
+ * name: '444-5555-666'
+ * total: 15,
+ * }]
+ * ```
+ * `Receipt Number` search field maps to the `name` of the order. So, the orders will be
+ * represented by their name, and the search will result to:
+ * ```
+ * result = [{
+ * name: '000-1111-222',
+ * total: 10,
+ * }]
+ * ```
+ * @returns Record<string, (models.Order) => string>
+ */
+ get _searchFields() {
+ return {
+ 'Receipt Number': (order) => order.name,
+ Date: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'),
+ Customer: (order) => order.get_client_name(),
+ };
+ }
+ /**
+ * Maps the order screen params to order status.
+ */
+ get _screenToStatusMap() {
+ return {
+ ProductScreen: 'Ongoing',
+ PaymentScreen: 'Payment',
+ ReceiptScreen: 'Receipt',
+ };
+ }
+ _initializeSearchFieldConstants() {
+ this.constants = {};
+ Object.assign(this.constants, {
+ searchFieldNames: Object.keys(this._searchFields),
+ screenToStatusMap: this._screenToStatusMap,
+ });
+ }
+ }
+ TicketScreen.template = 'TicketScreen';
+
+ Registries.Component.add(TicketScreen);
+
+ return TicketScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js
index dc8ea5d7b7b..346a61679f4 100644
--- a/addons/point_of_sale/static/src/js/main.js
+++ b/addons/point_of_sale/static/src/js/main.js
@@ -11,6 +11,8 @@ odoo.define('web.web_client', function (require) {
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
owl.Component.env = env;
+ Registries.Component.add(owl.misc.Portal);
+
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 44e9527ba9c..6d0d5ff63f8 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -717,11 +717,11 @@ exports.PosModel = Backbone.Model.extend({
var order_list = this.get_order_list();
if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
// when we intentionally remove an unfinished order, and there is another existing one
- this.set_order(order_list[index] || order_list[order_list.length -1]);
+ this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true });
}else{
// when the order was automatically removed after completion,
// or when we intentionally delete the only concurrent order
- this.add_new_order();
+ this.add_new_order({ silent: true });
}
},
@@ -739,10 +739,10 @@ exports.PosModel = Backbone.Model.extend({
this.db.set_cashier(this.get('cashier'));
},
// creates a new empty order and sets it as the current order
- add_new_order: function(){
+ add_new_order: function(options){
var order = new exports.Order({},{pos:this});
this.get('orders').add(order);
- this.set('selectedOrder', order);
+ this.set('selectedOrder', order, options);
return order;
},
/**
@@ -810,8 +810,8 @@ exports.PosModel = Backbone.Model.extend({
},
// change the current order
- set_order: function(order){
- this.set({ selectedOrder: order });
+ set_order: function(order, options){
+ this.set({ selectedOrder: order }, options);
},
// return the list of unpaid orders
@@ -2469,6 +2469,12 @@ exports.Paymentline = Backbone.Model.extend({
ticket: this.ticket,
};
},
+ // If payment status is a non-empty string, then it is an electronic payment.
+ // TODO: There has to be a less confusing way to distinguish simple payments
+ // from electronic transactions. Perhaps use a flag?
+ is_electronic: function() {
+ return Boolean(this.get_payment_status());
+ },
});
var PaymentlineCollection = Backbone.Collection.extend({
@@ -3280,23 +3286,27 @@ exports.Order = Backbone.Model.extend({
// the order also stores the screen status, as the PoS supports
// different active screens per order. This method is used to
// store the screen status.
- set_screen_data: function(key,value){
- if(arguments.length === 2){
- this.screen_data[key] = value;
- }else if(arguments.length === 1){
- for(var key in arguments[0]){
- this.screen_data[key] = arguments[0][key];
- }
- }
+ set_screen_data: function(value){
+ this.screen_data['value'] = value;
},
//see set_screen_data
- get_screen_data: function(key){
- return this.screen_data[key];
+ get_screen_data: function(){
+ const screen = this.screen_data['value'];
+ // If no screen data is saved
+ // no payment line -> product screen
+ // with payment line -> payment screen
+ if (!screen) {
+ if (this.get_paymentlines().length > 0) return { name: 'PaymentScreen' };
+ return { name: 'ProductScreen' };
+ }
+ if (screen.name !== 'ReceiptScreen' && this.get_paymentlines().length > 0) {
+ return { name: 'PaymentScreen' };
+ }
+ return screen;
},
wait_for_push_order: function () {
return this.is_to_email();
},
-
/**
* @returns {Object} object to use as props for instantiating OrderReceipt.
*/
diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js
index 26442ef776b..7aa7b35e93d 100644
--- a/addons/point_of_sale/static/src/js/utils.js
+++ b/addons/point_of_sale/static/src/js/utils.js
@@ -1,6 +1,8 @@
odoo.define('point_of_sale.utils', function (require) {
'use strict';
+ const { EventBus } = owl.core;
+
function getFileAsText(file) {
return new Promise((resolve, reject) => {
if (!file) {
@@ -43,5 +45,5 @@ odoo.define('point_of_sale.utils', function (require) {
);
}
- return { getFileAsText, nextFrame, isRpcError };
+ return { getFileAsText, nextFrame, isRpcError, posbus: new EventBus() };
});