summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/src/js/Screens/OrderManagementScreen
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/src/js/Screens/OrderManagementScreen')
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js155
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js36
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js25
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js29
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js214
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js31
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js124
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js101
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js42
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js55
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js32
11 files changed, 844 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js
new file mode 100644
index 00000000..53b858ba
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js
@@ -0,0 +1,155 @@
+odoo.define('point_of_sale.InvoiceButton', function (require) {
+ 'use strict';
+
+ const { useListener } = require('web.custom_hooks');
+ const { useContext } = owl.hooks;
+ const { isRpcError } = require('point_of_sale.utils');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
+ const OrderFetcher = require('point_of_sale.OrderFetcher');
+ const Registries = require('point_of_sale.Registries');
+ const contexts = require('point_of_sale.PosContext');
+
+ class InvoiceButton extends PosComponent {
+ constructor() {
+ super(...arguments);
+ useListener('click', this._onClick);
+ this.orderManagementContext = useContext(contexts.orderManagement);
+ }
+ get selectedOrder() {
+ return this.orderManagementContext.selectedOrder;
+ }
+ set selectedOrder(value) {
+ this.orderManagementContext.selectedOrder = value;
+ }
+ get isAlreadyInvoiced() {
+ if (!this.selectedOrder) return false;
+ return Boolean(this.selectedOrder.account_move);
+ }
+ get commandName() {
+ if (!this.selectedOrder) {
+ return 'Invoice';
+ } else {
+ return this.isAlreadyInvoiced
+ ? 'Reprint Invoice'
+ : this.selectedOrder.isFromClosedSession
+ ? 'Cannot Invoice'
+ : 'Invoice';
+ }
+ }
+ get isHighlighted() {
+ return this.selectedOrder && !this.isAlreadyInvoiced && !this.selectedOrder.isFromClosedSession;
+ }
+ async _downloadInvoice(orderId) {
+ try {
+ await this.env.pos.do_action('point_of_sale.pos_invoice_report', {
+ additional_context: {
+ active_ids: [orderId],
+ },
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ throw error;
+ } else {
+ // NOTE: error here is most probably undefined
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Network Error'),
+ body: this.env._t('Unable to download invoice.'),
+ });
+ }
+ }
+ }
+ async _invoiceOrder() {
+ const order = this.selectedOrder;
+ if (!order) return;
+
+ const orderId = order.backendId;
+
+ // Part 0.1. If already invoiced, print the invoice.
+ if (this.isAlreadyInvoiced) {
+ await this._downloadInvoice(orderId);
+ return;
+ }
+
+ // Part 0.2. Check if order belongs to an active session.
+ // If not, do not allow invoicing.
+ if (order.isFromClosedSession) {
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Session is closed'),
+ body: this.env._t('Cannot invoice order from closed session.'),
+ });
+ return;
+ }
+
+ // Part 1: Handle missing client.
+ // Write to pos.order the selected client.
+ if (!order.get_client()) {
+ const { confirmed: confirmedPopup } = await this.showPopup('ConfirmPopup', {
+ title: 'Need customer to invoice',
+ body: 'Do you want to open the customer list to select customer?',
+ });
+ if (!confirmedPopup) return;
+
+ const { confirmed: confirmedTempScreen, payload: newClient } = await this.showTempScreen(
+ 'ClientListScreen'
+ );
+ if (!confirmedTempScreen) return;
+
+ await this.rpc({
+ model: 'pos.order',
+ method: 'write',
+ args: [[orderId], { partner_id: newClient.id }],
+ kwargs: { context: this.env.session.user_context },
+ });
+ }
+
+ // Part 2: Invoice the order.
+ await this.rpc(
+ {
+ model: 'pos.order',
+ method: 'action_pos_order_invoice',
+ args: [orderId],
+ kwargs: { context: this.env.session.user_context },
+ },
+ {
+ timeout: 30000,
+ shadow: true,
+ }
+ );
+
+ // Part 3: Download invoice.
+ await this._downloadInvoice(orderId);
+
+ // Invalidate the cache then fetch the updated order.
+ OrderFetcher.invalidateCache([orderId]);
+ await OrderFetcher.fetch();
+ this.selectedOrder = OrderFetcher.get(this.selectedOrder.backendId);
+ }
+ async _onClick() {
+ try {
+ await this._invoiceOrder();
+ } catch (error) {
+ if (isRpcError(error) && error.message.code < 0) {
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Network Error'),
+ body: this.env._t('Unable to invoice order.'),
+ });
+ } else {
+ throw error;
+ }
+ }
+ }
+ }
+ InvoiceButton.template = 'InvoiceButton';
+
+ OrderManagementScreen.addControlButton({
+ component: InvoiceButton,
+ condition: function () {
+ return this.env.pos.config.module_account;
+ },
+ });
+
+ Registries.Component.add(InvoiceButton);
+
+ return InvoiceButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
new file mode 100644
index 00000000..5a227827
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
@@ -0,0 +1,36 @@
+odoo.define('point_of_sale.ReprintReceiptButton', function (require) {
+ 'use strict';
+
+ const { useListener } = require('web.custom_hooks');
+ const { useContext } = owl.hooks;
+ const PosComponent = require('point_of_sale.PosComponent');
+ const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
+ const Registries = require('point_of_sale.Registries');
+ const contexts = require('point_of_sale.PosContext');
+
+ class ReprintReceiptButton extends PosComponent {
+ constructor() {
+ super(...arguments);
+ useListener('click', this._onClick);
+ this.orderManagementContext = useContext(contexts.orderManagement);
+ }
+ async _onClick() {
+ const order = this.orderManagementContext.selectedOrder;
+ if (!order) return;
+
+ this.showScreen('ReprintReceiptScreen', { order: order });
+ }
+ }
+ ReprintReceiptButton.template = 'ReprintReceiptButton';
+
+ OrderManagementScreen.addControlButton({
+ component: ReprintReceiptButton,
+ condition: function () {
+ return true;
+ },
+ });
+
+ Registries.Component.add(ReprintReceiptButton);
+
+ return ReprintReceiptButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js
new file mode 100644
index 00000000..b5766ccf
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js
@@ -0,0 +1,25 @@
+odoo.define('point_of_sale.MobileOrderManagementScreen', function (require) {
+ const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
+ const Registries = require('point_of_sale.Registries');
+ const { useListener } = require('web.custom_hooks');
+ const { useState } = owl.hooks;
+
+ const MobileOrderManagementScreen = (OrderManagementScreen) => {
+ class MobileOrderManagementScreen extends OrderManagementScreen {
+ constructor() {
+ super(...arguments);
+ useListener('click-order', this._onShowDetails)
+ this.mobileState = useState({ showDetails: false });
+ }
+ _onShowDetails() {
+ this.mobileState.showDetails = true;
+ }
+ }
+ MobileOrderManagementScreen.template = 'MobileOrderManagementScreen';
+ return MobileOrderManagementScreen;
+ };
+
+ Registries.Component.addByExtending(MobileOrderManagementScreen, OrderManagementScreen);
+
+ return MobileOrderManagementScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js
new file mode 100644
index 00000000..cc0c671c
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js
@@ -0,0 +1,29 @@
+odoo.define('point_of_sale.OrderDetails', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * @props {models.Order} order
+ */
+ class OrderDetails extends PosComponent {
+ get order() {
+ return this.props.order;
+ }
+ get orderlines() {
+ return this.order ? this.order.orderlines.models : [];
+ }
+ get total() {
+ return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0);
+ }
+ get tax() {
+ return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0)
+ }
+ }
+ OrderDetails.template = 'OrderDetails';
+
+ Registries.Component.add(OrderDetails);
+
+ return OrderDetails;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js
new file mode 100644
index 00000000..57a02635
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js
@@ -0,0 +1,214 @@
+odoo.define('point_of_sale.OrderFetcher', function (require) {
+ 'use strict';
+
+ const { EventBus } = owl.core;
+ const { Gui } = require('point_of_sale.Gui');
+ const { isRpcError } = require('point_of_sale.utils');
+ const models = require('point_of_sale.models');
+
+ class OrderFetcher extends EventBus {
+ constructor() {
+ super();
+ this.currentPage = 1;
+ this.ordersToShow = [];
+ this.cache = {};
+ this.totalCount = 0;
+ }
+ get activeOrders() {
+ const allActiveOrders = this.comp.env.pos.get('orders').models;
+ return this.searchDomain
+ ? allActiveOrders.filter(this._predicateBasedOnSearchDomain.bind(this))
+ : allActiveOrders;
+ }
+ _predicateBasedOnSearchDomain(order) {
+ function check(order, field, searchWord) {
+ searchWord = searchWord.toLowerCase();
+ switch (field) {
+ case 'pos_reference':
+ return order.name.toLowerCase().includes(searchWord);
+ case 'partner_id.display_name':
+ const client = order.get_client();
+ return client ? client.name.toLowerCase().includes(searchWord) : false;
+ case 'date_order':
+ return moment(order.creation_date).format('YYYY-MM-DD hh:mm A').includes(searchWord);
+ default:
+ return false;
+ }
+ }
+ for (let [field, _, searchWord] of (this.searchDomain || []).filter((item) => item !== '|')) {
+ // remove surrounding "%" from `searchWord`
+ searchWord = searchWord.substring(1, searchWord.length - 1);
+ if (check(order, field, searchWord)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ get nActiveOrders() {
+ return this.activeOrders.length;
+ }
+ get lastPageFullOfActiveOrders() {
+ return Math.trunc(this.nActiveOrders / this.nPerPage);
+ }
+ get remainingActiveOrders() {
+ return this.nActiveOrders % this.nPerPage;
+ }
+ /**
+ * for nPerPage = 10
+ * +--------+----------+
+ * | nItems | lastPage |
+ * +--------+----------+
+ * | 2 | 1 |
+ * | 10 | 1 |
+ * | 11 | 2 |
+ * | 30 | 3 |
+ * | 35 | 4 |
+ * +--------+----------+
+ */
+ get lastPage() {
+ const nItems = this.nActiveOrders + this.totalCount;
+ return Math.trunc(nItems / (this.nPerPage + 1)) + 1;
+ }
+ /**
+ * Calling this methods populates the `ordersToShow` then trigger `update` event.
+ * @related get
+ *
+ * NOTE: This is tightly-coupled with pagination. So if the current page contains all
+ * active orders, it will not fetch anything from the server but only sets `ordersToShow`
+ * to the active orders that fits the current page.
+ */
+ async fetch() {
+ try {
+ let limit, offset;
+ let start, end;
+ if (this.currentPage <= this.lastPageFullOfActiveOrders) {
+ // Show only active orders.
+ start = (this.currentPage - 1) * this.nPerPage;
+ end = this.currentPage * this.nPerPage;
+ this.ordersToShow = this.activeOrders.slice(start, end);
+ } else if (this.currentPage === this.lastPageFullOfActiveOrders + 1) {
+ // Show partially the remaining active orders and
+ // some orders from the backend.
+ offset = 0;
+ limit = this.nPerPage - this.remainingActiveOrders;
+ start = (this.currentPage - 1) * this.nPerPage;
+ end = this.nActiveOrders;
+ this.ordersToShow = [
+ ...this.activeOrders.slice(start, end),
+ ...(await this._fetch(limit, offset)),
+ ];
+ } else {
+ // Show orders from the backend.
+ offset =
+ this.nPerPage -
+ this.remainingActiveOrders +
+ (this.currentPage - (this.lastPageFullOfActiveOrders + 1) - 1) *
+ this.nPerPage;
+ limit = this.nPerPage;
+ this.ordersToShow = await this._fetch(limit, offset);
+ }
+ this.trigger('update');
+ } catch (error) {
+ if (isRpcError(error) && error.message.code < 0) {
+ Gui.showPopup('ErrorPopup', {
+ title: this.comp.env._t('Network Error'),
+ body: this.comp.env._t('Unable to fetch orders if offline.'),
+ });
+ Gui.setSyncStatus('error');
+ } else {
+ throw error;
+ }
+ }
+ }
+ /**
+ * This returns the orders from the backend that needs to be shown.
+ * If the order is already in cache, the full information about that
+ * order is not fetched anymore, instead, we use info from cache.
+ *
+ * @param {number} limit
+ * @param {number} offset
+ */
+ async _fetch(limit, offset) {
+ const { ids, totalCount } = await this._getOrderIdsForCurrentPage(limit, offset);
+ const idsNotInCache = ids.filter((id) => !(id in this.cache));
+ if (idsNotInCache.length > 0) {
+ const fetchedOrders = await this._fetchOrders(idsNotInCache);
+ // Cache these fetched orders so that next time, no need to fetch
+ // them again, unless invalidated. See `invalidateCache`.
+ fetchedOrders.forEach((order) => {
+ this.cache[order.id] = new models.Order(
+ {},
+ { pos: this.comp.env.pos, json: order }
+ );
+ });
+ }
+ this.totalCount = totalCount;
+ return ids.map((id) => this.cache[id]);
+ }
+ async _getOrderIdsForCurrentPage(limit, offset) {
+ return await this.rpc({
+ model: 'pos.order',
+ method: 'search_paid_order_ids',
+ kwargs: { config_id: this.configId, domain: this.searchDomain ? this.searchDomain : [], limit, offset },
+ context: this.comp.env.session.user_context,
+ });
+ }
+ async _fetchOrders(ids) {
+ return await this.rpc({
+ model: 'pos.order',
+ method: 'export_for_ui',
+ args: [ids],
+ context: this.comp.env.session.user_context,
+ });
+ }
+ nextPage() {
+ if (this.currentPage < this.lastPage) {
+ this.currentPage += 1;
+ this.fetch();
+ }
+ }
+ prevPage() {
+ if (this.currentPage > 1) {
+ this.currentPage -= 1;
+ this.fetch();
+ }
+ }
+ /**
+ * @param {integer|undefined} id id of the cached order
+ * @returns {Array<models.Order>}
+ */
+ get(id) {
+ if (id) return this.cache[id];
+ return this.ordersToShow;
+ }
+ setSearchDomain(searchDomain) {
+ this.searchDomain = searchDomain;
+ }
+ setComponent(comp) {
+ this.comp = comp;
+ return this;
+ }
+ setConfigId(configId) {
+ this.configId = configId;
+ }
+ setNPerPage(val) {
+ this.nPerPage = val;
+ }
+ setPage(page) {
+ this.currentPage = page;
+ }
+ invalidateCache(ids) {
+ for (let id of ids) {
+ delete this.cache[id];
+ }
+ }
+ async rpc() {
+ Gui.setSyncStatus('connecting');
+ const result = await this.comp.rpc(...arguments);
+ Gui.setSyncStatus('connected');
+ return result;
+ }
+ }
+
+ return new OrderFetcher();
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js
new file mode 100644
index 00000000..2b4d3cd9
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js
@@ -0,0 +1,31 @@
+odoo.define('point_of_sale.OrderList', function (require) {
+ 'use strict';
+
+ const { useState } = owl.hooks;
+ const { useListener } = require('web.custom_hooks');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * @props {models.Order} [initHighlightedOrder] initially highligted order
+ * @props {Array<models.Order>} orders
+ */
+ class OrderList extends PosComponent {
+ constructor() {
+ super(...arguments);
+ useListener('click-order', this._onClickOrder);
+ this.state = useState({ highlightedOrder: this.props.initHighlightedOrder || null });
+ }
+ get highlightedOrder() {
+ return this.state.highlightedOrder;
+ }
+ _onClickOrder({ detail: order }) {
+ this.state.highlightedOrder = order;
+ }
+ }
+ OrderList.template = 'OrderList';
+
+ Registries.Component.add(OrderList);
+
+ return OrderList;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js
new file mode 100644
index 00000000..951a0956
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js
@@ -0,0 +1,124 @@
+odoo.define('point_of_sale.OrderManagementControlPanel', function (require) {
+ 'use strict';
+
+ const { useContext } = owl.hooks;
+ const { useAutofocus, useListener } = require('web.custom_hooks');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const OrderFetcher = require('point_of_sale.OrderFetcher');
+ const contexts = require('point_of_sale.PosContext');
+
+ // NOTE: These are constants so that they are only instantiated once
+ // and they can be used efficiently by the OrderManagementControlPanel.
+ const VALID_SEARCH_TAGS = new Set(['date', 'customer', 'client', 'name', 'order']);
+ const FIELD_MAP = {
+ date: 'date_order',
+ customer: 'partner_id.display_name',
+ client: 'partner_id.display_name',
+ name: 'pos_reference',
+ order: 'pos_reference',
+ };
+ const SEARCH_FIELDS = ['pos_reference', 'partner_id.display_name', 'date_order'];
+
+ function getDomainForSingleCondition(fields, toSearch) {
+ const orSymbols = Array(fields.length - 1).fill('|');
+ return orSymbols.concat(fields.map((field) => [field, 'ilike', `%${toSearch}%`]));
+ }
+
+ /**
+ * @emits close-screen
+ * @emits prev-page
+ * @emits next-page
+ * @emits search
+ */
+ class OrderManagementControlPanel extends PosComponent {
+ constructor() {
+ super(...arguments);
+ // We are using context because we want the `searchString` to be alive
+ // even if this component is destroyed (unmounted).
+ this.orderManagementContext = useContext(contexts.orderManagement);
+ useListener('clear-search', this._onClearSearch);
+ useAutofocus({ selector: 'input' });
+ }
+ onInputKeydown(event) {
+ if (event.key === 'Enter') {
+ this.trigger('search', this._computeDomain());
+ }
+ }
+ get showPageControls() {
+ return OrderFetcher.lastPage > 1;
+ }
+ get pageNumber() {
+ const currentPage = OrderFetcher.currentPage;
+ const lastPage = OrderFetcher.lastPage;
+ return isNaN(lastPage) ? '' : `(${currentPage}/${lastPage})`;
+ }
+ get validSearchTags() {
+ return VALID_SEARCH_TAGS;
+ }
+ get fieldMap() {
+ return FIELD_MAP;
+ }
+ get searchFields() {
+ return SEARCH_FIELDS;
+ }
+ /**
+ * E.g. 1
+ * ```
+ * searchString = 'Customer 1'
+ * result = [
+ * '|',
+ * '|',
+ * ['pos_reference', 'ilike', '%Customer 1%'],
+ * ['partner_id.display_name', 'ilike', '%Customer 1%'],
+ * ['date_order', 'ilike', '%Customer 1%']
+ * ]
+ * ```
+ *
+ * E.g. 2
+ * ```
+ * searchString = 'date: 2020-05'
+ * result = [
+ * ['date_order', 'ilike', '%2020-05%']
+ * ]
+ * ```
+ *
+ * E.g. 3
+ * ```
+ * searchString = 'customer: Steward, date: 2020-05-01'
+ * result = [
+ * ['partner_id.display_name', 'ilike', '%Steward%'],
+ * ['date_order', 'ilike', '%2020-05-01%']
+ * ]
+ * ```
+ */
+ _computeDomain() {
+ const input = this.orderManagementContext.searchString.trim();
+ if (!input) return;
+
+ const searchConditions = this.orderManagementContext.searchString.split(/[,&]\s*/);
+ if (searchConditions.length === 1) {
+ let cond = searchConditions[0].split(/:\s*/);
+ if (cond.length === 1) {
+ return getDomainForSingleCondition(this.searchFields, cond[0]);
+ }
+ }
+ const domain = [];
+ for (let cond of searchConditions) {
+ let [tag, value] = cond.split(/:\s*/);
+ if (!this.validSearchTags.has(tag)) continue;
+ domain.push([this.fieldMap[tag], 'ilike', `%${value}%`]);
+ }
+ return domain;
+ }
+ _onClearSearch() {
+ this.orderManagementContext.searchString = '';
+ this.onInputKeydown({ key: 'Enter' });
+ }
+ }
+ OrderManagementControlPanel.template = 'OrderManagementControlPanel';
+
+ Registries.Component.add(OrderManagementControlPanel);
+
+ return OrderManagementControlPanel;
+});
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
new file mode 100644
index 00000000..dcde9739
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -0,0 +1,101 @@
+odoo.define('point_of_sale.OrderManagementScreen', function (require) {
+ 'use strict';
+
+ const { useContext } = owl.hooks;
+ const { useListener } = require('web.custom_hooks');
+ const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
+ const NumberBuffer = require('point_of_sale.NumberBuffer');
+ const Registries = require('point_of_sale.Registries');
+ const OrderFetcher = require('point_of_sale.OrderFetcher');
+ const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
+ const contexts = require('point_of_sale.PosContext');
+
+ class OrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) {
+ constructor() {
+ super(...arguments);
+ useListener('close-screen', this.close);
+ useListener('set-numpad-mode', this._setNumpadMode);
+ useListener('click-order', this._onClickOrder);
+ useListener('next-page', this._onNextPage);
+ useListener('prev-page', this._onPrevPage);
+ useListener('search', this._onSearch);
+ NumberBuffer.use({
+ nonKeyboardInputEvent: 'numpad-click-input',
+ useWithBarcode: true,
+ });
+ this.numpadMode = 'quantity';
+ OrderFetcher.setComponent(this);
+ OrderFetcher.setConfigId(this.env.pos.config_id);
+ this.orderManagementContext = useContext(contexts.orderManagement);
+ }
+ mounted() {
+ OrderFetcher.on('update', this, this.render);
+ this.env.pos.get('orders').on('add remove', this.render, this);
+
+ // calculate how many can fit in the screen.
+ // It is based on the height of the header element.
+ // So the result is only accurate if each row is just single line.
+ const flexContainer = this.el.querySelector('.flex-container');
+ const cpEl = this.el.querySelector('.control-panel');
+ const headerEl = this.el.querySelector('.order-row.header');
+ const val = Math.trunc(
+ (flexContainer.offsetHeight - cpEl.offsetHeight - headerEl.offsetHeight) /
+ headerEl.offsetHeight
+ );
+ OrderFetcher.setNPerPage(val);
+
+ // Fetch the order after mounting so that order management screen
+ // is shown while fetching.
+ setTimeout(() => OrderFetcher.fetch(), 0);
+ }
+ willUnmount() {
+ OrderFetcher.off('update', this);
+ this.env.pos.get('orders').off('add remove', null, this);
+ }
+ get selectedClient() {
+ const order = this.orderManagementContext.selectedOrder;
+ return order ? order.get_client() : null;
+ }
+ get orders() {
+ return OrderFetcher.get();
+ }
+ async _setNumpadMode(event) {
+ const { mode } = event.detail;
+ this.numpadMode = mode;
+ NumberBuffer.reset();
+ }
+ _onNextPage() {
+ OrderFetcher.nextPage();
+ }
+ _onPrevPage() {
+ OrderFetcher.prevPage();
+ }
+ _onSearch({ detail: domain }) {
+ OrderFetcher.setSearchDomain(domain);
+ OrderFetcher.setPage(1);
+ OrderFetcher.fetch();
+ }
+ _onClickOrder({ detail: clickedOrder }) {
+ if (!clickedOrder || clickedOrder.locked) {
+ this.orderManagementContext.selectedOrder = clickedOrder;
+ } else {
+ this._setOrder(clickedOrder);
+ }
+ }
+ /**
+ * @param {models.Order} order
+ */
+ _setOrder(order) {
+ this.env.pos.set_order(order);
+ if (order === this.env.pos.get_order()) {
+ this.close();
+ }
+ }
+ }
+ OrderManagementScreen.template = 'OrderManagementScreen';
+ OrderManagementScreen.hideOrderSelector = true;
+
+ Registries.Component.add(OrderManagementScreen);
+
+ return OrderManagementScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js
new file mode 100644
index 00000000..959ea5a1
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js
@@ -0,0 +1,42 @@
+odoo.define('point_of_sale.OrderRow', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * @props {models.Order} order
+ * @props columns
+ * @emits click-order
+ */
+ class OrderRow extends PosComponent {
+ get order() {
+ return this.props.order;
+ }
+ get highlighted() {
+ const highlightedOrder = this.props.highlightedOrder;
+ return !highlightedOrder ? false : highlightedOrder.backendId === this.props.order.backendId;
+ }
+
+ // Column getters //
+
+ get name() {
+ return this.order.get_name();
+ }
+ get date() {
+ return moment(this.order.validation_date).format('YYYY-MM-DD hh:mm A');
+ }
+ get customer() {
+ const customer = this.order.get('client');
+ return customer ? customer.name : null;
+ }
+ get total() {
+ return this.env.pos.format_currency(this.order.get_total_with_tax());
+ }
+ }
+ OrderRow.template = 'OrderRow';
+
+ Registries.Component.add(OrderRow);
+
+ return OrderRow;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js
new file mode 100644
index 00000000..35f6ec5d
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js
@@ -0,0 +1,55 @@
+odoo.define('point_of_sale.OrderlineDetails', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { format } = require('web.field_utils');
+ const { round_precision: round_pr } = require('web.utils');
+
+ /**
+ * @props {pos.order.line} line
+ */
+ class OrderlineDetails extends PosComponent {
+ get line() {
+ const line = this.props.line;
+ const formatQty = (line) => {
+ const quantity = line.get_quantity();
+ const unit = line.get_unit();
+ const decimals = this.env.pos.dp['Product Unit of Measure'];
+ const rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
+ const roundedQuantity = round_pr(quantity, rounding);
+ return format.float(roundedQuantity, { digits: [69, decimals] });
+ };
+ return {
+ productName: line.get_full_product_name(),
+ totalPrice: line.get_price_with_tax(),
+ quantity: formatQty(line),
+ unit: line.get_unit().name,
+ unitPrice: line.get_unit_price(),
+ };
+ }
+ get productName() {
+ return this.line.productName;
+ }
+ get totalPrice() {
+ return this.env.pos.format_currency(this.line.totalPrice);
+ }
+ get quantity() {
+ return this.line.quantity;
+ }
+ get unitPrice() {
+ return this.line.unitPrice;
+ }
+ get unit() {
+ return this.line.unit;
+ }
+ get pricePerUnit() {
+ return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`;
+ }
+ }
+ OrderlineDetails.template = 'OrderlineDetails';
+
+ Registries.Component.add(OrderlineDetails);
+
+ return OrderlineDetails;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
new file mode 100644
index 00000000..7fcc514d
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
@@ -0,0 +1,32 @@
+odoo.define('point_of_sale.ReprintReceiptScreen', function (require) {
+ 'use strict';
+
+ const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen');
+ const Registries = require('point_of_sale.Registries');
+
+ const ReprintReceiptScreen = (AbstractReceiptScreen) => {
+ class ReprintReceiptScreen extends AbstractReceiptScreen {
+ mounted() {
+ this.printReceipt();
+ }
+ confirm() {
+ this.showScreen('OrderManagementScreen');
+ }
+ async printReceipt() {
+ if(this.env.pos.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
+ let result = await this._printReceipt();
+ if(result)
+ this.showScreen('OrderManagementScreen');
+ }
+ }
+ async tryReprint() {
+ await this._printReceipt();
+ }
+ }
+ ReprintReceiptScreen.template = 'ReprintReceiptScreen';
+ return ReprintReceiptScreen;
+ };
+ Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen);
+
+ return ReprintReceiptScreen;
+});