diff --git a/addons/l10n_ae_pos/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/l10n_ae_pos/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml index 08656a8b220..c9b25b4b5f2 100644 --- a/addons/l10n_ae_pos/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml +++ b/addons/l10n_ae_pos/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -3,7 +3,7 @@
- VAT + VAT Total Taxes
diff --git a/addons/l10n_co_pos/static/src/js/PaymentScreen.js b/addons/l10n_co_pos/static/src/js/PaymentScreen.js index 0d177cc47a4..37698fe1e6e 100644 --- a/addons/l10n_co_pos/static/src/js/PaymentScreen.js +++ b/addons/l10n_co_pos/static/src/js/PaymentScreen.js @@ -7,7 +7,7 @@ patch(PaymentScreen.prototype, "l10n_co_pos.PaymentScreen", { async _postPushOrderResolve(order, order_server_ids) { const _super = this._super; try { - if (this.pos.globalState.is_colombian_country()) { + if (this.pos.is_colombian_country()) { const result = await this.orm.searchRead( "pos.order", [["id", "in", order_server_ids]], diff --git a/addons/l10n_co_pos/static/src/js/pos.js b/addons/l10n_co_pos/static/src/js/pos.js index 2105bab8c0c..baff30f355b 100644 --- a/addons/l10n_co_pos/static/src/js/pos.js +++ b/addons/l10n_co_pos/static/src/js/pos.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import { PosGlobalState, Order } from "@point_of_sale/app/store/models"; +import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { Order } from "@point_of_sale/app/store/models"; import { patch } from "@web/core/utils/patch"; -patch(PosGlobalState.prototype, "l10n_co_pos.PosGlobalState", { +patch(PosStore.prototype, "l10n_co_pos.PosStore", { is_colombian_country() { return this.company.country.code === "CO"; }, diff --git a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js index 6e812191e5d..0ed19433cbd 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Chrome.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Chrome.js @@ -9,13 +9,12 @@ patch(Chrome.prototype, "l10n_fr_pos_cert.Chrome", { setup() { this._super(...arguments); onMounted(async () => { - const { globalState } = this.pos; - if (globalState.is_french_country() && globalState.pos_session.start_at) { + if (this.pos.is_french_country() && this.pos.pos_session.start_at) { const now = Date.now(); - const limitDate = new Date(globalState.pos_session.start_at); + const limitDate = new Date(this.pos.pos_session.start_at); limitDate.setDate(limitDate.getDate() + 1); if (limitDate.getTime() < now) { - const info = await globalState.getClosePosInfo(); + const info = await this.pos.getClosePosInfo(); this.popup.add(ClosePosPopup, { info }); } } diff --git a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js index 5f73feb7d72..45f73d0b438 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js +++ b/addons/l10n_fr_pos_cert/static/src/js/PaymentScreen.js @@ -7,7 +7,7 @@ patch(PaymentScreen.prototype, "l10n_fr_pos_cert.PaymentScreen", { async _postPushOrderResolve(order, order_server_ids) { const _super = this._super; try { - if (this.pos.globalState.is_french_country()) { + if (this.pos.is_french_country()) { const result = await this.orm.searchRead( "pos.order", [["id", "in", order_server_ids]], diff --git a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js index 0ddc1ddc8fa..c3c4615fcec 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js +++ b/addons/l10n_fr_pos_cert/static/src/js/Popups/ClosePosPopup.js @@ -6,9 +6,9 @@ import { patch } from "@web/core/utils/patch"; patch(ClosePosPopup.prototype, "l10n_fr_pos_cert.ClosePosPopup", { sessionIsOutdated() { let isOutdated = false; - if (this.pos.globalState.is_french_country() && this.pos.globalState.pos_session.start_at) { + if (this.pos.is_french_country() && this.pos.pos_session.start_at) { const now = Date.now(); - const limitDate = new Date(this.pos.globalState.pos_session.start_at); + const limitDate = new Date(this.pos.pos_session.start_at); limitDate.setDate(limitDate.getDate() + 1); isOutdated = limitDate < now; } diff --git a/addons/l10n_fr_pos_cert/static/src/js/pos.js b/addons/l10n_fr_pos_cert/static/src/js/pos.js index 66fb77ee64f..06e0490f11b 100644 --- a/addons/l10n_fr_pos_cert/static/src/js/pos.js +++ b/addons/l10n_fr_pos_cert/static/src/js/pos.js @@ -1,12 +1,13 @@ /** @odoo-module */ -import { PosGlobalState, Order, Orderline } from "@point_of_sale/app/store/models"; +import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { Order, Orderline } from "@point_of_sale/app/store/models"; import { _t } from "@web/core/l10n/translation"; import { patch } from "@web/core/utils/patch"; import { sprintf } from "@web/core/utils/strings"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; -patch(PosGlobalState.prototype, "l10n_fr_pos_cert.PosGlobalState", { +patch(PosStore.prototype, "l10n_fr_pos_cert.PosStore", { is_french_country() { var french_countries = ["FR", "MF", "MQ", "NC", "PF", "RE", "GF", "GP", "TF"]; if (!this.company.country) { diff --git a/addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml b/addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml index d51460ee103..dffef9c4555 100644 --- a/addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml +++ b/addons/l10n_fr_pos_cert/static/src/xml/Orderline.xml @@ -3,7 +3,7 @@ - +
  • Old unit price: diff --git a/addons/l10n_gcc_pos/static/src/xml/OrderReceipt.xml b/addons/l10n_gcc_pos/static/src/xml/OrderReceipt.xml index ae30b96d2eb..91c264bf359 100644 --- a/addons/l10n_gcc_pos/static/src/xml/OrderReceipt.xml +++ b/addons/l10n_gcc_pos/static/src/xml/OrderReceipt.xml @@ -114,7 +114,7 @@
    - + x diff --git a/addons/l10n_in_pos/static/src/xml/pos_receipt.xml b/addons/l10n_in_pos/static/src/xml/pos_receipt.xml index 84856c69c35..0eff3d0055e 100644 --- a/addons/l10n_in_pos/static/src/xml/pos_receipt.xml +++ b/addons/l10n_in_pos/static/src/xml/pos_receipt.xml @@ -2,7 +2,7 @@ - +
    @@ -19,7 +19,7 @@ - +
    HSN Code: diff --git a/addons/point_of_sale/static/src/app/bus/pos_bus_service.js b/addons/point_of_sale/static/src/app/bus/pos_bus_service.js index 651fcf0f22e..0f193dea67a 100644 --- a/addons/point_of_sale/static/src/app/bus/pos_bus_service.js +++ b/addons/point_of_sale/static/src/app/bus/pos_bus_service.js @@ -14,7 +14,7 @@ export class PosBus { this.orm = orm; // initialize bus_service and listen on pos_config-`id` channel - bus_service.addChannel(`pos_config-${pos.globalState.config.id}`); + bus_service.addChannel(`pos_config-${pos.config.id}`); bus_service.addEventListener("notification", ({ detail }) => { for (const message of detail) { this.dispatch(message); diff --git a/addons/point_of_sale/static/src/app/customer_display/customer_display_service.js b/addons/point_of_sale/static/src/app/customer_display/customer_display_service.js index 0f95e813c09..9f3288f6fc9 100644 --- a/addons/point_of_sale/static/src/app/customer_display/customer_display_service.js +++ b/addons/point_of_sale/static/src/app/customer_display/customer_display_service.js @@ -11,8 +11,8 @@ export class LocalDisplay extends Reactive { super(); this.setup(...arguments); } - setup(globalState) { - this.globalState = globalState; + setup(pos) { + this.pos = pos; } async connect() { if (this.popupWindow && !this.popupWindow.closed) { @@ -59,7 +59,7 @@ export class LocalDisplay extends Reactive { // animations. const { body: displayBody, head: displayHead } = this.popupWindow.document; const container = document.createElement("div"); - container.innerHTML = await this.globalState.customerDisplayHTML(closeUI); + container.innerHTML = await this.pos.customerDisplayHTML(closeUI); if (!container.innerHTML || container.innerHTML === "undefined") { displayBody.textContent = ""; @@ -95,13 +95,13 @@ export class RemoteDisplay extends Reactive { super(); this.setup(...arguments); } - setup(globalState, { hardware_proxy }) { + setup(pos, { hardware_proxy }) { this.hardwareProxy = hardware_proxy; - this.globalState = globalState; + this.pos = pos; this.updateStatus(); } async connect() { - const html = await this.globalState.customerDisplayHTML(); + const html = await this.pos.customerDisplayHTML(); try { const { status } = await this.hardwareProxy.message("take_control", { html }); this.status = status === "success" ? "success" : "warning"; @@ -111,7 +111,7 @@ export class RemoteDisplay extends Reactive { } } async update({ closeUI = false } = {}) { - const html = await this.globalState.customerDisplayHTML(closeUI); + const html = await this.pos.customerDisplayHTML(closeUI); if (this.isUpdatingStatus && this.hardwareProxy.connectionInfo.status === "connected") { return this.hardwareProxy.message("customer_facing_display", { html }); } @@ -147,19 +147,19 @@ export const customerDisplayService = { return ["pos", ...RemoteDisplay.serviceDependencies]; }, start(env, deps) { - const { globalState } = deps.pos; + const pos = deps.pos; const { iface_customer_facing_display: enabled, iface_customer_facing_display_via_proxy: proxy, - } = globalState.config; + } = pos.config; if (!enabled) { return; } const display = proxy - ? new RemoteDisplay(globalState, pick(deps, ...RemoteDisplay.serviceDependencies)) - : new LocalDisplay(globalState); + ? new RemoteDisplay(pos, pick(deps, ...RemoteDisplay.serviceDependencies)) + : new LocalDisplay(pos); // Register an effect to update the display automatically when anything it renders changes effect( batched((display) => display.update()), diff --git a/addons/point_of_sale/static/src/app/debug/debug_widget.js b/addons/point_of_sale/static/src/app/debug/debug_widget.js index 712b2fe4af5..71198540364 100644 --- a/addons/point_of_sale/static/src/app/debug/debug_widget.js +++ b/addons/point_of_sale/static/src/app/debug/debug_widget.js @@ -97,8 +97,8 @@ export class DebugWidget extends Component { ), }); if (confirmed) { - this.pos.globalState.db.remove_all_orders(); - this.pos.globalState.set_synch("connected", 0); + this.pos.db.remove_all_orders(); + this.pos.set_synch("connected", 0); } } async deleteUnpaidOrders() { @@ -109,7 +109,7 @@ export class DebugWidget extends Component { ), }); if (confirmed) { - this.pos.globalState.db.remove_all_unpaid_orders(); + this.pos.db.remove_all_unpaid_orders(); window.location = "/"; } } @@ -123,7 +123,7 @@ export class DebugWidget extends Component { // The implementation can be better. preparePaidOrders() { try { - this.paidOrdersBlob = this._createBlob(this.pos.globalState.export_paid_orders()); + this.paidOrdersBlob = this._createBlob(this.pos.export_paid_orders()); this.state.isPaidOrdersReady = true; } catch (error) { console.warn(error); @@ -139,7 +139,7 @@ export class DebugWidget extends Component { // FIXME POSREF why is this two steps? prepareUnpaidOrders() { try { - this.unpaidOrdersBlob = this._createBlob(this.pos.globalState.export_unpaid_orders()); + this.unpaidOrdersBlob = this._createBlob(this.pos.export_unpaid_orders()); this.state.isUnpaidOrdersReady = true; } catch (error) { console.warn(error); @@ -155,7 +155,7 @@ export class DebugWidget extends Component { async importOrders(event) { const file = event.target.files[0]; if (file) { - const report = this.pos.globalState.import_orders(await file.text()); + const report = this.pos.import_orders(await file.text()); await this.popup.add(OrderImportPopup, { report }); } } diff --git a/addons/point_of_sale/static/src/app/errors/popups/error_traceback_popup.js b/addons/point_of_sale/static/src/app/errors/popups/error_traceback_popup.js index 1b0395abd08..206c51200f3 100644 --- a/addons/point_of_sale/static/src/app/errors/popups/error_traceback_popup.js +++ b/addons/point_of_sale/static/src/app/errors/popups/error_traceback_popup.js @@ -30,7 +30,7 @@ export class ErrorTracebackPopup extends ErrorPopup { return `${this.env._t("error")} ${moment().format("YYYY-MM-DD-HH-mm-ss")}.txt`; } emailTraceback() { - const address = this.pos.globalState.company.email; + const address = this.pos.company.email; const subject = this.env._t("IMPORTANT: Bug Report From Odoo Point Of Sale"); window.open( "mailto:" + diff --git a/addons/point_of_sale/static/src/app/errors/popups/offline_error_popup.js b/addons/point_of_sale/static/src/app/errors/popups/offline_error_popup.js index 9ddede73dbe..f9e642e2ac7 100644 --- a/addons/point_of_sale/static/src/app/errors/popups/offline_error_popup.js +++ b/addons/point_of_sale/static/src/app/errors/popups/offline_error_popup.js @@ -22,15 +22,15 @@ export class OfflineErrorPopup extends ErrorPopup { super.setup(...arguments); this.pos = usePos(); - if (!this.pos.globalState.showOfflineWarning) { + if (!this.pos.showOfflineWarning) { this.cancel(); } else { - this.pos.globalState.set_synch("disconnected"); + this.pos.set_synch("disconnected"); } } confirm() { - this.pos.globalState.showOfflineWarning = false; + this.pos.showOfflineWarning = false; this.cancel(); } } diff --git a/addons/point_of_sale/static/src/app/navbar/back_button/back_button.js b/addons/point_of_sale/static/src/app/navbar/back_button/back_button.js index 9667d9cf148..ea9abf06f9c 100644 --- a/addons/point_of_sale/static/src/app/navbar/back_button/back_button.js +++ b/addons/point_of_sale/static/src/app/navbar/back_button/back_button.js @@ -12,7 +12,7 @@ export class BackButton extends Component { this.ui = useState(useService("ui")); } async backToFloorScreen() { - this.pos.globalState.mobile_pane = "right"; + this.pos.mobile_pane = "right"; this.pos.showScreen("ProductScreen"); } } diff --git a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js index eb5ea6ab0b5..8fc4bd6240e 100644 --- a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js +++ b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.js @@ -59,13 +59,13 @@ export class CashMovePopup extends AbstractAwaitablePopup { const extras = { formattedAmount, translatedType }; const reason = this.state.reason.trim(); await this.orm.call("pos.session", "try_cash_in_out", [ - [this.pos.globalState.pos_session.id], + [this.pos.pos_session.id], type, amount, reason, extras, ]); - await this.pos.globalState.logEmployeeMessage( + await this.pos.logEmployeeMessage( `${_t("Cash")} ${translatedType} - ${_t("Amount")}: ${formattedAmount}`, "CASH_DRAWER_ACTION" ); @@ -78,8 +78,8 @@ export class CashMovePopup extends AbstractAwaitablePopup { amount, translatedType, formattedAmount, - cashier: this.pos.globalState.get_cashier(), - company: this.pos.globalState.company, + cashier: this.pos.get_cashier(), + company: this.pos.company, }, }); const printResult = await this.hardwareProxy.printer.printReceipt(renderedReceipt); @@ -116,10 +116,10 @@ export class CashMovePopup extends AbstractAwaitablePopup { async cancel() { super.cancel(); if ( - this.pos.globalState.config.iface_cashdrawer && - this.pos.globalState.hardwareProxy.connectionInfo.status === "connected" + this.pos.config.iface_cashdrawer && + this.pos.hardwareProxy.connectionInfo.status === "connected" ) { - this.pos.globalState.logEmployeeMessage(_t("Cash in / out"), "ACTION_CANCELLED"); + this.pos.logEmployeeMessage(_t("Cash in / out"), "ACTION_CANCELLED"); } } } diff --git a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.xml b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.xml index 01a95652291..59f779884a6 100644 --- a/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.xml +++ b/addons/point_of_sale/static/src/app/navbar/cash_move_popup/cash_move_popup.xml @@ -14,11 +14,11 @@
    - - + + - +
    diff --git a/addons/point_of_sale/static/src/app/navbar/cashier_name/cashier_name.js b/addons/point_of_sale/static/src/app/navbar/cashier_name/cashier_name.js index 4ba4b4d2f4c..de2c779e8f7 100644 --- a/addons/point_of_sale/static/src/app/navbar/cashier_name/cashier_name.js +++ b/addons/point_of_sale/static/src/app/navbar/cashier_name/cashier_name.js @@ -13,11 +13,11 @@ export class CashierName extends Component { this.ui = useState(useService("ui")); } get username() { - const { name } = this.pos.globalState.get_cashier(); + const { name } = this.pos.get_cashier(); return name ? name : ""; } get avatar() { - const user_id = this.pos.globalState.get_cashier_user_id(); + const user_id = this.pos.get_cashier_user_id(); const id = user_id ? user_id : -1; return `/web/image/res.users/${id}/avatar_128`; } diff --git a/addons/point_of_sale/static/src/app/navbar/closing_popup/closing_popup.js b/addons/point_of_sale/static/src/app/navbar/closing_popup/closing_popup.js index 541e8d2cd5c..9c5736b3afc 100644 --- a/addons/point_of_sale/static/src/app/navbar/closing_popup/closing_popup.js +++ b/addons/point_of_sale/static/src/app/navbar/closing_popup/closing_popup.js @@ -27,7 +27,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { this.hardwareProxy = useService("hardware_proxy"); this.customerDisplay = useService("customer_display"); this.manualInputCashCount = false; - this.cashControl = this.pos.globalState.config.cash_control; + this.cashControl = this.pos.config.cash_control; this.closeSessionClicked = false; this.moneyDetails = null; Object.assign(this, this.props.info); @@ -97,7 +97,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { } async downloadSalesReport() { return this.report.download("point_of_sale.sale_details_report", [ - this.pos.globalState.pos_session.id, + this.pos.pos_session.id, ]); } handleInputChange(paymentId) { @@ -142,7 +142,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { const response = await this.orm.call( "pos.session", "post_closing_cash_details", - [this.pos.globalState.pos_session.id], + [this.pos.pos_session.id], { counted_cash: this.state.payments[this.defaultCashDetails.id].counted, } @@ -155,7 +155,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { try { await this.orm.call("pos.session", "update_closing_control_state_session", [ - this.pos.globalState.pos_session.id, + this.pos.pos_session.id, this.state.notes, ]); } catch (error) { @@ -176,7 +176,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup { .filter((pm) => pm.type == "bank") .map((pm) => [pm.id, this.state.payments[pm.id].difference]); const response = await this.orm.call("pos.session", "close_session_from_ui", [ - this.pos.globalState.pos_session.id, + this.pos.pos_session.id, bankPaymentMethodDiffPairs, ]); if (!response.successful) { diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.js b/addons/point_of_sale/static/src/app/navbar/navbar.js index 786f8c850ee..d8480fb0cdf 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.js +++ b/addons/point_of_sale/static/src/app/navbar/navbar.js @@ -45,10 +45,10 @@ export class Navbar extends Component { } get customerFacingDisplayButtonIsShown() { - return this.pos.globalState.config.iface_customer_facing_display; + return this.pos.config.iface_customer_facing_display; } get showCashMoveButton() { - return Boolean(this.pos.globalState?.config?.cash_control); + return Boolean(this.pos?.config?.cash_control); } onCashMoveButtonClick() { this.hardwareProxy.openCashbox(_t("Cash in / out")); @@ -59,15 +59,14 @@ export class Navbar extends Component { this.pos.closeScreen(); } else { if (this._shouldLoadOrders()) { - const { globalState } = this.pos; try { - globalState.setLoadingOrderState(true); - const message = await globalState._syncAllOrdersFromServer(); + this.pos.setLoadingOrderState(true); + const message = await this.pos._syncAllOrdersFromServer(); if (message) { this.notification.add(message, 5000); } } finally { - globalState.setLoadingOrderState(false); + this.pos.setLoadingOrderState(false); this.pos.showScreen("TicketScreen"); } } else { @@ -77,7 +76,7 @@ export class Navbar extends Component { } _shouldLoadOrders() { - return this.pos.globalState.config.trusted_config_ids.length > 0; + return this.pos.config.trusted_config_ids.length > 0; } get isTicketScreenShown() { @@ -85,7 +84,7 @@ export class Navbar extends Component { } get orderCount() { - return this.pos.globalState.get_order_list().length; + return this.pos.get_order_list().length; } isBurgerMenuClosed() { @@ -101,7 +100,7 @@ export class Navbar extends Component { } async closeSession() { - const info = await this.pos.globalState.getClosePosInfo(); + const info = await this.pos.getClosePosInfo(); this.popup.add(ClosePosPopup, { info, keepBehind: true }); } diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.xml b/addons/point_of_sale/static/src/app/navbar/navbar.xml index 3aad708165b..192f3f8b863 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.xml +++ b/addons/point_of_sale/static/src/app/navbar/navbar.xml @@ -12,7 +12,7 @@
    - +
    diff --git a/addons/point_of_sale/static/src/app/navbar/proxy_status/proxy_status.js b/addons/point_of_sale/static/src/app/navbar/proxy_status/proxy_status.js index 102827dc5ed..daccb1df210 100644 --- a/addons/point_of_sale/static/src/app/navbar/proxy_status/proxy_status.js +++ b/addons/point_of_sale/static/src/app/navbar/proxy_status/proxy_status.js @@ -24,7 +24,7 @@ export class ProxyStatus extends Component { iface_print_via_proxy, iface_cashdrawer, iface_electronic_scale, - } = this.pos.globalState.config; + } = this.pos.config; const devices = [ { name: _t("Scanner"), diff --git a/addons/point_of_sale/static/src/app/navbar/sale_details_button/sale_details_button.js b/addons/point_of_sale/static/src/app/navbar/sale_details_button/sale_details_button.js index d4ce76fa7ee..0ac544ee354 100644 --- a/addons/point_of_sale/static/src/app/navbar/sale_details_button/sale_details_button.js +++ b/addons/point_of_sale/static/src/app/navbar/sale_details_button/sale_details_button.js @@ -18,20 +18,19 @@ export class SaleDetailsButton extends Component { } async onClick() { - const { globalState } = this.pos; // IMPROVEMENT: Perhaps put this logic in a parent component // so that for unit testing, we can check if this simple // component correctly triggers an event. const saleDetails = await this.orm.call( "report.point_of_sale.report_saledetails", "get_sale_details", - [false, false, false, [globalState.pos_session.id]] + [false, false, false, [this.pos.pos_session.id]] ); const report = renderToElement( "point_of_sale.SaleDetailsReport", Object.assign({}, saleDetails, { date: new Date().toLocaleString(), - pos: globalState, + pos: this.pos, formatCurrency: this.env.utils.formatCurrency, }) ); diff --git a/addons/point_of_sale/static/src/app/navbar/sync_notification/sync_notification.js b/addons/point_of_sale/static/src/app/navbar/sync_notification/sync_notification.js index 8284c9c7ddd..4c5ef365114 100644 --- a/addons/point_of_sale/static/src/app/navbar/sync_notification/sync_notification.js +++ b/addons/point_of_sale/static/src/app/navbar/sync_notification/sync_notification.js @@ -10,12 +10,12 @@ export class SyncNotification extends Component { this.pos = usePos(); } get sync() { - return this.pos.globalState.synch; + return this.pos.synch; } onClick() { - if (this.pos.globalState.synch.status !== "connected") { - this.pos.globalState.showOfflineWarning = true; + if (this.pos.synch.status !== "connected") { + this.pos.showOfflineWarning = true; } - this.pos.globalState.push_orders({ show_error: true }); + this.pos.push_orders({ show_error: true }); } } diff --git a/addons/point_of_sale/static/src/app/payment/payment_interface.js b/addons/point_of_sale/static/src/app/payment/payment_interface.js index 28910fa25f9..1af0a5c8d1a 100644 --- a/addons/point_of_sale/static/src/app/payment/payment_interface.js +++ b/addons/point_of_sale/static/src/app/payment/payment_interface.js @@ -8,7 +8,7 @@ * * To connect the interface to the right payment methods register it: * - * import { register_payment_method } models from "@point_of_sale/app/store/models"; + * import { register_payment_method } models from "@point_of_sale/app/store/pos_store"; * register_payment_method('my_payment', MyPayment); * * my_payment is the technical name of the added selection in diff --git a/addons/point_of_sale/static/src/app/pos_app.js b/addons/point_of_sale/static/src/app/pos_app.js index b96e65bd004..d70826bfe2f 100644 --- a/addons/point_of_sale/static/src/app/pos_app.js +++ b/addons/point_of_sale/static/src/app/pos_app.js @@ -19,7 +19,7 @@ export class Chrome extends Component { this.pos = usePos(); this.popup = useService("popup"); - const reactivePos = reactive(this.pos.globalState); + const reactivePos = reactive(this.pos); // TODO: Should we continue on exposing posmodel as global variable? window.posmodel = reactivePos; @@ -33,6 +33,11 @@ export class Chrome extends Component { onMounted(this.props.disableLoader); } + // GETTERS // + + get showCashMoveButton() { + return Boolean(this.pos?.config?.cash_control); + } /** * Unmounts the tempScreen on error and dispatches the error in a separate * stack so that it can be handled by the error service and display an error diff --git a/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.js b/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.js index b977db13a90..a47f240fb4c 100644 --- a/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.js +++ b/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.js @@ -29,7 +29,7 @@ export class PartnerDetailsEdit extends Component { if (partner.property_product_pricelist) { return partner.property_product_pricelist[0]; } - return this.pos.globalState.default_pricelist?.id ?? false; + return this.pos.default_pricelist?.id ?? false; } get partnerImageUrl() { diff --git a/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.xml b/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.xml index 4f89e8438d0..676bf472b35 100644 --- a/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.xml +++ b/addons/point_of_sale/static/src/app/screens/partner_list/partner_editor/partner_editor.xml @@ -40,7 +40,7 @@ - +
    -
    +
    Pricelist - + +
    diff --git a/addons/point_of_sale/static/src/app/screens/product_screen/product_list/product_list.js b/addons/point_of_sale/static/src/app/screens/product_screen/product_list/product_list.js index 9371e9a1351..d6b4992b64d 100644 --- a/addons/point_of_sale/static/src/app/screens/product_screen/product_list/product_list.js +++ b/addons/point_of_sale/static/src/app/screens/product_screen/product_list/product_list.js @@ -31,16 +31,16 @@ export class ProductsWidget extends Component { this.orm = useService("orm"); } get hasProducts() { - return Object.keys(this.pos.globalState.db.product_by_id).length > 0; + return Object.keys(this.pos.db.product_by_id).length > 0; } get selectedCategoryId() { - return this.pos.globalState.selectedCategoryId; + return this.pos.selectedCategoryId; } get searchWord() { - return this.pos.globalState.searchProductWord.trim(); + return this.pos.searchProductWord.trim(); } get productsToDisplay() { - const { db } = this.pos.globalState; + const { db } = this.pos; let list = []; if (this.searchWord !== "") { list = db.search_product_in_category(this.selectedCategoryId, this.searchWord); @@ -52,13 +52,13 @@ export class ProductsWidget extends Component { }); } get subcategories() { - const { db } = this.pos.globalState; + const { db } = this.pos; return db .get_category_childs_ids(this.selectedCategoryId) .map((id) => db.get_category_by_id(id)); } get breadcrumbs() { - const { db } = this.pos.globalState; + const { db } = this.pos; if (this.selectedCategoryId === db.root_category_id) { return []; } @@ -68,25 +68,25 @@ export class ProductsWidget extends Component { ].map((id) => db.get_category_by_id(id)); } get hasNoCategories() { - return this.pos.globalState.db.get_category_childs_ids(0).length === 0; + return this.pos.db.get_category_childs_ids(0).length === 0; } get shouldShowButton() { return this.productsToDisplay.length === 0 && this.searchWord; } switchCategory(categoryId) { - this.pos.globalState.setSelectedCategoryId(categoryId); + this.pos.setSelectedCategoryId(categoryId); } updateSearch(searchWord) { - this.pos.globalState.searchProductWord = searchWord; + this.pos.searchProductWord = searchWord; } clearSearch() { - this.pos.globalState.searchProductWord = ""; + this.pos.searchProductWord = ""; } updateProductList(event) { this.switchCategory(0); } async onPressEnterKey() { - const { searchProductWord } = this.pos.globalState; + const { searchProductWord } = this.pos; if (!searchProductWord) { return; } @@ -117,7 +117,7 @@ export class ProductsWidget extends Component { } } async loadProductFromDB() { - const { searchProductWord } = this.pos.globalState; + const { searchProductWord } = this.pos; if (!searchProductWord) { return; } @@ -144,7 +144,7 @@ export class ProductsWidget extends Component { } ); if (ProductIds.length) { - await this.pos.globalState._addProducts(ProductIds, false); + await this.pos._addProducts(ProductIds, false); } this.updateProductList(); return ProductIds; @@ -169,10 +169,10 @@ export class ProductsWidget extends Component { const { products, categories } = await this.orm.call( "pos.session", "load_product_frontend", - [this.pos.globalState.pos_session.id] + [this.pos.pos_session.id] ); - this.pos.globalState.db.add_categories(categories); - this.pos.globalState._loadProductProduct(products); + this.pos.db.add_categories(categories); + this.pos._loadProductProduct(products); } createNewProducts() { diff --git a/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.js b/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.js index ac8b67c298a..fd70c933378 100644 --- a/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.js +++ b/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.js @@ -72,7 +72,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { return this.currentOrder ? this.currentOrder.get_partner() : null; } get currentOrder() { - return this.pos.globalState.get_order(); + return this.pos.get_order(); } get total() { return this.env.utils.formatCurrency(this.currentOrder?.get_total_with_tax() ?? 0); @@ -81,16 +81,15 @@ export class ProductScreen extends ControlButtonsMixin(Component) { return this.currentOrder.orderlines?.reduce((items, line) => items + line.quantity, 0) ?? 0; } async updateSelectedOrderline({ buffer, key }) { - const { globalState } = this.pos; - if (globalState.numpadMode === "quantity" && globalState.disallowLineQuantityChange()) { - const order = globalState.get_order(); + if (this.pos.numpadMode === "quantity" && this.pos.disallowLineQuantityChange()) { + const order = this.pos.get_order(); if (!order.orderlines.length) { return; } const selectedLine = order.get_selected_orderline(); const orderlines = order.orderlines; const lastId = orderlines.length !== 0 && orderlines.at(orderlines.length - 1).cid; - const currentQuantity = globalState.get_order().get_selected_orderline().get_quantity(); + const currentQuantity = this.pos.get_order().get_selected_orderline().get_quantity(); if (selectedLine.noDecrease) { this.popup.add(ErrorPopup, { @@ -112,12 +111,12 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this._setValue(val); if (val == "remove") { this.numberBuffer.reset(); - globalState.numpadMode = "quantity"; + this.pos.numpadMode = "quantity"; } } } _setValue(val) { - const { numpadMode } = this.pos.globalState; + const { numpadMode } = this.pos; if (this.currentOrder.get_selected_orderline()) { if (numpadMode === "quantity") { const result = this.currentOrder.get_selected_orderline().set_quantity(val); @@ -134,8 +133,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { } } async _barcodeProductAction(code) { - const { globalState } = this.pos; - let product = globalState.db.get_product_by_barcode(code.base_code); + let product = this.pos.db.get_product_by_barcode(code.base_code); if (!product) { // find the barcode in the backend let foundProductIds = []; @@ -143,9 +141,9 @@ export class ProductScreen extends ControlButtonsMixin(Component) { ["barcode", "=", code.base_code], ]); if (foundProductIds.length) { - await globalState._addProducts(foundProductIds); + await this.pos._addProducts(foundProductIds); // assume that the result is unique. - product = globalState.db.get_product_by_id(foundProductIds[0]); + product = this.pos.db.get_product_by_id(foundProductIds[0]); } else { return this.popup.add(ErrorBarcodePopup, { code: code.base_code }); } @@ -180,7 +178,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this.numberBuffer.reset(); } _barcodePartnerAction(code) { - const partner = this.pos.globalState.db.get_partner_by_barcode(code.code); + const partner = this.pos.db.get_partner_by_barcode(code.code); if (partner) { if (this.currentOrder.get_partner() !== partner) { this.currentOrder.set_partner(partner); @@ -209,7 +207,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { }); const newQuantity = inputNumber && inputNumber !== "" ? parseFloat(inputNumber) : null; if (confirmed && newQuantity !== null) { - const order = this.pos.globalState.get_order(); + const order = this.pos.get_order(); const selectedLine = order.get_selected_orderline(); const currentQuantity = selectedLine.get_quantity(); if (newQuantity >= currentQuantity) { @@ -268,23 +266,22 @@ export class ProductScreen extends ControlButtonsMixin(Component) { // FIXME POSREF this is dead code, check if we need the business logic that's left in here // If we do it should be in the model. async onClickPay() { - const { globalState } = this.pos; - if (globalState.get_order().server_id) { + if (this.pos.get_order().server_id) { try { const isPaid = await this.orm.call("pos.order", "is_already_paid", [ - globalState.get_order().server_id, + this.pos.get_order().server_id, ]); if (isPaid) { const searchDetails = { fieldName: "RECEIPT_NUMBER", - searchTerm: globalState.get_order().uid, + searchTerm: this.pos.get_order().uid, }; this.pos.showScreen("TicketScreen", { ui: { filter: "SYNCED", searchDetails }, }); this.notification.add(this.env._t("The order has been already paid."), 3000); - globalState.removeOrder(globalState.get_order(), false); - globalState.add_new_order(); + this.pos.removeOrder(this.pos.get_order(), false); + this.pos.add_new_order(); return; } } catch (error) { @@ -298,7 +295,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this.currentOrder.pay(); } switchPane() { - this.pos.globalState.switchPane(); + this.pos.switchPane(); } } diff --git a/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.xml b/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.xml index b9837170979..bdb0d863e55 100644 --- a/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.xml +++ b/addons/point_of_sale/static/src/app/screens/product_screen/product_screen.xml @@ -4,7 +4,7 @@
    -
    +
    @@ -22,11 +22,11 @@ onClickMore.bind="displayAllControlPopup" /> + activeMode="pos.numpadMode" />
    -
    +
    diff --git a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt.xml b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt.xml index 7cb2da0890a..b55d6a08950 100644 --- a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt.xml +++ b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt/receipt.xml @@ -197,7 +197,7 @@
    - + @@ -211,7 +211,7 @@
    - + x diff --git a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js index d03ece626ac..7ef1328c4df 100644 --- a/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js +++ b/addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js @@ -23,7 +23,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { this.orderReceipt = useRef("order-receipt"); this.buttonMailReceipt = useRef("order-mail-receipt-button"); this.buttonPrintReceipt = useRef("order-print-receipt-button"); - this.currentOrder = this.pos.globalState.get_order(); + this.currentOrder = this.pos.get_order(); const partner = this.currentOrder.get_partner(); this.orderUiState = this.currentOrder.uiState.ReceiptScreen; this.orderUiState.inputEmail = @@ -49,14 +49,14 @@ export class ReceiptScreen extends AbstractReceiptScreen { onWillStart(async () => { // When the order is paid, if there is still a part of the order // to send in preparation it is automatically sent - if (this.pos.globalState.orderPreparationCategories.size) { + if (this.pos.orderPreparationCategories.size) { await this.pos.sendOrderInPreparation(this.currentOrder); } }); } _addNewOrder() { - this.pos.globalState.add_new_order(); + this.pos.add_new_order(); } onSendEmail() { if (this.buttonMailReceipt.el.classList.contains("fa-spin")) { @@ -96,7 +96,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { get orderAmountPlusTip() { const order = this.currentOrder; const orderTotalAmount = order.get_total_with_tax(); - const tip_product_id = this.pos.globalState.config.tip_product_id?.[0]; + const tip_product_id = this.pos.config.tip_product_id?.[0]; const tipLine = order .get_orderlines() .find((line) => tip_product_id && line.product.id === tip_product_id); @@ -137,24 +137,24 @@ export class ReceiptScreen extends AbstractReceiptScreen { } } orderDone() { - this.pos.globalState.removeOrder(this.currentOrder); + this.pos.removeOrder(this.currentOrder); this._addNewOrder(); const { name, props } = this.nextScreen; this.pos.showScreen(name, props); } resumeOrder() { - this.pos.globalState.removeOrder(this.currentOrder); - this.pos.globalState.selectNextOrder(); + this.pos.removeOrder(this.currentOrder); + this.pos.selectNextOrder(); const { name, props } = this.ticketScreen; this.pos.showScreen(name, props); } continueSplitting() { - this.pos.globalState.selectedOrder = this.currentOrder.originalSplittedOrder; - this.pos.globalState.removeOrder(this.currentOrder); + this.pos.selectedOrder = this.currentOrder.originalSplittedOrder; + this.pos.removeOrder(this.currentOrder); this.pos.showScreen("ProductScreen"); } isResumeVisible() { - return this.pos.globalState.get_order_list().length > 1; + return this.pos.get_order_list().length > 1; } async printReceipt() { this.buttonPrintReceipt.el.className = "fa fa-fw fa-spin fa-circle-o-notch"; @@ -170,7 +170,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { } } _shouldAutoPrint() { - return this.pos.globalState.config.iface_print_auto && !this.currentOrder._printed; + return this.pos.config.iface_print_auto && !this.currentOrder._printed; } _shouldCloseImmediately() { var invoiced_finalized = this.currentOrder.is_to_invoice() @@ -178,7 +178,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { : true; return ( this.hardwareProxy.printer && - this.pos.globalState.config.iface_print_skip_screen && + this.pos.config.iface_print_skip_screen && invoiced_finalized ); } @@ -193,7 +193,7 @@ export class ReceiptScreen extends AbstractReceiptScreen { email: this.orderUiState.inputEmail, name: partner ? partner.name : this.orderUiState.inputEmail, }; - const order_server_id = this.pos.globalState.validated_orders_name_server_id_map[orderName]; + const order_server_id = this.pos.validated_orders_name_server_id_map[orderName]; if (!order_server_id) { this.popup.add(OfflineErrorPopup, { title: this.env._t("Unsynced order"), diff --git a/addons/point_of_sale/static/src/app/screens/receipt_screen/reprint_receipt_screen.js b/addons/point_of_sale/static/src/app/screens/receipt_screen/reprint_receipt_screen.js index 8550f28a683..29a3f24ffda 100644 --- a/addons/point_of_sale/static/src/app/screens/receipt_screen/reprint_receipt_screen.js +++ b/addons/point_of_sale/static/src/app/screens/receipt_screen/reprint_receipt_screen.js @@ -21,7 +21,7 @@ export class ReprintReceiptScreen extends AbstractReceiptScreen { this.pos.showScreen("TicketScreen", { reuseSavedUIState: true }); } async printReceipt() { - const { iface_print_skip_screen } = this.pos.globalState.config; + const { iface_print_skip_screen } = this.pos.config; if (this.hardwareProxy.printer && iface_print_skip_screen) { const result = await this._printReceipt(); if (result) { diff --git a/addons/point_of_sale/static/src/app/screens/scale_screen/scale_screen.js b/addons/point_of_sale/static/src/app/screens/scale_screen/scale_screen.js index 113594cf7e6..d51edec5442 100644 --- a/addons/point_of_sale/static/src/app/screens/scale_screen/scale_screen.js +++ b/addons/point_of_sale/static/src/app/screens/scale_screen/scale_screen.js @@ -60,8 +60,8 @@ export class ScaleScreen extends Component { setTimeout(() => this._setWeight(), 500); } get _activePricelist() { - const current_order = this.pos.globalState.get_order(); - let current_pricelist = this.pos.globalState.default_pricelist; + const current_order = this.pos.get_order(); + let current_pricelist = this.pos.default_pricelist; if (current_order) { current_pricelist = current_order.pricelist; } @@ -76,7 +76,7 @@ export class ScaleScreen extends Component { if (!unit_id) { return defaultstr; } - const unit = this.pos.globalState.units_by_id[unit_id[0]]; + const unit = this.pos.units_by_id[unit_id[0]]; const weight = round_pr(this.state.weight || 0, unit.rounding); let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); weightstr += " " + unit.name; @@ -96,7 +96,7 @@ export class ScaleScreen extends Component { if (!this.props.product) { return ""; } - return this.pos.globalState.units_by_id[this.props.product.uom_id[0]].name; + return this.pos.units_by_id[this.props.product.uom_id[0]].name; } } diff --git a/addons/point_of_sale/static/src/app/screens/ticket_screen/order_details/orderline_details.js b/addons/point_of_sale/static/src/app/screens/ticket_screen/order_details/orderline_details.js index 00236d9e460..a8dd78207e2 100644 --- a/addons/point_of_sale/static/src/app/screens/ticket_screen/order_details/orderline_details.js +++ b/addons/point_of_sale/static/src/app/screens/ticket_screen/order_details/orderline_details.js @@ -20,7 +20,7 @@ export class OrderlineDetails extends Component { const formatQty = (line) => { const quantity = line.get_quantity(); const unit = line.get_unit(); - const decimals = this.pos.globalState.dp["Product Unit of Measure"]; + const decimals = this.pos.dp["Product Unit of Measure"]; const rounding = Math.max(unit.rounding, Math.pow(10, -decimals)); const roundedQuantity = round_pr(quantity, rounding); return formatFloat(roundedQuantity, { digits: [69, decimals] }); @@ -55,17 +55,17 @@ export class OrderlineDetails extends Component { return this.props.line.get_customer_note(); } getToRefundDetail() { - return this.pos.globalState.toRefundLines[this.props.line.id]; + return this.pos.toRefundLines[this.props.line.id]; } hasRefundedQty() { - return !this.pos.globalState.isProductQtyZero(this.props.line.refunded_qty); + return !this.pos.isProductQtyZero(this.props.line.refunded_qty); } getFormattedRefundedQty() { return this.env.utils.formatProductQty(this.props.line.refunded_qty); } hasToRefundQty() { const toRefundDetail = this.getToRefundDetail(); - return !this.pos.globalState.isProductQtyZero(toRefundDetail && toRefundDetail.qty); + return !this.pos.isProductQtyZero(toRefundDetail && toRefundDetail.qty); } getFormattedToRefundQty() { const toRefundDetail = this.getToRefundDetail(); diff --git a/addons/point_of_sale/static/src/app/screens/ticket_screen/ticket_screen.js b/addons/point_of_sale/static/src/app/screens/ticket_screen/ticket_screen.js index 013a1e07837..b3bec296495 100644 --- a/addons/point_of_sale/static/src/app/screens/ticket_screen/ticket_screen.js +++ b/addons/point_of_sale/static/src/app/screens/ticket_screen/ticket_screen.js @@ -54,7 +54,7 @@ export class TicketScreen extends Component { this.numberBuffer.use({ triggerAtInput: (event) => this._onUpdateSelectedOrderline(event), }); - this._state = this.pos.globalState.TICKET_SCREEN_STATE; + this._state = this.pos.TICKET_SCREEN_STATE; this.state = useState({ showSearchBar: !this.ui.isSmall, }); @@ -62,7 +62,7 @@ export class TicketScreen extends Component { ? this._state.ui : { selectedSyncedOrderId: null, - searchDetails: this.pos.globalState.getDefaultSearchDetails(), + searchDetails: this.pos.getDefaultSearchDetails(), filter: null, selectedOrderlineIds: {}, }; @@ -112,15 +112,13 @@ export class TicketScreen extends Component { } } onCreateNewOrder() { - this.pos.globalState.add_new_order(); + this.pos.add_new_order(); this.pos.showScreen("ProductScreen"); } _selectNextOrder(currentOrder) { const currentOrderIndex = this._getOrderList().indexOf(currentOrder); const orderList = this._getOrderList(); - this.pos.globalState.set_order( - orderList[currentOrderIndex + 1] || orderList[currentOrderIndex - 1] - ); + this.pos.set_order(orderList[currentOrderIndex + 1] || orderList[currentOrderIndex - 1]); } async onDeleteOrder(order) { const screen = order.get_screen_data(); @@ -142,18 +140,17 @@ export class TicketScreen extends Component { return; } } - const { globalState } = this.pos; if (order && (await this._onBeforeDeleteOrder(order))) { if (Object.keys(order.lastOrderPrepaChange).length > 0) { await this.pos.sendOrderInPreparation(order, true); } - if (order === globalState.get_order()) { + if (order === this.pos.get_order()) { this._selectNextOrder(order); } - globalState.removeOrder(order); + this.pos.removeOrder(order); } - if (globalState.isOpenOrderShareable()) { - globalState._removeOrdersFromServer(); + if (this.pos.isOpenOrderShareable()) { + this.pos._removeOrdersFromServer(); } } async onNextPage() { @@ -169,7 +166,7 @@ export class TicketScreen extends Component { } } async onInvoiceOrder(orderId) { - this.pos.globalState._invalidateSyncedOrdersCache([orderId]); + this.pos._invalidateSyncedOrdersCache([orderId]); await this._fetchSyncedOrders(); } onClickOrderline(orderline) { @@ -179,7 +176,7 @@ export class TicketScreen extends Component { } onClickRefundOrderUid(orderUid) { // Open the refund order. - const refundOrder = this.pos.globalState.orders.find((order) => order.uid == orderUid); + const refundOrder = this.pos.orders.find((order) => order.uid == orderUid); if (refundOrder) { this._setOrder(refundOrder); } @@ -259,10 +256,9 @@ export class TicketScreen extends Component { ? this.props.destinationOrder : this._getEmptyOrder(partner); - const { globalState } = this.pos; // Add orderline for each toRefundDetail to the destinationOrder. for (const refundDetail of allToRefundDetails) { - const product = globalState.db.get_product_by_id(refundDetail.orderline.productId); + const product = this.pos.db.get_product_by_id(refundDetail.orderline.productId); const options = this._prepareRefundOrderlineOptions(refundDetail); await destinationOrder.add_product(product, options); refundDetail.destinationOrderUid = destinationOrder.uid; @@ -274,8 +270,8 @@ export class TicketScreen extends Component { destinationOrder.updatePricelist(partner); } - if (globalState.get_order().cid !== destinationOrder.cid) { - globalState.set_order(destinationOrder); + if (this.pos.get_order().cid !== destinationOrder.cid) { + this.pos.set_order(destinationOrder); } this.pos.closeScreen(); @@ -363,7 +359,7 @@ export class TicketScreen extends Component { const productScreenStatus = this._getScreenToStatusMap().ProductScreen; return ( order.get_orderlines().length === 0 && - this.pos.globalState.get_order_list().length === 1 && + this.pos.get_order_list().length === 1 && status === productScreenStatus && order.get_paymentlines().length === 0 ); @@ -387,12 +383,12 @@ export class TicketScreen extends Component { const selectedOrder = this.getSelectedSyncedOrder(); return selectedOrder ? order.backendId == selectedOrder.backendId : false; } else { - const activeOrder = this.pos.globalState.get_order(); + const activeOrder = this.pos.get_order(); return activeOrder ? activeOrder.uid == order.uid : false; } } showCardholderName() { - return this.pos.globalState.payment_methods.some((method) => method.use_payment_terminal); + return this.pos.payment_methods.some((method) => method.use_payment_terminal); } getSearchBarConfig() { return { @@ -426,7 +422,7 @@ export class TicketScreen extends Component { if (this._doesOrderHaveSoleItem(order)) { return true; } - const total = Object.values(this.pos.globalState.toRefundLines) + const total = Object.values(this.pos.toRefundLines) .filter( (toRefundDetail) => toRefundDetail.orderline.orderUid === order.uid && @@ -434,7 +430,7 @@ export class TicketScreen extends Component { ) .map((toRefundDetail) => toRefundDetail.qty) .reduce((acc, val) => acc + val, 0); - return !this.pos.globalState.isProductQtyZero(total); + return !this.pos.isProductQtyZero(total); } //#endregion //#region PRIVATE METHODS @@ -449,7 +445,7 @@ export class TicketScreen extends Component { _getEmptyOrder(partner) { let emptyOrderForPartner = null; let emptyOrder = null; - for (const order of this.pos.globalState.orders) { + for (const order of this.pos.orders) { if (order.get_orderlines().length === 0 && order.get_paymentlines().length === 0) { if (order.get_partner() === partner) { emptyOrderForPartner = order; @@ -460,7 +456,7 @@ export class TicketScreen extends Component { } } } - return emptyOrderForPartner || emptyOrder || this.pos.globalState.add_new_order(); + return emptyOrderForPartner || emptyOrder || this.pos.add_new_order(); } _doesOrderHaveSoleItem(order) { const orderlines = order.get_orderlines(); @@ -469,7 +465,7 @@ export class TicketScreen extends Component { } const theOrderline = orderlines[0]; const refundableQty = theOrderline.get_quantity() - theOrderline.refunded_qty; - return this.pos.globalState.isProductQtyZero(refundableQty - 1); + return this.pos.isProductQtyZero(refundableQty - 1); } _prepareAutoRefundOnOrder(order) { const selectedOrderlineId = this.getSelectedOrderlineId(); @@ -480,7 +476,7 @@ export class TicketScreen extends Component { const toRefundDetail = this._getToRefundDetail(orderline); const refundableQty = orderline.get_quantity() - orderline.refunded_qty; - if (this.pos.globalState.isProductQtyZero(refundableQty - 1) && toRefundDetail.qty === 0) { + if (this.pos.isProductQtyZero(refundableQty - 1) && toRefundDetail.qty === 0) { toRefundDetail.qty = 1; } return true; @@ -493,7 +489,7 @@ export class TicketScreen extends Component { * @returns */ _getToRefundDetail(orderline) { - const { toRefundLines } = this.pos.globalState; + const { toRefundLines } = this.pos; if (orderline.id in toRefundLines) { return toRefundLines[orderline.id]; } @@ -529,9 +525,9 @@ export class TicketScreen extends Component { * @returns {Array} refundableDetails */ _getRefundableDetails(partner) { - return Object.values(this.pos.globalState.toRefundLines).filter( + return Object.values(this.pos.toRefundLines).filter( ({ qty, orderline, destinationOrderUid }) => - !this.pos.globalState.isProductQtyZero(qty) && + !this.pos.isProductQtyZero(qty) && (partner ? orderline.orderPartnerId == partner.id : true) && !destinationOrderUid ); @@ -555,15 +551,14 @@ export class TicketScreen extends Component { }; } _setOrder(order) { - const { globalState } = this.pos; - if (globalState.isOpenOrderShareable()) { - globalState.sendDraftToServer(); + if (this.pos.isOpenOrderShareable()) { + this.pos.sendDraftToServer(); } - globalState.set_order(order); + this.pos.set_order(order); this.pos.closeScreen(); } _getOrderList() { - return this.pos.globalState.get_order_list(); + return this.pos.get_order_list(); } _getFilterOptions() { const orderStates = this._getOrderStates(); @@ -662,12 +657,11 @@ export class TicketScreen extends Component { * order is not fetched anymore, instead, we use info from cache. */ async _fetchSyncedOrders() { - const { globalState } = this.pos; const domain = this._computeSyncedOrdersDomain(); const limit = this._state.syncedOrders.nPerPage; const offset = (this._state.syncedOrders.currentPage - 1) * this._state.syncedOrders.nPerPage; - const config_id = globalState.config.id; + const config_id = this.pos.config.id; const { ordersInfo, totalCount } = await this.orm.call( "pos.order", "search_paid_order_ids", @@ -686,14 +680,14 @@ export class TicketScreen extends Component { if (idsToLoad.length > 0) { const fetchedOrders = await this.orm.call("pos.order", "export_for_ui", [idsToLoad]); // Check for missing products and partners and load them in the PoS - await globalState._loadMissingProducts(fetchedOrders); - await globalState._loadMissingPartners(fetchedOrders); + await this.pos._loadMissingProducts(fetchedOrders); + await this.pos._loadMissingPartners(fetchedOrders); // Cache these fetched orders so that next time, no need to fetch // them again, unless invalidated. See `_onInvoiceOrder`. fetchedOrders.forEach((order) => { this._state.syncedOrders.cache[order.id] = new Order( {}, - { pos: globalState, json: order } + { pos: this.pos, json: order } ); }); //Update the datetime indicator of the cache refresh diff --git a/addons/point_of_sale/static/src/app/store/cash_opening_popup/cash_opening_popup.js b/addons/point_of_sale/static/src/app/store/cash_opening_popup/cash_opening_popup.js index 0249807333f..e6bb8157bba 100644 --- a/addons/point_of_sale/static/src/app/store/cash_opening_popup/cash_opening_popup.js +++ b/addons/point_of_sale/static/src/app/store/cash_opening_popup/cash_opening_popup.js @@ -18,7 +18,7 @@ export class CashOpeningPopup extends AbstractAwaitablePopup { this.pos = usePos(); this.state = useState({ notes: "", - openingCash: this.pos.globalState.pos_session.cash_register_balance_start || 0, + openingCash: this.pos.pos_session.cash_register_balance_start || 0, }); this.popup = useService("popup"); this.orm = useService("orm"); @@ -27,10 +27,10 @@ export class CashOpeningPopup extends AbstractAwaitablePopup { } //@override async confirm() { - this.pos.globalState.pos_session.cash_register_balance_start = this.state.openingCash; - this.pos.globalState.pos_session.state = "opened"; + this.pos.pos_session.cash_register_balance_start = this.state.openingCash; + this.pos.pos_session.state = "opened"; this.orm.call("pos.session", "set_cashbox_pos", [ - this.pos.globalState.pos_session.id, + this.pos.pos_session.id, this.state.openingCash, this.state.notes, ]); diff --git a/addons/point_of_sale/static/src/app/store/models.js b/addons/point_of_sale/static/src/app/store/models.js index 90c8803e6f9..74fe8466963 100644 --- a/addons/point_of_sale/static/src/app/store/models.js +++ b/addons/point_of_sale/static/src/app/store/models.js @@ -1,10 +1,7 @@ /** @odoo-module */ -/* global waitForWebfonts */ -import { PosDB } from "@point_of_sale/app/store/db"; import { formatFloat } from "@web/views/fields/formatters"; -import { uuidv4, batched, deduceUrl } from "@point_of_sale/utils"; -import { HWPrinter } from "@point_of_sale/app/printer/hw_printer"; +import { uuidv4 } from "@point_of_sale/utils"; // FIXME POSREF - unify use of native parseFloat and web's parseFloat. We probably don't need the native version. import { parseFloat as oParseFloat } from "@web/views/fields/parsers"; import { formatDate, formatDateTime, serializeDateTime } from "@web/core/l10n/dates"; @@ -16,70 +13,20 @@ import { import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; import { ProductConfiguratorPopup } from "@point_of_sale/app/store/product_configurator_popup/product_configurator_popup"; import { EditListPopup } from "@point_of_sale/app/store/select_lot_popup/select_lot_popup"; -import { markRaw, reactive } from "@odoo/owl"; import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup"; import { sprintf } from "@web/core/utils/strings"; -import { Mutex } from "@web/core/utils/concurrency"; -import { memoize } from "@web/core/utils/functions"; import { _t } from "@web/core/l10n/translation"; -import { renderToString, renderToElement } from "@web/core/utils/render"; +import { renderToElement } from "@web/core/utils/render"; const { DateTime } = luxon; -/* Returns an array containing all elements of the given - * array corresponding to the rule function {agg} and without duplicates - * - * @template T - * @template F - * @param {T[]} array - * @param {F} function - * @returns {T[]} - */ -export function uniqueBy(array, agg) { - const map = new Map(); - for (const item of array) { - const key = agg(item); - if (!map.has(key)) { - map.set(key, item); - } - } - return [...map.values()]; -} - -/** - * Gets a product image as a base64 string so that it can be sent to the - * customer display, as the display won't be able to fetch it, since the image - * controller requires the client to be logged. This function is memoized on the - * product id, so that we will only do this once per product. - * - * @param {number} productId id of the product - * @param {string} writeDate the write date of the product, used as a cache - * buster in case the product image has been changed - * @returns {string} the base64 representation of the product's image - */ -const getProductImage = memoize(function getProductImage(productId, writeDate) { - return new Promise(function (resolve, reject) { - const img = new Image(); - img.addEventListener("load", () => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.height = img.height; - canvas.width = img.width; - ctx.drawImage(img, 0, 0); - resolve(canvas.toDataURL("image/jpeg")); - }); - img.addEventListener("error", reject); - img.src = `/web/image?model=product.product&field=image_128&id=${productId}&unique=${writeDate}`; - }); -}); - /** * If optimization is needed, then we should implement this * using a Balanced Binary Tree to behave like an Object and an Array. * But behaving like Object (indexed by cid) might not be * needed. Let's see how it turns out. */ -class PosCollection extends Array { +export class PosCollection extends Array { getByCID(cid) { return this.find((item) => item.cid == cid); } @@ -136,1447 +83,6 @@ class PosModel { return `c${nextId++}`; } } - -export class PosGlobalState extends PosModel { - setup() { - super.setup(...arguments); - - this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders - this.unwatched = markRaw({}); - this.pushOrderMutex = new Mutex(); - - // Business data; loaded from the server at launch - this.company_logo = null; - this.company_logo_base64 = ""; - this.currency = null; - this.company = null; - this.user = null; - this.partners = []; - this.taxes = []; - this.pos_session = null; - this.config = null; - this.units = []; - this.units_by_id = {}; - this.uom_unit_id = null; - this.default_pricelist = null; - this.order_sequence = 1; - this.printers_category_ids_set = new Set(); - - // Object mapping the order's name (which contains the uid) to it's server_id after - // validation (order paid then sent to the backend). - this.validated_orders_name_server_id_map = {}; - - this.numpadMode = "quantity"; - this.mobile_pane = "right"; - - // Record - this.toRefundLines = {}; - this.TICKET_SCREEN_STATE = { - syncedOrders: { - currentPage: 1, - cache: {}, - toShow: [], - nPerPage: 80, - totalCount: null, - cacheDate: null, - }, - ui: { - selectedSyncedOrderId: null, - searchDetails: this.getDefaultSearchDetails(), - filter: null, - // maps the order's backendId to it's selected orderline - selectedOrderlineIds: {}, - highlightHeaderNote: false, - }, - }; - - this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing - this.loadingOrderState = false; // used to prevent orders fetched to be put in the update set during the reactive change - this.showOfflineWarning = true; // Allows to avoid the display of the offline popup when the user has already had it. - - // these dynamic attributes can be watched for change by other models or widgets - Object.assign(this, { - synch: { status: "connected", pending: 0 }, - orders: new PosCollection(), - selectedOrder: null, - selectedPartner: null, - selectedCategoryId: null, - // FIXME POSREF this piece of state should probably be private to the product screen - // but it currently needs to be available to the ProductInfo screen for dubious functional reasons - searchProductWord: "", - }); - - this.ready = new Promise((resolve) => { - this.markReady = resolve; - }); - } - getDefaultSearchDetails() { - return { - fieldName: "RECEIPT_NUMBER", - searchTerm: "", - }; - } - async load_product_uom_unit() { - const uom_id = await this.orm.call("ir.model.data", "check_object_reference", [ - "uom", - "product_uom_unit", - ]); - this.uom_unit_id = uom_id[1]; - } - - async after_load_server_data() { - await this.load_product_uom_unit(); - await this.load_orders(); - this.set_start_order(); - Object.assign(this.toRefundLines, this.db.load("TO_REFUND_LINES") || {}); - window.addEventListener("beforeunload", () => - this.db.save("TO_REFUND_LINES", this.toRefundLines) - ); - const { start_category, iface_start_categ_id } = this.config; - this.selectedCategoryId = (start_category && iface_start_categ_id?.[0]) || 0; - this.hasBigScrollBars = this.config.iface_big_scrollbars; - // Push orders in background, do not await - this.push_orders(); - this.markReady(); - } - - async load_server_data() { - const loadedData = await this.orm.silent.call("pos.session", "load_pos_data", [ - [odoo.pos_session_id], - ]); - await this._processData(loadedData); - return this.after_load_server_data(); - } - async _processData(loadedData) { - this.version = loadedData["version"]; - this.company = loadedData["res.company"]; - this.dp = loadedData["decimal.precision"]; - this.units = loadedData["uom.uom"]; - this.units_by_id = loadedData["units_by_id"]; - this.states = loadedData["res.country.state"]; - this.countries = loadedData["res.country"]; - this.langs = loadedData["res.lang"]; - this.taxes = loadedData["account.tax"]; - this.taxes_by_id = loadedData["taxes_by_id"]; - this.pos_session = loadedData["pos.session"]; - this._loadPosSession(); - this.config = loadedData["pos.config"]; - this._loadPoSConfig(); - this.bills = loadedData["pos.bill"]; - this.partners = loadedData["res.partner"]; - this.addPartners(this.partners); - this.picking_type = loadedData["stock.picking.type"]; - this.user = loadedData["res.users"]; - this.pricelists = loadedData["product.pricelist"]; - this.default_pricelist = loadedData["default_pricelist"]; - this.currency = loadedData["res.currency"]; - this.db.add_categories(loadedData["pos.category"]); - this._loadProductProduct(loadedData["product.product"]); - this.db.add_packagings(loadedData["product.packaging"]); - this.attributes_by_ptal_id = loadedData["attributes_by_ptal_id"]; - this.cash_rounding = loadedData["account.cash.rounding"]; - this.payment_methods = loadedData["pos.payment.method"]; - this._loadPosPaymentMethod(); - this.fiscal_positions = loadedData["account.fiscal.position"]; - this.base_url = loadedData["base_url"]; - await this._loadFonts(); - await this._loadPictures(); - await this._loadPosPrinters(loadedData["pos.printer"]); - } - _loadPosSession() { - // We need to do it here, since only then the local storage has the correct uuid - this.db.save("pos_session_id", this.pos_session.id); - const orders = this.db.get_orders(); - const sequences = orders.map((order) => order.data.sequence_number + 1); - this.pos_session.sequence_number = Math.max(this.pos_session.sequence_number, ...sequences); - this.pos_session.login_number = odoo.login_number; - } - _loadPosPrinters(printers) { - this.unwatched.printers = []; - // list of product categories that belong to one or more order printer - for (const printerConfig of printers) { - const printer = this.create_printer(printerConfig); - printer.config = printerConfig; - this.unwatched.printers.push(printer); - for (const id of printer.config.product_categories_ids) { - this.printers_category_ids_set.add(id); - } - } - this.config.iface_printers = !!this.unwatched.printers.length; - } - create_printer(config) { - const url = deduceUrl(config.proxy_ip || ""); - return new HWPrinter({ rpc: this.env.services.rpc, url }); - } - _loadPoSConfig() { - this.db.set_uuid(this.config.uuid); - } - addPartners(partners) { - return this.db.add_partners(partners); - } - _assignApplicableItems(pricelist, correspondingProduct, pricelistItem) { - if (!(pricelist.id in correspondingProduct.applicablePricelistItems)) { - correspondingProduct.applicablePricelistItems[pricelist.id] = []; - } - correspondingProduct.applicablePricelistItems[pricelist.id].push(pricelistItem); - } - _loadProductProduct(products) { - const productMap = {}; - const productTemplateMap = {}; - - const modelProducts = products.map((product) => { - product.pos = this; - product.applicablePricelistItems = {}; - productMap[product.id] = product; - productTemplateMap[product.product_tmpl_id[0]] = ( - productTemplateMap[product.product_tmpl_id[0]] || [] - ).concat(product); - return new Product(product); - }); - - for (const pricelist of this.pricelists) { - for (const pricelistItem of pricelist.items) { - if (pricelistItem.product_id) { - const product_id = pricelistItem.product_id[0]; - const correspondingProduct = productMap[product_id]; - if (correspondingProduct) { - this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); - } - } else if (pricelistItem.product_tmpl_id) { - const product_tmpl_id = pricelistItem.product_tmpl_id[0]; - const correspondingProducts = productTemplateMap[product_tmpl_id]; - for (const correspondingProduct of correspondingProducts || []) { - this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); - } - } else { - for (const correspondingProduct of products) { - this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); - } - } - } - } - this.db.add_products(modelProducts); - } - _loadPosPaymentMethod() { - // need to do this for pos_iot due to reference, this is a temporary fix - this.payment_methods_by_id = {}; - for (const pm of this.payment_methods) { - this.payment_methods_by_id[pm.id] = pm; - const PaymentInterface = this.electronic_payment_interfaces[pm.use_payment_terminal]; - if (PaymentInterface) { - pm.payment_terminal = new PaymentInterface(this, pm); - } - } - } - async _loadFonts() { - return new Promise(function (resolve, reject) { - // Waiting for fonts to be loaded to prevent receipt printing - // from printing empty receipt while loading Inconsolata - // ( The font used for the receipt ) - waitForWebfonts(["Lato", "Inconsolata"], function () { - resolve(); - }); - // The JS used to detect font loading is not 100% robust, so - // do not wait more than 5sec - setTimeout(resolve, 5000); - }); - } - async _loadPictures() { - this.company_logo = new Image(); - return new Promise((resolve, reject) => { - this.company_logo.onload = () => { - const img = this.company_logo; - let ratio = 1; - const targetwidth = 300; - const maxheight = 150; - if (img.width !== targetwidth) { - ratio = targetwidth / img.width; - } - if (img.height * ratio > maxheight) { - ratio = maxheight / img.height; - } - const width = Math.floor(img.width * ratio); - const height = Math.floor(img.height * ratio); - const c = document.createElement("canvas"); - c.width = width; - c.height = height; - const ctx = c.getContext("2d"); - ctx.drawImage(this.company_logo, 0, 0, width, height); - - this.company_logo_base64 = c.toDataURL(); - resolve(); - }; - this.company_logo.onerror = () => { - reject(); - }; - this.company_logo.crossOrigin = "anonymous"; - this.company_logo.src = `/web/image?model=res.company&id=${this.company.id}&field=logo`; - }); - } - prepare_new_partners_domain() { - return [["write_date", ">", this.db.get_partner_write_date()]]; - } - - // reload the list of partner, returns as a promise that resolves if there were - // updated partners, and fails if not - async load_new_partners() { - const search_params = { domain: this.prepare_new_partners_domain() }; - // FIXME POSREF TIMEOUT 3000 - const partners = await this.orm.silent.call( - "pos.session", - "get_pos_ui_res_partner_by_params", - [[odoo.pos_session_id], search_params] - ); - return this.addPartners(partners); - } - - setSelectedCategoryId(categoryId) { - this.selectedCategoryId = categoryId; - } - - /** - * Remove the order passed in params from the list of orders - * @param order - */ - removeOrder(order, removeFromServer = true) { - this.orders.remove(order); - this.db.remove_unpaid_order(order); - for (const line of order.get_orderlines()) { - if (line.refunded_orderline_id) { - delete this.toRefundLines[line.refunded_orderline_id]; - } - } - if (this.isOpenOrderShareable() && removeFromServer) { - if (this.ordersToUpdateSet.has(order)) { - this.ordersToUpdateSet.delete(order); - } - if (order.server_id && !order.finalized) { - this.setOrderToRemove(order); - } - } - } - setOrderToRemove(order) { - this.db.set_order_to_remove_from_server(order); - } - - /** - * Return the current cashier (in this case, the user) - * @returns {name: string, id: int, role: string} - */ - get_cashier() { - return this.user; - } - get_cashier_user_id() { - return this.user.id; - } - get orderPreparationCategories() { - if (this.printers_category_ids_set) { - return new Set([...this.printers_category_ids_set]); - } - return new Set(); - } - cashierHasPriceControlRights() { - return !this.config.restrict_price_control || this.get_cashier().role == "manager"; - } - _onReactiveOrderUpdated(order) { - order.save_to_db(); - if (this.isOpenOrderShareable() && !this.loadingOrderState) { - this.ordersToUpdateSet.add(order); - } - } - createReactiveOrder(json) { - const options = { pos: this }; - if (json) { - options.json = json; - } - return this.makeOrderReactive(new Order({}, options)); - } - makeOrderReactive(order) { - const batchedCallback = batched(() => { - this._onReactiveOrderUpdated(order); - }); - order = reactive(order, batchedCallback); - order.save_to_db(); - return order; - } - // creates a new empty order and sets it as the current order - add_new_order() { - if (this.isOpenOrderShareable()) { - this.sendDraftToServer(); - } - if (this.selectedOrder) { - this.selectedOrder.firstDraft = false; - this.selectedOrder.updateSavedQuantity(); - } - const order = this.createReactiveOrder(); - this.orders.add(order); - this.selectedOrder = order; - return order; - } - selectNextOrder() { - if (this.orders.length > 0) { - this.selectedOrder = this.orders[0]; - } else { - this.add_new_order(); - } - } - async sendDraftToServer() { - const ordersUidsToSync = [...this.ordersToUpdateSet].map((order) => order.uid); - const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); - const ordersResponse = await this._save_to_server(ordersToSync, { draft: true }); - const orders = [...this.ordersToUpdateSet].map((order) => order); - ordersResponse.forEach((orderResponseData) => this._updateOrder(orderResponseData, orders)); - this.ordersToUpdateSet.clear(); - } - addOrderToUpdateSet() { - this.ordersToUpdateSet.add(this.selectedOrder); - } - // created this hook for modularity - _updateOrder(ordersResponseData, orders) { - const order = orders.find((order) => order.name === ordersResponseData.pos_reference); - if (order) { - order.server_id = ordersResponseData.id; - return order; - } - } - /** - * Load the locally saved unpaid orders for this PoS Config. - * - * First load all orders belonging to the current session. - * Second load all orders belonging to the same config but from other sessions, - * Only if tho order has orderlines. - */ - async load_orders() { - this.loadingOrderState = true; - var jsons = this.db.get_unpaid_orders(); - await this._loadMissingProducts(jsons); - await this._loadMissingPartners(jsons); - var orders = []; - - for (var i = 0; i < jsons.length; i++) { - var json = jsons[i]; - if (json.pos_session_id === this.pos_session.id) { - orders.push(this.createReactiveOrder(json)); - } - } - for (i = 0; i < jsons.length; i++) { - json = jsons[i]; - if ( - json.pos_session_id !== this.pos_session.id && - (json.lines.length > 0 || json.statement_ids.length > 0) - ) { - orders.push(this.createReactiveOrder(json)); - } else if (json.pos_session_id !== this.pos_session.id) { - this.db.remove_unpaid_order(jsons[i]); - } - } - - orders = orders.sort(function (a, b) { - return a.sequence_number - b.sequence_number; - }); - - if (orders.length) { - for (const order of orders) { - this.orders.add(order); - } - } - this.loadingOrderState = false; - } - async _loadMissingProducts(orders) { - const missingProductIds = new Set([]); - for (const order of orders) { - for (const line of order.lines) { - const productId = line[2].product_id; - if (missingProductIds.has(productId)) { - continue; - } - if (!this.db.get_product_by_id(productId)) { - missingProductIds.add(productId); - } - } - } - const products = await this.orm.call( - "pos.session", - "get_pos_ui_product_product_by_params", - [odoo.pos_session_id, { domain: [["id", "in", [...missingProductIds]]] }] - ); - this._loadProductProduct(products); - } - // load the partners based on the ids - async _loadPartners(partnerIds) { - if (partnerIds.length > 0) { - // FIXME POSREF TIMEOUT - const fetchedPartners = await this.orm.silent.call( - "pos.session", - "get_pos_ui_res_partner_by_params", - [[odoo.pos_session_id], { domain: [["id", "in", partnerIds]] }] - ); - this.addPartners(fetchedPartners); - } - } - async _loadMissingPartners(orders) { - const missingPartnerIds = new Set([]); - for (const order of orders) { - const partnerId = order.partner_id; - if (missingPartnerIds.has(partnerId)) { - continue; - } - if (partnerId && !this.db.get_partner_by_id(partnerId)) { - missingPartnerIds.add(partnerId); - } - } - await this._loadPartners([...missingPartnerIds]); - } - setLoadingOrderState(bool) { - this.loadingOrderState = bool; - } - async _removeOrdersFromServer() { - const removedOrdersIds = this.db.get_ids_to_remove_from_server(); - if (removedOrdersIds.length === 0) { - return; - } - - this.set_synch("connecting", removedOrdersIds.length); - try { - const removeOrdersResponseData = await this.orm.silent.call( - "pos.order", - "remove_from_ui", - [removedOrdersIds] - ); - this.set_synch("connected"); - this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData); - } catch (reason) { - const error = reason.message; - if (error.code === 200) { - // Business Logic Error, not a connection problem - //if warning do not need to display traceback!! - if (error.data.exception_type == "warning") { - delete error.data.debug; - } - } - // important to throw error here and let the rendering component handle the error - console.warn("Failed to remove orders:", removedOrdersIds); - this._postRemoveFromServer(removedOrdersIds); - throw error; - } - } - _postRemoveFromServer(serverIds, data) { - this.db.set_ids_removed_from_server(serverIds); - } - _replaceOrders(ordersToReplace, newOrdersJsons) { - ordersToReplace.forEach((order) => { - // We don't remove the validated orders because we still want to see them in the ticket screen. - // Orders in 'ReceiptScreen' or 'TipScreen' are validated orders. - if (this._shouldRemoveOrder(order)) { - this.removeOrder(order, false); - } - }); - let removeSelected = true; - newOrdersJsons.forEach((json) => { - const isSelectedOrder = this._createOrder(json); - if (removeSelected && isSelectedOrder) { - removeSelected = false; - } - }); - if (this._shouldRemoveSelectedOrder(removeSelected)) { - this._removeSelectedOrder(); - } - } - _shouldRemoveOrder(order) { - return ( - (!this.selectedOrder || this.selectedOrder.uid != order.uid) && - order.server_id && - !order.finalized - ); - } - _shouldRemoveSelectedOrder(removeSelected) { - return removeSelected && this.selectedOrder.server_id && !this.selectedOrder.finalized; - } - _shouldCreateOrder(json) { - return json.uid != this.selectedOrder.uid; - } - _isSelectedOrder(json) { - return json.uid == this.selectedOrder.uid; - } - _createOrder(json) { - if (this._shouldCreateOrder(json)) { - const order = this.createReactiveOrder(json); - this.orders.add(order); - } - return this._isSelectedOrder(json); - } - _removeSelectedOrder() { - this.removeOrder(this.selectedOrder, false); - const orderList = this.get_order_list(); - if (orderList.length != 0) { - this.set_order(orderList[0]); - } - } - async _syncAllOrdersFromServer() { - await this._removeOrdersFromServer(); - const ordersJson = await this._getOrdersJson(); - let message = null; - message = await this._addPricelists(ordersJson); - let messageFp = null; - messageFp = await this._addFiscalPositions(ordersJson); - if (messageFp) { - if (message) { - message += "\n" + messageFp; - } else { - message = messageFp; - } - } - const allOrders = [...this.get_order_list()]; - this._replaceOrders(allOrders, ordersJson); - this.sortOrders(); - return message; - } - async _getOrdersJson() { - return await this.orm.call("pos.order", "get_draft_share_order_ids", [], { - config_id: this.config.id, - }); - } - async _addPricelists(ordersJson) { - const pricelistsToGet = []; - ordersJson.forEach((order) => { - let found = false; - for (const pricelist of this.pricelists) { - if (pricelist.id === order.pricelist_id) { - found = true; - break; - } - } - if (!found && order.pricelist_id) { - pricelistsToGet.push(order.pricelist_id); - } - }); - let message = null; - if (pricelistsToGet.length > 0) { - const pricelistsJson = await this._getPricelistJson(pricelistsToGet); - message = this._addPosPricelists(pricelistsJson); - } - return message; - } - async _getPricelistJson(pricelistsToGet) { - return await this.env.services.orm.call( - "pos.session", - "get_pos_ui_product_pricelists_by_ids", - [[odoo.pos_session_id], pricelistsToGet] - ); - } - _addPosPricelists(pricelistsJson) { - if (!this.config.use_pricelist) { - this.config.use_pricelist = true; - } - this.pricelists.push(...pricelistsJson); - let message = ""; - const pricelistsNames = pricelistsJson.map((pricelist) => { - return pricelist.display_name; - }); - message = sprintf( - _t("%s fiscal position(s) added to the configuration."), - pricelistsNames.join(", ") - ); - return message; - } - async _addFiscalPositions(ordersJson) { - const fiscalPositionToGet = []; - ordersJson.forEach((order) => { - let found = false; - for (const fp of this.fiscal_positions) { - if (fp.id === order.fiscal_position_id) { - found = true; - break; - } - } - if (!found && order.fiscal_position_id) { - fiscalPositionToGet.push(order.fiscal_position_id); - } - }); - let message = null; - if (fiscalPositionToGet.length > 0) { - const fiscalPositionJson = await this._getFiscalPositionJson(fiscalPositionToGet); - message = this._addPosFiscalPosition(fiscalPositionJson); - } - return message; - } - async _getFiscalPositionJson(fiscalPositionToGet) { - return await this.env.services.orm.call( - "pos.session", - "get_pos_ui_account_fiscal_positions_by_ids", - [[odoo.pos_session_id], fiscalPositionToGet] - ); - } - _addPosFiscalPosition(fiscalPositionJson) { - this.fiscal_positions.push(...fiscalPositionJson); - let message = ""; - const fiscalPositionNames = fiscalPositionJson.map((fp) => { - return fp.display_name; - }); - message = sprintf( - _t("%s fiscal position(s) added to the configuration."), - fiscalPositionNames.join(", ") - ); - return message; - } - sortOrders() { - this.orders.sort((a, b) => (a.name > b.name ? 1 : -1)); - } - async getProductInfo(product, quantity) { - const order = this.get_order(); - // check back-end method `get_product_info_pos` to see what it returns - // We do this so it's easier to override the value returned and use it in the component template later - const productInfo = await this.orm.call("product.product", "get_product_info_pos", [ - [product.id], - product.get_price(order.pricelist, quantity), - quantity, - this.config.id, - ]); - - const priceWithoutTax = productInfo["all_prices"]["price_without_tax"]; - const margin = priceWithoutTax - product.standard_price; - const orderPriceWithoutTax = order.get_total_without_tax(); - const orderCost = order.get_total_cost(); - const orderMargin = orderPriceWithoutTax - orderCost; - - const costCurrency = this.env.utils.formatCurrency(product.standard_price); - const marginCurrency = this.env.utils.formatCurrency(margin); - const marginPercent = priceWithoutTax - ? Math.round((margin / priceWithoutTax) * 10000) / 100 - : 0; - const orderPriceWithoutTaxCurrency = this.env.utils.formatCurrency(orderPriceWithoutTax); - const orderCostCurrency = this.env.utils.formatCurrency(orderCost); - const orderMarginCurrency = this.env.utils.formatCurrency(orderMargin); - const orderMarginPercent = orderPriceWithoutTax - ? Math.round((orderMargin / orderPriceWithoutTax) * 10000) / 100 - : 0; - return { - costCurrency, - marginCurrency, - marginPercent, - orderPriceWithoutTaxCurrency, - orderCostCurrency, - orderMarginCurrency, - orderMarginPercent, - productInfo, - }; - } - async getClosePosInfo() { - const closingData = await this.orm.call("pos.session", "get_closing_control_data", [ - [this.pos_session.id], - ]); - const ordersDetails = closingData.orders_details; - const paymentsAmount = closingData.payments_amount; - const payLaterAmount = closingData.pay_later_amount; - const openingNotes = closingData.opening_notes; - const defaultCashDetails = closingData.default_cash_details; - const otherPaymentMethods = closingData.other_payment_methods; - const isManager = closingData.is_manager; - const amountAuthorizedDiff = closingData.amount_authorized_diff; - const cashControl = this.config.cash_control; - - // component state and refs definition - const state = { notes: "", acceptClosing: false, payments: {} }; - if (cashControl) { - state.payments[defaultCashDetails.id] = { - counted: 0, - difference: -defaultCashDetails.amount, - number: 0, - }; - } - if (otherPaymentMethods.length > 0) { - otherPaymentMethods.forEach((pm) => { - if (pm.type === "bank") { - state.payments[pm.id] = { - counted: this.env.utils - .roundCurrency(pm.amount) - .toFixed(this.currency.decimal_places), - difference: 0, - number: pm.number, - }; - } - }); - } - return { - ordersDetails, - paymentsAmount, - payLaterAmount, - openingNotes, - defaultCashDetails, - otherPaymentMethods, - isManager, - amountAuthorizedDiff, - state, - cashControl, - }; - } - set_start_order() { - if (this.orders.length && !this.selectedOrder) { - this.selectedOrder = this.orders[0]; - if (this.isOpenOrderShareable()) { - this.ordersToUpdateSet.add(this.orders[0]); - } - } else { - this.add_new_order(); - } - } - - // return the current order - get_order() { - return this.selectedOrder; - } - - // change the current order - set_order(order, options) { - if (this.selectedOrder) { - this.selectedOrder.firstDraft = false; - this.selectedOrder.updateSavedQuantity(); - } - this.selectedOrder = order; - } - - // return the list of unpaid orders - get_order_list() { - return this.orders; - } - - /** - * Renders the HTML for the customer display and returns it as a string. - * - * @returns {string} - */ - async customerDisplayHTML(closeUI = false) { - const backgroundImageBase64 = - this.config.iface_customer_facing_display_background_image_1920; - let backgroundImageCSSValue; - if (backgroundImageBase64) { - backgroundImageCSSValue = "url('data:image/png;base64," + backgroundImageBase64 + "')"; - } else { - backgroundImageCSSValue = "none"; - } - - const order = this.get_order(); - if (closeUI || !order) { - return renderToString("point_of_sale.CustomerFacingDisplayNoOrder", { - pos: this, - origin: window.location.origin, - backgroundImageCSSValue, - }); - } - - const orderLines = order.get_orderlines(); - const productImages = Object.fromEntries( - await Promise.all( - orderLines.map(async ({ product }) => [ - product.id, - await getProductImage(product.id, product.writeDate), - ]) - ) - ); - - return renderToString("point_of_sale.CustomerFacingDisplayOrder", { - pos: this, - formatCurrency: this.env.utils.formatCurrency, - origin: window.location.origin, - backgroundImageCSSValue, - order, - productImages, - }); - } - - push_orders(opts = {}) { - return this.pushOrderMutex.exec(() => this._flush_orders(this.db.get_orders(), opts)); - } - - push_single_order(order) { - const order_id = this.db.add_order(order.export_as_JSON()); - return this.pushOrderMutex.exec(() => this._flush_orders([this.db.get_order(order_id)])); - } - - // Send validated orders to the backend. - // Resolves to the backend ids of the synced orders. - _flush_orders(orders, options) { - var self = this; - - return this._save_to_server(orders, options) - .then(function (server_ids) { - for (let i = 0; i < server_ids.length; i++) { - self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = - server_ids[i].id; - } - return server_ids; - }) - .finally(function () { - self._after_flush_orders(orders); - }); - } - /** - * Hook method after _flush_orders resolved or rejected. - * It aims to: - * - remove the refund orderlines from toRefundLines - * - invalidate cache of refunded synced orders - */ - _after_flush_orders(orders) { - const refundedOrderIds = new Set(); - for (const order of orders) { - for (const line of order.data.lines) { - const refundDetail = this.toRefundLines[line[2].refunded_orderline_id]; - if (!refundDetail) { - continue; - } - // Collect the backend id of the refunded orders. - refundedOrderIds.add(refundDetail.orderline.orderBackendId); - // Reset the refund detail for the orderline. - delete this.toRefundLines[refundDetail.orderline.id]; - } - } - this._invalidateSyncedOrdersCache([...refundedOrderIds]); - } - _invalidateSyncedOrdersCache(ids) { - for (const id of ids) { - delete this.TICKET_SCREEN_STATE.syncedOrders.cache[id]; - } - } - set_synch(status, pending) { - if (["connected", "connecting", "error", "disconnected"].indexOf(status) === -1) { - console.error(status, " is not a known connection state."); - } else if (status === "connected") { - this.showOfflineWarning = true; - } - - pending = - pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length; - this.synch = { status, pending }; - } - - // send an array of orders to the server - // available options: - // - timeout: timeout for the rpc call in ms - // returns a promise that resolves with the list of - // server generated ids for the sent orders - async _save_to_server(orders, options) { - if (!orders || !orders.length) { - return Promise.resolve([]); - } - this.set_synch("connecting", orders.length); - options = options || {}; - - // Keep the order ids that are about to be sent to the - // backend. In between create_from_ui and the success callback - // new orders may have been added to it. - var order_ids_to_sync = orders.map((o) => o.id); - - for (const order of orders) { - order.to_invoice = options.to_invoice || false; - } - // we try to send the order. silent prevents a spinner if it takes too long. (unless we are sending an invoice, - // then we want to notify the user that we are waiting on something ) - const orm = options.to_invoice ? this.orm : this.orm.silent; - - try { - // FIXME POSREF timeout - // const timeout = typeof options.timeout === "number" ? options.timeout : 30000 * orders.length; - const serverIds = await orm.call("pos.order", "create_from_ui", [ - orders, - options.draft || false, - ]); - - for (const serverId of serverIds) { - const order = this.env.services.pos.globalState.orders.find( - (order) => order.name === serverId.pos_reference - ); - - if (order) { - order.server_id = serverId.id; - } - } - - for (const order_id of order_ids_to_sync) { - this.db.remove_order(order_id); - } - - this.failed = false; - this.set_synch("connected"); - return serverIds; - } catch (error) { - console.warn("Failed to send orders:", orders); - if (error.code === 200) { - // Business Logic Error, not a connection problem - // Hide error if already shown before ... - if ((!this.failed || options.show_error) && !options.to_invoice) { - this.failed = error; - this.set_synch("error"); - throw error; - } - } - this.set_synch("disconnected"); - throw error; - } - } - - // Exports the paid orders (the ones waiting for internet connection) - export_paid_orders() { - return JSON.stringify( - { - paid_orders: this.db.get_orders(), - session: this.pos_session.name, - session_id: this.pos_session.id, - date: new Date().toUTCString(), - version: this.version.server_version_info, - }, - null, - 2 - ); - } - - // Exports the unpaid orders (the tabs) - export_unpaid_orders() { - return JSON.stringify( - { - unpaid_orders: this.db.get_unpaid_orders(), - session: this.pos_session.name, - session_id: this.pos_session.id, - date: new Date().toUTCString(), - version: this.version.server_version_info, - }, - null, - 2 - ); - } - - // This imports paid or unpaid orders from a json file whose - // contents are provided as the string str. - // It returns a report of what could and what could not be - // imported. - import_orders(str) { - var json = JSON.parse(str); - var report = { - // Number of paid orders that were imported - paid: 0, - // Number of unpaid orders that were imported - unpaid: 0, - // Orders that were not imported because they already exist (uid conflict) - unpaid_skipped_existing: 0, - // Orders that were not imported because they belong to another session - unpaid_skipped_session: 0, - // The list of session ids to which skipped orders belong. - unpaid_skipped_sessions: [], - }; - - if (json.paid_orders) { - for (var i = 0; i < json.paid_orders.length; i++) { - this.db.add_order(json.paid_orders[i].data); - } - report.paid = json.paid_orders.length; - this.push_orders(); - } - - if (json.unpaid_orders) { - var orders = []; - var existing = this.get_order_list(); - var existing_uids = {}; - var skipped_sessions = {}; - - for (i = 0; i < existing.length; i++) { - existing_uids[existing[i].uid] = true; - } - - for (i = 0; i < json.unpaid_orders.length; i++) { - var order = json.unpaid_orders[i]; - if (order.pos_session_id !== this.pos_session.id) { - report.unpaid_skipped_session += 1; - skipped_sessions[order.pos_session_id] = true; - } else if (existing_uids[order.uid]) { - report.unpaid_skipped_existing += 1; - } else { - orders.push(this.createReactiveOrder(order)); - } - } - - orders = orders.sort(function (a, b) { - return a.sequence_number - b.sequence_number; - }); - - if (orders.length) { - report.unpaid = orders.length; - this.orders.add(orders); - } - - report.unpaid_skipped_sessions = Object.keys(skipped_sessions); - } - - return report; - } - - _load_orders() { - var jsons = this.db.get_unpaid_orders(); - var orders = []; - var not_loaded_count = 0; - - for (var i = 0; i < jsons.length; i++) { - var json = jsons[i]; - if (json.pos_session_id === this.pos_session.id) { - orders.push(this.createReactiveOrder(json)); - } else { - not_loaded_count += 1; - } - } - - if (not_loaded_count) { - console.info( - "There are " + - not_loaded_count + - " locally saved unpaid orders belonging to another session" - ); - } - - orders = orders.sort(function (a, b) { - return a.sequence_number - b.sequence_number; - }); - - if (orders.length) { - this.orders.add(orders); - } - } - - /** - * Mirror JS method of: - * _compute_amount in addons/account/models/account.py - */ - _compute_all(tax, base_amount, quantity, price_exclude) { - if (price_exclude === undefined) { - var price_include = tax.price_include; - } else { - price_include = !price_exclude; - } - if (tax.amount_type === "fixed") { - // Use sign on base_amount and abs on quantity to take into account the sign of the base amount, - // which includes the sign of the quantity and the sign of the price_unit - // Amount is the fixed price for the tax, it can be negative - // Base amount included the sign of the quantity and the sign of the unit price and when - // a product is returned, it can be done either by changing the sign of quantity or by changing the - // sign of the price unit. - // When the price unit is equal to 0, the sign of the quantity is absorbed in base_amount then - // a "else" case is needed. - if (base_amount) { - return Math.sign(base_amount) * Math.abs(quantity) * tax.amount; - } else { - return quantity * tax.amount; - } - } - if (tax.amount_type === "percent" && !price_include) { - return (base_amount * tax.amount) / 100; - } - if (tax.amount_type === "percent" && price_include) { - return base_amount - base_amount / (1 + tax.amount / 100); - } - if (tax.amount_type === "division" && !price_include) { - return base_amount / (1 - tax.amount / 100) - base_amount; - } - if (tax.amount_type === "division" && price_include) { - return base_amount - base_amount * (tax.amount / 100); - } - return false; - } - - /** - * Mirror JS method of: - * compute_all in addons/account/models/account.py - * - * Read comments in the python side method for more details about each sub-methods. - */ - compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include = true) { - var self = this; - - // 1) Flatten the taxes. - - var _collect_taxes = function (taxes, all_taxes) { - taxes = [...taxes].sort(function (tax1, tax2) { - return tax1.sequence - tax2.sequence; - }); - taxes.forEach((tax) => { - if (tax.amount_type === "group") { - all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes); - } else { - all_taxes.push(tax); - } - }); - return all_taxes; - }; - var collect_taxes = function (taxes) { - return _collect_taxes(taxes, []); - }; - - taxes = collect_taxes(taxes); - - // 2) Deal with the rounding methods - - var round_tax = this.company.tax_calculation_rounding_method != "round_globally"; - - var initial_currency_rounding = currency_rounding; - if (!round_tax) { - currency_rounding = currency_rounding * 0.00001; - } - - // 3) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. - var recompute_base = function (base_amount, fixed_amount, percent_amount, division_amount) { - return ( - (((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)) * - (100 - division_amount)) / - 100 - ); - }; - - var base = round_pr(price_unit * quantity, initial_currency_rounding); - - var sign = 1; - if (base < 0) { - base = -base; - sign = -1; - } - - var total_included_checkpoints = {}; - var i = taxes.length - 1; - var store_included_tax_total = true; - - var incl_fixed_amount = 0.0; - var incl_percent_amount = 0.0; - var incl_division_amount = 0.0; - - var cached_tax_amounts = {}; - if (handle_price_include) { - taxes.reverse().forEach(function (tax) { - if (tax.include_base_amount) { - base = recompute_base( - base, - incl_fixed_amount, - incl_percent_amount, - incl_division_amount - ); - incl_fixed_amount = 0.0; - incl_percent_amount = 0.0; - incl_division_amount = 0.0; - store_included_tax_total = true; - } - if (tax.price_include) { - if (tax.amount_type === "percent") { - incl_percent_amount += tax.amount * tax.sum_repartition_factor; - } else if (tax.amount_type === "division") { - incl_division_amount += tax.amount * tax.sum_repartition_factor; - } else if (tax.amount_type === "fixed") { - incl_fixed_amount += - Math.abs(quantity) * tax.amount * tax.sum_repartition_factor; - } else { - var tax_amount = self._compute_all(tax, base, quantity); - incl_fixed_amount += tax_amount; - cached_tax_amounts[i] = tax_amount; - } - if (store_included_tax_total) { - total_included_checkpoints[i] = base; - store_included_tax_total = false; - } - } - i -= 1; - }); - } - - var total_excluded = round_pr( - recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount), - initial_currency_rounding - ); - var total_included = total_excluded; - - // 4) Iterate the taxes in the sequence order to fill missing base/amount values. - - base = total_excluded; - - var skip_checkpoint = false; - - var taxes_vals = []; - i = 0; - var cumulated_tax_included_amount = 0; - taxes.reverse().forEach(function (tax) { - if (tax.price_include || tax.is_base_affected) { - var tax_base_amount = base; - } else { - tax_base_amount = total_excluded; - } - - if ( - !skip_checkpoint && - tax.price_include && - total_included_checkpoints[i] !== undefined && - tax.sum_repartition_factor != 0 - ) { - var tax_amount = - total_included_checkpoints[i] - (base + cumulated_tax_included_amount); - cumulated_tax_included_amount = 0; - } else { - tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); - } - - tax_amount = round_pr(tax_amount, currency_rounding); - var factorized_tax_amount = round_pr( - tax_amount * tax.sum_repartition_factor, - currency_rounding - ); - - if (tax.price_include && total_included_checkpoints[i] === undefined) { - cumulated_tax_included_amount += factorized_tax_amount; - } - - taxes_vals.push({ - id: tax.id, - name: tax.name, - amount: sign * factorized_tax_amount, - base: sign * round_pr(tax_base_amount, currency_rounding), - }); - - if (tax.include_base_amount) { - base += factorized_tax_amount; - if (!tax.price_include) { - skip_checkpoint = true; - } - } - - total_included += factorized_tax_amount; - i += 1; - }); - - return { - taxes: taxes_vals, - total_excluded: sign * round_pr(total_excluded, this.currency.rounding), - total_included: sign * round_pr(total_included, this.currency.rounding), - }; - } - - /** - * Taxes after fiscal position mapping. - * @param {number[]} taxIds - * @param {object | falsy} fpos - fiscal position - * @returns {object[]} - */ - get_taxes_after_fp(taxIds, fpos) { - if (!fpos) { - return taxIds.map((taxId) => this.taxes_by_id[taxId]); - } - const mappedTaxes = []; - for (const taxId of taxIds) { - const tax = this.taxes_by_id[taxId]; - if (tax) { - const taxMaps = Object.values(fpos.fiscal_position_taxes_by_id).filter( - (fposTax) => fposTax.tax_src_id[0] === tax.id - ); - if (taxMaps.length) { - for (const taxMap of taxMaps) { - if (taxMap.tax_dest_id) { - const mappedTax = this.taxes_by_id[taxMap.tax_dest_id[0]]; - if (mappedTax) { - mappedTaxes.push(mappedTax); - } - } - } - } else { - mappedTaxes.push(tax); - } - } - } - return uniqueBy(mappedTaxes, (tax) => tax.id); - } - - /** - * TODO: We can probably remove this here and put it somewhere else. - * And that somewhere else becomes the parent of the proxy. - * Directly calls the requested service, instead of triggering a - * 'call_service' event up, which wouldn't work as services have no parent - * - * @param {OdooEvent} ev - */ - _trigger_up(ev) { - if (ev.is_stopped()) { - return; - } - const payload = ev.data; - if (ev.name === "call_service") { - let args = payload.args || []; - if (payload.service === "ajax" && payload.method === "rpc") { - // ajax service uses an extra 'target' argument for rpc - args = args.concat(ev.target); - } - const service = this.env.services[payload.service]; - const result = service[payload.method].apply(service, args); - payload.callback(result); - } - } - - isProductQtyZero(qty) { - return floatIsZero(qty, this.dp["Product Unit of Measure"]); - } - - disallowLineQuantityChange() { - return false; - } - - getCurrencySymbol() { - return this.currency ? this.currency.symbol : "$"; - } - /** - * Make the products corresponding to the given ids to be available_in_pos and - * fetch them to be added on the loaded products. - */ - async _addProducts(ids, setAvailable = true) { - if (setAvailable) { - await this.orm.write("product.product", ids, { available_in_pos: true }); - } - const product = await this.orm.call("pos.session", "get_pos_ui_product_product_by_params", [ - odoo.pos_session_id, - { domain: [["id", "in", ids]] }, - ]); - this._loadProductProduct(product); - } - async refreshTotalDueOfPartner(partner) { - const partnerWithUpdatedTotalDue = await this.orm.searchRead( - "res.partner", - [["id", "=", partner.id]], - ["total_due"] - ); - this.db.update_partners(partnerWithUpdatedTotalDue); - return partnerWithUpdatedTotalDue; - } - isOpenOrderShareable() { - return this.config.trusted_config_ids.length > 0; - } - switchPane() { - this.mobile_pane = this.mobile_pane === "left" ? "right" : "left"; - } - async logEmployeeMessage(action, message) { - await this.orm.call("pos.session", "log_partner_message", [ - this.pos_session.id, - this.user.partner_id.id, - action, - message, - ]); - } -} -PosGlobalState.prototype.electronic_payment_interfaces = {}; - -/** - * Call this function to map your PaymentInterface implementation to - * the use_payment_terminal field. When the POS loads it will take - * care of instantiating your interface and setting it on the right - * payment methods. - * - * @param {string} use_payment_terminal - value used in the - * use_payment_terminal selection field - * - * @param {Object} ImplementedPaymentInterface - implemented - * PaymentInterface - */ -export function register_payment_method(use_payment_terminal, ImplementedPaymentInterface) { - PosGlobalState.prototype.electronic_payment_interfaces[use_payment_terminal] = - ImplementedPaymentInterface; -} - export class Product extends PosModel { constructor(obj) { super(obj); diff --git a/addons/point_of_sale/static/src/app/store/pos_store.js b/addons/point_of_sale/static/src/app/store/pos_store.js index fa73bc50b0d..efc06b43d56 100644 --- a/addons/point_of_sale/static/src/app/store/pos_store.js +++ b/addons/point_of_sale/static/src/app/store/pos_store.js @@ -1,16 +1,71 @@ /** @odoo-module */ +/* global waitForWebfonts */ -import { PosGlobalState } from "@point_of_sale/app/store/models"; - +import { PosCollection, Order, Product } from "@point_of_sale/app/store/models"; +import { Mutex } from "@web/core/utils/concurrency"; +import { PosDB } from "@point_of_sale/app/store/db"; +import { markRaw, reactive } from "@odoo/owl"; +import { roundPrecision as round_pr, floatIsZero } from "@web/core/utils/numbers"; import { registry } from "@web/core/registry"; import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup"; +import { batched, deduceUrl } from "@point_of_sale/utils"; import { Reactive } from "@web/core/utils/reactive"; +import { HWPrinter } from "@point_of_sale/app/printer/hw_printer"; +import { memoize } from "@web/core/utils/functions"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; import { _t } from "@web/core/l10n/translation"; import { CashOpeningPopup } from "@point_of_sale/app/store/cash_opening_popup/cash_opening_popup"; import { sprintf } from "@web/core/utils/strings"; import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; +import { renderToString } from "@web/core/utils/render"; + +/* Returns an array containing all elements of the given + * array corresponding to the rule function {agg} and without duplicates + * + * @template T + * @template F + * @param {T[]} array + * @param {F} function + * @returns {T[]} + */ +export function uniqueBy(array, agg) { + const map = new Map(); + for (const item of array) { + const key = agg(item); + if (!map.has(key)) { + map.set(key, item); + } + } + return [...map.values()]; +} + +/** + * Gets a product image as a base64 string so that it can be sent to the + * customer display, as the display won't be able to fetch it, since the image + * controller requires the client to be logged. This function is memoized on the + * product id, so that we will only do this once per product. + * + * @param {number} productId id of the product + * @param {string} writeDate the write date of the product, used as a cache + * buster in case the product image has been changed + * @returns {string} the base64 representation of the product's image + */ +const getProductImage = memoize(function getProductImage(productId, writeDate) { + return new Promise(function (resolve, reject) { + const img = new Image(); + img.addEventListener("load", () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.height = img.height; + canvas.width = img.width; + ctx.drawImage(img, 0, 0); + resolve(canvas.toDataURL("image/jpeg")); + }); + img.addEventListener("error", reject); + img.src = `/web/image?model=product.product&field=image_128&id=${productId}&unique=${writeDate}`; + }); +}); export class PosStore extends Reactive { hasBigScrollBars = false; @@ -31,17 +86,87 @@ export class PosStore extends Reactive { } // use setup instead of constructor because setup can be patched. async setup(env, { popup, orm, number_buffer, hardware_proxy, barcode_reader }) { + this.env = env; this.orm = orm; this.popup = popup; this.numberBuffer = number_buffer; this.barcodeReader = barcode_reader; - this.globalState = new PosGlobalState({ orm, env, hardwareProxy: hardware_proxy }); + + this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders + this.unwatched = markRaw({}); + this.pushOrderMutex = new Mutex(); + + // Business data; loaded from the server at launch + this.company_logo = null; + this.company_logo_base64 = ""; + this.currency = null; + this.company = null; + this.user = null; + this.partners = []; + this.taxes = []; + this.pos_session = null; + this.config = null; + this.units = []; + this.units_by_id = {}; + this.uom_unit_id = null; + this.default_pricelist = null; + this.order_sequence = 1; + this.printers_category_ids_set = new Set(); + + // Object mapping the order's name (which contains the uid) to it's server_id after + // validation (order paid then sent to the backend). + this.validated_orders_name_server_id_map = {}; + + this.numpadMode = "quantity"; + this.mobile_pane = "right"; + + // Record + this.toRefundLines = {}; + this.TICKET_SCREEN_STATE = { + syncedOrders: { + currentPage: 1, + cache: {}, + toShow: [], + nPerPage: 80, + totalCount: null, + cacheDate: null, + }, + ui: { + selectedSyncedOrderId: null, + searchDetails: this.getDefaultSearchDetails(), + filter: null, + // maps the order's backendId to it's selected orderline + selectedOrderlineIds: {}, + highlightHeaderNote: false, + }, + }; + + this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing + this.loadingOrderState = false; // used to prevent orders fetched to be put in the update set during the reactive change + this.showOfflineWarning = true; // Allows to avoid the display of the offline popup when the user has already had it. + + // these dynamic attributes can be watched for change by other models or widgets + Object.assign(this, { + synch: { status: "connected", pending: 0 }, + orders: new PosCollection(), + selectedOrder: null, + selectedPartner: null, + selectedCategoryId: null, + // FIXME POSREF this piece of state should probably be private to the product screen + // but it currently needs to be available to the ProductInfo screen for dubious functional reasons + searchProductWord: "", + }); + + this.ready = new Promise((resolve) => { + this.markReady = resolve; + }); + this.hardwareProxy = hardware_proxy; // FIXME POSREF: the hardwareProxy needs the pos and the pos needs the hardwareProxy. Maybe // the hardware proxy should just be part of the pos service? - this.hardwareProxy.pos = this.globalState; - await this.globalState.load_server_data(); - if (this.globalState.config.use_proxy) { + this.hardwareProxy.pos = this; + await this.load_server_data(); + if (this.config.use_proxy) { await this.connectToProxy(); } this.closeOtherTabs(); @@ -49,18 +174,1365 @@ export class PosStore extends Reactive { this.showScreen("ProductScreen"); } + getDefaultSearchDetails() { + return { + fieldName: "RECEIPT_NUMBER", + searchTerm: "", + }; + } + async load_product_uom_unit() { + const uom_id = await this.orm.call("ir.model.data", "check_object_reference", [ + "uom", + "product_uom_unit", + ]); + this.uom_unit_id = uom_id[1]; + } + + async after_load_server_data() { + await this.load_product_uom_unit(); + await this.load_orders(); + this.set_start_order(); + Object.assign(this.toRefundLines, this.db.load("TO_REFUND_LINES") || {}); + window.addEventListener("beforeunload", () => + this.db.save("TO_REFUND_LINES", this.toRefundLines) + ); + const { start_category, iface_start_categ_id } = this.config; + this.selectedCategoryId = (start_category && iface_start_categ_id?.[0]) || 0; + this.hasBigScrollBars = this.config.iface_big_scrollbars; + // Push orders in background, do not await + this.push_orders(); + this.markReady(); + } + + async load_server_data() { + const loadedData = await this.orm.silent.call("pos.session", "load_pos_data", [ + [odoo.pos_session_id], + ]); + await this._processData(loadedData); + return this.after_load_server_data(); + } + async _processData(loadedData) { + this.version = loadedData["version"]; + this.company = loadedData["res.company"]; + this.dp = loadedData["decimal.precision"]; + this.units = loadedData["uom.uom"]; + this.units_by_id = loadedData["units_by_id"]; + this.states = loadedData["res.country.state"]; + this.countries = loadedData["res.country"]; + this.langs = loadedData["res.lang"]; + this.taxes = loadedData["account.tax"]; + this.taxes_by_id = loadedData["taxes_by_id"]; + this.pos_session = loadedData["pos.session"]; + this._loadPosSession(); + this.config = loadedData["pos.config"]; + this._loadPoSConfig(); + this.bills = loadedData["pos.bill"]; + this.partners = loadedData["res.partner"]; + this.addPartners(this.partners); + this.picking_type = loadedData["stock.picking.type"]; + this.user = loadedData["res.users"]; + this.pricelists = loadedData["product.pricelist"]; + this.default_pricelist = loadedData["default_pricelist"]; + this.currency = loadedData["res.currency"]; + this.db.add_categories(loadedData["pos.category"]); + this._loadProductProduct(loadedData["product.product"]); + this.db.add_packagings(loadedData["product.packaging"]); + this.attributes_by_ptal_id = loadedData["attributes_by_ptal_id"]; + this.cash_rounding = loadedData["account.cash.rounding"]; + this.payment_methods = loadedData["pos.payment.method"]; + this._loadPosPaymentMethod(); + this.fiscal_positions = loadedData["account.fiscal.position"]; + this.base_url = loadedData["base_url"]; + await this._loadFonts(); + await this._loadPictures(); + await this._loadPosPrinters(loadedData["pos.printer"]); + } + _loadPosSession() { + // We need to do it here, since only then the local storage has the correct uuid + this.db.save("pos_session_id", this.pos_session.id); + const orders = this.db.get_orders(); + const sequences = orders.map((order) => order.data.sequence_number + 1); + this.pos_session.sequence_number = Math.max(this.pos_session.sequence_number, ...sequences); + this.pos_session.login_number = odoo.login_number; + } + _loadPosPrinters(printers) { + this.unwatched.printers = []; + // list of product categories that belong to one or more order printer + for (const printerConfig of printers) { + const printer = this.create_printer(printerConfig); + printer.config = printerConfig; + this.unwatched.printers.push(printer); + for (const id of printer.config.product_categories_ids) { + this.printers_category_ids_set.add(id); + } + } + this.config.iface_printers = !!this.unwatched.printers.length; + } + create_printer(config) { + const url = deduceUrl(config.proxy_ip || ""); + return new HWPrinter({ rpc: this.env.services.rpc, url }); + } + _loadPoSConfig() { + this.db.set_uuid(this.config.uuid); + } + addPartners(partners) { + return this.db.add_partners(partners); + } + _assignApplicableItems(pricelist, correspondingProduct, pricelistItem) { + if (!(pricelist.id in correspondingProduct.applicablePricelistItems)) { + correspondingProduct.applicablePricelistItems[pricelist.id] = []; + } + correspondingProduct.applicablePricelistItems[pricelist.id].push(pricelistItem); + } + _loadProductProduct(products) { + const productMap = {}; + const productTemplateMap = {}; + + const modelProducts = products.map((product) => { + product.pos = this; + product.applicablePricelistItems = {}; + productMap[product.id] = product; + productTemplateMap[product.product_tmpl_id[0]] = ( + productTemplateMap[product.product_tmpl_id[0]] || [] + ).concat(product); + return new Product(product); + }); + + for (const pricelist of this.pricelists) { + for (const pricelistItem of pricelist.items) { + if (pricelistItem.product_id) { + const product_id = pricelistItem.product_id[0]; + const correspondingProduct = productMap[product_id]; + if (correspondingProduct) { + this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); + } + } else if (pricelistItem.product_tmpl_id) { + const product_tmpl_id = pricelistItem.product_tmpl_id[0]; + const correspondingProducts = productTemplateMap[product_tmpl_id]; + for (const correspondingProduct of correspondingProducts || []) { + this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); + } + } else { + for (const correspondingProduct of products) { + this._assignApplicableItems(pricelist, correspondingProduct, pricelistItem); + } + } + } + } + this.db.add_products(modelProducts); + } + _loadPosPaymentMethod() { + // need to do this for pos_iot due to reference, this is a temporary fix + this.payment_methods_by_id = {}; + for (const pm of this.payment_methods) { + this.payment_methods_by_id[pm.id] = pm; + const PaymentInterface = this.electronic_payment_interfaces[pm.use_payment_terminal]; + if (PaymentInterface) { + pm.payment_terminal = new PaymentInterface(this, pm); + } + } + } + async _loadFonts() { + return new Promise(function (resolve, reject) { + // Waiting for fonts to be loaded to prevent receipt printing + // from printing empty receipt while loading Inconsolata + // ( The font used for the receipt ) + waitForWebfonts(["Lato", "Inconsolata"], function () { + resolve(); + }); + // The JS used to detect font loading is not 100% robust, so + // do not wait more than 5sec + setTimeout(resolve, 5000); + }); + } + async _loadPictures() { + this.company_logo = new Image(); + return new Promise((resolve, reject) => { + this.company_logo.onload = () => { + const img = this.company_logo; + let ratio = 1; + const targetwidth = 300; + const maxheight = 150; + if (img.width !== targetwidth) { + ratio = targetwidth / img.width; + } + if (img.height * ratio > maxheight) { + ratio = maxheight / img.height; + } + const width = Math.floor(img.width * ratio); + const height = Math.floor(img.height * ratio); + const c = document.createElement("canvas"); + c.width = width; + c.height = height; + const ctx = c.getContext("2d"); + ctx.drawImage(this.company_logo, 0, 0, width, height); + + this.company_logo_base64 = c.toDataURL(); + resolve(); + }; + this.company_logo.onerror = () => { + reject(); + }; + this.company_logo.crossOrigin = "anonymous"; + this.company_logo.src = `/web/image?model=res.company&id=${this.company.id}&field=logo`; + }); + } + prepare_new_partners_domain() { + return [["write_date", ">", this.db.get_partner_write_date()]]; + } + + // reload the list of partner, returns as a promise that resolves if there were + // updated partners, and fails if not + async load_new_partners() { + const search_params = { domain: this.prepare_new_partners_domain() }; + // FIXME POSREF TIMEOUT 3000 + const partners = await this.orm.silent.call( + "pos.session", + "get_pos_ui_res_partner_by_params", + [[odoo.pos_session_id], search_params] + ); + return this.addPartners(partners); + } + + setSelectedCategoryId(categoryId) { + this.selectedCategoryId = categoryId; + } + + /** + * Remove the order passed in params from the list of orders + * @param order + */ + removeOrder(order, removeFromServer = true) { + this.orders.remove(order); + this.db.remove_unpaid_order(order); + for (const line of order.get_orderlines()) { + if (line.refunded_orderline_id) { + delete this.toRefundLines[line.refunded_orderline_id]; + } + } + if (this.isOpenOrderShareable() && removeFromServer) { + if (this.ordersToUpdateSet.has(order)) { + this.ordersToUpdateSet.delete(order); + } + if (order.server_id && !order.finalized) { + this.setOrderToRemove(order); + } + } + } + setOrderToRemove(order) { + this.db.set_order_to_remove_from_server(order); + } + + /** + * Return the current cashier (in this case, the user) + * @returns {name: string, id: int, role: string} + */ + get_cashier() { + return this.user; + } + get_cashier_user_id() { + return this.user.id; + } + get orderPreparationCategories() { + if (this.printers_category_ids_set) { + return new Set([...this.printers_category_ids_set]); + } + return new Set(); + } + cashierHasPriceControlRights() { + return !this.config.restrict_price_control || this.get_cashier().role == "manager"; + } + _onReactiveOrderUpdated(order) { + order.save_to_db(); + if (this.isOpenOrderShareable() && !this.loadingOrderState) { + this.ordersToUpdateSet.add(order); + } + } + createReactiveOrder(json) { + const options = { pos: this }; + if (json) { + options.json = json; + } + return this.makeOrderReactive(new Order({}, options)); + } + makeOrderReactive(order) { + const batchedCallback = batched(() => { + this._onReactiveOrderUpdated(order); + }); + order = reactive(order, batchedCallback); + order.save_to_db(); + return order; + } + // creates a new empty order and sets it as the current order + add_new_order() { + if (this.isOpenOrderShareable()) { + this.sendDraftToServer(); + } + if (this.selectedOrder) { + this.selectedOrder.firstDraft = false; + this.selectedOrder.updateSavedQuantity(); + } + const order = this.createReactiveOrder(); + this.orders.add(order); + this.selectedOrder = order; + return order; + } + selectNextOrder() { + if (this.orders.length > 0) { + this.selectedOrder = this.orders[0]; + } else { + this.add_new_order(); + } + } + async sendDraftToServer() { + const ordersUidsToSync = [...this.ordersToUpdateSet].map((order) => order.uid); + const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); + const ordersResponse = await this._save_to_server(ordersToSync, { draft: true }); + const orders = [...this.ordersToUpdateSet].map((order) => order); + ordersResponse.forEach((orderResponseData) => this._updateOrder(orderResponseData, orders)); + this.ordersToUpdateSet.clear(); + } + addOrderToUpdateSet() { + this.ordersToUpdateSet.add(this.selectedOrder); + } + // created this hook for modularity + _updateOrder(ordersResponseData, orders) { + const order = orders.find((order) => order.name === ordersResponseData.pos_reference); + if (order) { + order.server_id = ordersResponseData.id; + return order; + } + } + /** + * Load the locally saved unpaid orders for this PoS Config. + * + * First load all orders belonging to the current session. + * Second load all orders belonging to the same config but from other sessions, + * Only if tho order has orderlines. + */ + async load_orders() { + this.loadingOrderState = true; + var jsons = this.db.get_unpaid_orders(); + await this._loadMissingProducts(jsons); + await this._loadMissingPartners(jsons); + var orders = []; + + for (var i = 0; i < jsons.length; i++) { + var json = jsons[i]; + if (json.pos_session_id === this.pos_session.id) { + orders.push(this.createReactiveOrder(json)); + } + } + for (i = 0; i < jsons.length; i++) { + json = jsons[i]; + if ( + json.pos_session_id !== this.pos_session.id && + (json.lines.length > 0 || json.statement_ids.length > 0) + ) { + orders.push(this.createReactiveOrder(json)); + } else if (json.pos_session_id !== this.pos_session.id) { + this.db.remove_unpaid_order(jsons[i]); + } + } + + orders = orders.sort(function (a, b) { + return a.sequence_number - b.sequence_number; + }); + + if (orders.length) { + for (const order of orders) { + this.orders.add(order); + } + } + this.loadingOrderState = false; + } + async _loadMissingProducts(orders) { + const missingProductIds = new Set([]); + for (const order of orders) { + for (const line of order.lines) { + const productId = line[2].product_id; + if (missingProductIds.has(productId)) { + continue; + } + if (!this.db.get_product_by_id(productId)) { + missingProductIds.add(productId); + } + } + } + const products = await this.orm.call( + "pos.session", + "get_pos_ui_product_product_by_params", + [odoo.pos_session_id, { domain: [["id", "in", [...missingProductIds]]] }] + ); + this._loadProductProduct(products); + } + // load the partners based on the ids + async _loadPartners(partnerIds) { + if (partnerIds.length > 0) { + // FIXME POSREF TIMEOUT + const fetchedPartners = await this.orm.silent.call( + "pos.session", + "get_pos_ui_res_partner_by_params", + [[odoo.pos_session_id], { domain: [["id", "in", partnerIds]] }] + ); + this.addPartners(fetchedPartners); + } + } + async _loadMissingPartners(orders) { + const missingPartnerIds = new Set([]); + for (const order of orders) { + const partnerId = order.partner_id; + if (missingPartnerIds.has(partnerId)) { + continue; + } + if (partnerId && !this.db.get_partner_by_id(partnerId)) { + missingPartnerIds.add(partnerId); + } + } + await this._loadPartners([...missingPartnerIds]); + } + setLoadingOrderState(bool) { + this.loadingOrderState = bool; + } + async _removeOrdersFromServer() { + const removedOrdersIds = this.db.get_ids_to_remove_from_server(); + if (removedOrdersIds.length === 0) { + return; + } + + this.set_synch("connecting", removedOrdersIds.length); + try { + const removeOrdersResponseData = await this.orm.silent.call( + "pos.order", + "remove_from_ui", + [removedOrdersIds] + ); + this.set_synch("connected"); + this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData); + } catch (reason) { + const error = reason.message; + if (error.code === 200) { + // Business Logic Error, not a connection problem + //if warning do not need to display traceback!! + if (error.data.exception_type == "warning") { + delete error.data.debug; + } + } + // important to throw error here and let the rendering component handle the error + console.warn("Failed to remove orders:", removedOrdersIds); + this._postRemoveFromServer(removedOrdersIds); + throw error; + } + } + _postRemoveFromServer(serverIds, data) { + this.db.set_ids_removed_from_server(serverIds); + } + _replaceOrders(ordersToReplace, newOrdersJsons) { + ordersToReplace.forEach((order) => { + // We don't remove the validated orders because we still want to see them in the ticket screen. + // Orders in 'ReceiptScreen' or 'TipScreen' are validated orders. + if (this._shouldRemoveOrder(order)) { + this.removeOrder(order, false); + } + }); + let removeSelected = true; + newOrdersJsons.forEach((json) => { + const isSelectedOrder = this._createOrder(json); + if (removeSelected && isSelectedOrder) { + removeSelected = false; + } + }); + if (this._shouldRemoveSelectedOrder(removeSelected)) { + this._removeSelectedOrder(); + } + } + _shouldRemoveOrder(order) { + return ( + (!this.selectedOrder || this.selectedOrder.uid != order.uid) && + order.server_id && + !order.finalized + ); + } + _shouldRemoveSelectedOrder(removeSelected) { + return removeSelected && this.selectedOrder.server_id && !this.selectedOrder.finalized; + } + _shouldCreateOrder(json) { + return json.uid != this.selectedOrder.uid; + } + _isSelectedOrder(json) { + return json.uid == this.selectedOrder.uid; + } + _createOrder(json) { + if (this._shouldCreateOrder(json)) { + const order = this.createReactiveOrder(json); + this.orders.add(order); + } + return this._isSelectedOrder(json); + } + _removeSelectedOrder() { + this.removeOrder(this.selectedOrder, false); + const orderList = this.get_order_list(); + if (orderList.length != 0) { + this.set_order(orderList[0]); + } + } + async _syncAllOrdersFromServer() { + await this._removeOrdersFromServer(); + const ordersJson = await this._getOrdersJson(); + let message = null; + message = await this._addPricelists(ordersJson); + let messageFp = null; + messageFp = await this._addFiscalPositions(ordersJson); + if (messageFp) { + if (message) { + message += "\n" + messageFp; + } else { + message = messageFp; + } + } + const allOrders = [...this.get_order_list()]; + this._replaceOrders(allOrders, ordersJson); + this.sortOrders(); + return message; + } + async _getOrdersJson() { + return await this.orm.call("pos.order", "get_draft_share_order_ids", [], { + config_id: this.config.id, + }); + } + async _addPricelists(ordersJson) { + const pricelistsToGet = []; + ordersJson.forEach((order) => { + let found = false; + for (const pricelist of this.pricelists) { + if (pricelist.id === order.pricelist_id) { + found = true; + break; + } + } + if (!found && order.pricelist_id) { + pricelistsToGet.push(order.pricelist_id); + } + }); + let message = null; + if (pricelistsToGet.length > 0) { + const pricelistsJson = await this._getPricelistJson(pricelistsToGet); + message = this._addPosPricelists(pricelistsJson); + } + return message; + } + async _getPricelistJson(pricelistsToGet) { + return await this.env.services.orm.call( + "pos.session", + "get_pos_ui_product_pricelists_by_ids", + [[odoo.pos_session_id], pricelistsToGet] + ); + } + _addPosPricelists(pricelistsJson) { + if (!this.config.use_pricelist) { + this.config.use_pricelist = true; + } + this.pricelists.push(...pricelistsJson); + let message = ""; + const pricelistsNames = pricelistsJson.map((pricelist) => { + return pricelist.display_name; + }); + message = sprintf( + _t("%s fiscal position(s) added to the configuration."), + pricelistsNames.join(", ") + ); + return message; + } + async _addFiscalPositions(ordersJson) { + const fiscalPositionToGet = []; + ordersJson.forEach((order) => { + let found = false; + for (const fp of this.fiscal_positions) { + if (fp.id === order.fiscal_position_id) { + found = true; + break; + } + } + if (!found && order.fiscal_position_id) { + fiscalPositionToGet.push(order.fiscal_position_id); + } + }); + let message = null; + if (fiscalPositionToGet.length > 0) { + const fiscalPositionJson = await this._getFiscalPositionJson(fiscalPositionToGet); + message = this._addPosFiscalPosition(fiscalPositionJson); + } + return message; + } + async _getFiscalPositionJson(fiscalPositionToGet) { + return await this.env.services.orm.call( + "pos.session", + "get_pos_ui_account_fiscal_positions_by_ids", + [[odoo.pos_session_id], fiscalPositionToGet] + ); + } + _addPosFiscalPosition(fiscalPositionJson) { + this.fiscal_positions.push(...fiscalPositionJson); + let message = ""; + const fiscalPositionNames = fiscalPositionJson.map((fp) => { + return fp.display_name; + }); + message = sprintf( + _t("%s fiscal position(s) added to the configuration."), + fiscalPositionNames.join(", ") + ); + return message; + } + sortOrders() { + this.orders.sort((a, b) => (a.name > b.name ? 1 : -1)); + } + async getProductInfo(product, quantity) { + const order = this.get_order(); + // check back-end method `get_product_info_pos` to see what it returns + // We do this so it's easier to override the value returned and use it in the component template later + const productInfo = await this.orm.call("product.product", "get_product_info_pos", [ + [product.id], + product.get_price(order.pricelist, quantity), + quantity, + this.config.id, + ]); + + const priceWithoutTax = productInfo["all_prices"]["price_without_tax"]; + const margin = priceWithoutTax - product.standard_price; + const orderPriceWithoutTax = order.get_total_without_tax(); + const orderCost = order.get_total_cost(); + const orderMargin = orderPriceWithoutTax - orderCost; + + const costCurrency = this.env.utils.formatCurrency(product.standard_price); + const marginCurrency = this.env.utils.formatCurrency(margin); + const marginPercent = priceWithoutTax + ? Math.round((margin / priceWithoutTax) * 10000) / 100 + : 0; + const orderPriceWithoutTaxCurrency = this.env.utils.formatCurrency(orderPriceWithoutTax); + const orderCostCurrency = this.env.utils.formatCurrency(orderCost); + const orderMarginCurrency = this.env.utils.formatCurrency(orderMargin); + const orderMarginPercent = orderPriceWithoutTax + ? Math.round((orderMargin / orderPriceWithoutTax) * 10000) / 100 + : 0; + return { + costCurrency, + marginCurrency, + marginPercent, + orderPriceWithoutTaxCurrency, + orderCostCurrency, + orderMarginCurrency, + orderMarginPercent, + productInfo, + }; + } + async getClosePosInfo() { + const closingData = await this.orm.call("pos.session", "get_closing_control_data", [ + [this.pos_session.id], + ]); + const ordersDetails = closingData.orders_details; + const paymentsAmount = closingData.payments_amount; + const payLaterAmount = closingData.pay_later_amount; + const openingNotes = closingData.opening_notes; + const defaultCashDetails = closingData.default_cash_details; + const otherPaymentMethods = closingData.other_payment_methods; + const isManager = closingData.is_manager; + const amountAuthorizedDiff = closingData.amount_authorized_diff; + const cashControl = this.config.cash_control; + + // component state and refs definition + const state = { notes: "", acceptClosing: false, payments: {} }; + if (cashControl) { + state.payments[defaultCashDetails.id] = { + counted: 0, + difference: -defaultCashDetails.amount, + number: 0, + }; + } + if (otherPaymentMethods.length > 0) { + otherPaymentMethods.forEach((pm) => { + if (pm.type === "bank") { + state.payments[pm.id] = { + counted: this.env.utils + .roundCurrency(pm.amount) + .toFixed(this.currency.decimal_places), + difference: 0, + number: pm.number, + }; + } + }); + } + return { + ordersDetails, + paymentsAmount, + payLaterAmount, + openingNotes, + defaultCashDetails, + otherPaymentMethods, + isManager, + amountAuthorizedDiff, + state, + cashControl, + }; + } + set_start_order() { + if (this.orders.length && !this.selectedOrder) { + this.selectedOrder = this.orders[0]; + if (this.isOpenOrderShareable()) { + this.ordersToUpdateSet.add(this.orders[0]); + } + } else { + this.add_new_order(); + } + } + + // return the current order + get_order() { + return this.selectedOrder; + } + + // change the current order + set_order(order, options) { + if (this.selectedOrder) { + this.selectedOrder.firstDraft = false; + this.selectedOrder.updateSavedQuantity(); + } + this.selectedOrder = order; + } + + // return the list of unpaid orders + get_order_list() { + return this.orders; + } + + /** + * Renders the HTML for the customer display and returns it as a string. + * + * @returns {string} + */ + async customerDisplayHTML(closeUI = false) { + const backgroundImageBase64 = + this.config.iface_customer_facing_display_background_image_1920; + let backgroundImageCSSValue; + if (backgroundImageBase64) { + backgroundImageCSSValue = "url('data:image/png;base64," + backgroundImageBase64 + "')"; + } else { + backgroundImageCSSValue = "none"; + } + + const order = this.get_order(); + if (closeUI || !order) { + return renderToString("point_of_sale.CustomerFacingDisplayNoOrder", { + pos: this, + origin: window.location.origin, + backgroundImageCSSValue, + }); + } + + const orderLines = order.get_orderlines(); + const productImages = Object.fromEntries( + await Promise.all( + orderLines.map(async ({ product }) => [ + product.id, + await getProductImage(product.id, product.writeDate), + ]) + ) + ); + + return renderToString("point_of_sale.CustomerFacingDisplayOrder", { + pos: this, + formatCurrency: this.env.utils.formatCurrency, + origin: window.location.origin, + backgroundImageCSSValue, + order, + productImages, + }); + } + + push_orders(opts = {}) { + return this.pushOrderMutex.exec(() => this._flush_orders(this.db.get_orders(), opts)); + } + + push_single_order(order) { + const order_id = this.db.add_order(order.export_as_JSON()); + return this.pushOrderMutex.exec(() => this._flush_orders([this.db.get_order(order_id)])); + } + + // Send validated orders to the backend. + // Resolves to the backend ids of the synced orders. + _flush_orders(orders, options) { + var self = this; + + return this._save_to_server(orders, options) + .then(function (server_ids) { + for (let i = 0; i < server_ids.length; i++) { + self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = + server_ids[i].id; + } + return server_ids; + }) + .finally(function () { + self._after_flush_orders(orders); + }); + } + /** + * Hook method after _flush_orders resolved or rejected. + * It aims to: + * - remove the refund orderlines from toRefundLines + * - invalidate cache of refunded synced orders + */ + _after_flush_orders(orders) { + const refundedOrderIds = new Set(); + for (const order of orders) { + for (const line of order.data.lines) { + const refundDetail = this.toRefundLines[line[2].refunded_orderline_id]; + if (!refundDetail) { + continue; + } + // Collect the backend id of the refunded orders. + refundedOrderIds.add(refundDetail.orderline.orderBackendId); + // Reset the refund detail for the orderline. + delete this.toRefundLines[refundDetail.orderline.id]; + } + } + this._invalidateSyncedOrdersCache([...refundedOrderIds]); + } + _invalidateSyncedOrdersCache(ids) { + for (const id of ids) { + delete this.TICKET_SCREEN_STATE.syncedOrders.cache[id]; + } + } + set_synch(status, pending) { + if (["connected", "connecting", "error", "disconnected"].indexOf(status) === -1) { + console.error(status, " is not a known connection state."); + } else if (status === "connected") { + this.showOfflineWarning = true; + } + + pending = + pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length; + this.synch = { status, pending }; + } + + // send an array of orders to the server + // available options: + // - timeout: timeout for the rpc call in ms + // returns a promise that resolves with the list of + // server generated ids for the sent orders + async _save_to_server(orders, options) { + if (!orders || !orders.length) { + return Promise.resolve([]); + } + this.set_synch("connecting", orders.length); + options = options || {}; + + // Keep the order ids that are about to be sent to the + // backend. In between create_from_ui and the success callback + // new orders may have been added to it. + var order_ids_to_sync = orders.map((o) => o.id); + + for (const order of orders) { + order.to_invoice = options.to_invoice || false; + } + // we try to send the order. silent prevents a spinner if it takes too long. (unless we are sending an invoice, + // then we want to notify the user that we are waiting on something ) + const orm = options.to_invoice ? this.orm : this.orm.silent; + + try { + // FIXME POSREF timeout + // const timeout = typeof options.timeout === "number" ? options.timeout : 30000 * orders.length; + const serverIds = await orm.call("pos.order", "create_from_ui", [ + orders, + options.draft || false, + ]); + + for (const serverId of serverIds) { + const order = this.env.services.pos.orders.find( + (order) => order.name === serverId.pos_reference + ); + + if (order) { + order.server_id = serverId.id; + } + } + + for (const order_id of order_ids_to_sync) { + this.db.remove_order(order_id); + } + + this.failed = false; + this.set_synch("connected"); + return serverIds; + } catch (error) { + console.warn("Failed to send orders:", orders); + if (error.code === 200) { + // Business Logic Error, not a connection problem + // Hide error if already shown before ... + if ((!this.failed || options.show_error) && !options.to_invoice) { + this.failed = error; + this.set_synch("error"); + throw error; + } + } + this.set_synch("disconnected"); + throw error; + } + } + + // Exports the paid orders (the ones waiting for internet connection) + export_paid_orders() { + return JSON.stringify( + { + paid_orders: this.db.get_orders(), + session: this.pos_session.name, + session_id: this.pos_session.id, + date: new Date().toUTCString(), + version: this.version.server_version_info, + }, + null, + 2 + ); + } + + // Exports the unpaid orders (the tabs) + export_unpaid_orders() { + return JSON.stringify( + { + unpaid_orders: this.db.get_unpaid_orders(), + session: this.pos_session.name, + session_id: this.pos_session.id, + date: new Date().toUTCString(), + version: this.version.server_version_info, + }, + null, + 2 + ); + } + + // This imports paid or unpaid orders from a json file whose + // contents are provided as the string str. + // It returns a report of what could and what could not be + // imported. + import_orders(str) { + var json = JSON.parse(str); + var report = { + // Number of paid orders that were imported + paid: 0, + // Number of unpaid orders that were imported + unpaid: 0, + // Orders that were not imported because they already exist (uid conflict) + unpaid_skipped_existing: 0, + // Orders that were not imported because they belong to another session + unpaid_skipped_session: 0, + // The list of session ids to which skipped orders belong. + unpaid_skipped_sessions: [], + }; + + if (json.paid_orders) { + for (var i = 0; i < json.paid_orders.length; i++) { + this.db.add_order(json.paid_orders[i].data); + } + report.paid = json.paid_orders.length; + this.push_orders(); + } + + if (json.unpaid_orders) { + var orders = []; + var existing = this.get_order_list(); + var existing_uids = {}; + var skipped_sessions = {}; + + for (i = 0; i < existing.length; i++) { + existing_uids[existing[i].uid] = true; + } + + for (i = 0; i < json.unpaid_orders.length; i++) { + var order = json.unpaid_orders[i]; + if (order.pos_session_id !== this.pos_session.id) { + report.unpaid_skipped_session += 1; + skipped_sessions[order.pos_session_id] = true; + } else if (existing_uids[order.uid]) { + report.unpaid_skipped_existing += 1; + } else { + orders.push(this.createReactiveOrder(order)); + } + } + + orders = orders.sort(function (a, b) { + return a.sequence_number - b.sequence_number; + }); + + if (orders.length) { + report.unpaid = orders.length; + this.orders.add(orders); + } + + report.unpaid_skipped_sessions = Object.keys(skipped_sessions); + } + + return report; + } + + _load_orders() { + var jsons = this.db.get_unpaid_orders(); + var orders = []; + var not_loaded_count = 0; + + for (var i = 0; i < jsons.length; i++) { + var json = jsons[i]; + if (json.pos_session_id === this.pos_session.id) { + orders.push(this.createReactiveOrder(json)); + } else { + not_loaded_count += 1; + } + } + + if (not_loaded_count) { + console.info( + "There are " + + not_loaded_count + + " locally saved unpaid orders belonging to another session" + ); + } + + orders = orders.sort(function (a, b) { + return a.sequence_number - b.sequence_number; + }); + + if (orders.length) { + this.orders.add(orders); + } + } + + /** + * Mirror JS method of: + * _compute_amount in addons/account/models/account.py + */ + _compute_all(tax, base_amount, quantity, price_exclude) { + if (price_exclude === undefined) { + var price_include = tax.price_include; + } else { + price_include = !price_exclude; + } + if (tax.amount_type === "fixed") { + // Use sign on base_amount and abs on quantity to take into account the sign of the base amount, + // which includes the sign of the quantity and the sign of the price_unit + // Amount is the fixed price for the tax, it can be negative + // Base amount included the sign of the quantity and the sign of the unit price and when + // a product is returned, it can be done either by changing the sign of quantity or by changing the + // sign of the price unit. + // When the price unit is equal to 0, the sign of the quantity is absorbed in base_amount then + // a "else" case is needed. + if (base_amount) { + return Math.sign(base_amount) * Math.abs(quantity) * tax.amount; + } else { + return quantity * tax.amount; + } + } + if (tax.amount_type === "percent" && !price_include) { + return (base_amount * tax.amount) / 100; + } + if (tax.amount_type === "percent" && price_include) { + return base_amount - base_amount / (1 + tax.amount / 100); + } + if (tax.amount_type === "division" && !price_include) { + return base_amount / (1 - tax.amount / 100) - base_amount; + } + if (tax.amount_type === "division" && price_include) { + return base_amount - base_amount * (tax.amount / 100); + } + return false; + } + + /** + * Mirror JS method of: + * compute_all in addons/account/models/account.py + * + * Read comments in the python side method for more details about each sub-methods. + */ + compute_all(taxes, price_unit, quantity, currency_rounding, handle_price_include = true) { + var self = this; + + // 1) Flatten the taxes. + + var _collect_taxes = function (taxes, all_taxes) { + taxes = [...taxes].sort(function (tax1, tax2) { + return tax1.sequence - tax2.sequence; + }); + taxes.forEach((tax) => { + if (tax.amount_type === "group") { + all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes); + } else { + all_taxes.push(tax); + } + }); + return all_taxes; + }; + var collect_taxes = function (taxes) { + return _collect_taxes(taxes, []); + }; + + taxes = collect_taxes(taxes); + + // 2) Deal with the rounding methods + + var round_tax = this.company.tax_calculation_rounding_method != "round_globally"; + + var initial_currency_rounding = currency_rounding; + if (!round_tax) { + currency_rounding = currency_rounding * 0.00001; + } + + // 3) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. + var recompute_base = function (base_amount, fixed_amount, percent_amount, division_amount) { + return ( + (((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)) * + (100 - division_amount)) / + 100 + ); + }; + + var base = round_pr(price_unit * quantity, initial_currency_rounding); + + var sign = 1; + if (base < 0) { + base = -base; + sign = -1; + } + + var total_included_checkpoints = {}; + var i = taxes.length - 1; + var store_included_tax_total = true; + + var incl_fixed_amount = 0.0; + var incl_percent_amount = 0.0; + var incl_division_amount = 0.0; + + var cached_tax_amounts = {}; + if (handle_price_include) { + taxes.reverse().forEach(function (tax) { + if (tax.include_base_amount) { + base = recompute_base( + base, + incl_fixed_amount, + incl_percent_amount, + incl_division_amount + ); + incl_fixed_amount = 0.0; + incl_percent_amount = 0.0; + incl_division_amount = 0.0; + store_included_tax_total = true; + } + if (tax.price_include) { + if (tax.amount_type === "percent") { + incl_percent_amount += tax.amount * tax.sum_repartition_factor; + } else if (tax.amount_type === "division") { + incl_division_amount += tax.amount * tax.sum_repartition_factor; + } else if (tax.amount_type === "fixed") { + incl_fixed_amount += + Math.abs(quantity) * tax.amount * tax.sum_repartition_factor; + } else { + var tax_amount = self._compute_all(tax, base, quantity); + incl_fixed_amount += tax_amount; + cached_tax_amounts[i] = tax_amount; + } + if (store_included_tax_total) { + total_included_checkpoints[i] = base; + store_included_tax_total = false; + } + } + i -= 1; + }); + } + + var total_excluded = round_pr( + recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount), + initial_currency_rounding + ); + var total_included = total_excluded; + + // 4) Iterate the taxes in the sequence order to fill missing base/amount values. + + base = total_excluded; + + var skip_checkpoint = false; + + var taxes_vals = []; + i = 0; + var cumulated_tax_included_amount = 0; + taxes.reverse().forEach(function (tax) { + if (tax.price_include || tax.is_base_affected) { + var tax_base_amount = base; + } else { + tax_base_amount = total_excluded; + } + + if ( + !skip_checkpoint && + tax.price_include && + total_included_checkpoints[i] !== undefined && + tax.sum_repartition_factor != 0 + ) { + var tax_amount = + total_included_checkpoints[i] - (base + cumulated_tax_included_amount); + cumulated_tax_included_amount = 0; + } else { + tax_amount = self._compute_all(tax, tax_base_amount, quantity, true); + } + + tax_amount = round_pr(tax_amount, currency_rounding); + var factorized_tax_amount = round_pr( + tax_amount * tax.sum_repartition_factor, + currency_rounding + ); + + if (tax.price_include && total_included_checkpoints[i] === undefined) { + cumulated_tax_included_amount += factorized_tax_amount; + } + + taxes_vals.push({ + id: tax.id, + name: tax.name, + amount: sign * factorized_tax_amount, + base: sign * round_pr(tax_base_amount, currency_rounding), + }); + + if (tax.include_base_amount) { + base += factorized_tax_amount; + if (!tax.price_include) { + skip_checkpoint = true; + } + } + + total_included += factorized_tax_amount; + i += 1; + }); + + return { + taxes: taxes_vals, + total_excluded: sign * round_pr(total_excluded, this.currency.rounding), + total_included: sign * round_pr(total_included, this.currency.rounding), + }; + } + + /** + * Taxes after fiscal position mapping. + * @param {number[]} taxIds + * @param {object | falsy} fpos - fiscal position + * @returns {object[]} + */ + get_taxes_after_fp(taxIds, fpos) { + if (!fpos) { + return taxIds.map((taxId) => this.taxes_by_id[taxId]); + } + const mappedTaxes = []; + for (const taxId of taxIds) { + const tax = this.taxes_by_id[taxId]; + if (tax) { + const taxMaps = Object.values(fpos.fiscal_position_taxes_by_id).filter( + (fposTax) => fposTax.tax_src_id[0] === tax.id + ); + if (taxMaps.length) { + for (const taxMap of taxMaps) { + if (taxMap.tax_dest_id) { + const mappedTax = this.taxes_by_id[taxMap.tax_dest_id[0]]; + if (mappedTax) { + mappedTaxes.push(mappedTax); + } + } + } + } else { + mappedTaxes.push(tax); + } + } + } + return uniqueBy(mappedTaxes, (tax) => tax.id); + } + + /** + * TODO: We can probably remove this here and put it somewhere else. + * And that somewhere else becomes the parent of the proxy. + * Directly calls the requested service, instead of triggering a + * 'call_service' event up, which wouldn't work as services have no parent + * + * @param {OdooEvent} ev + */ + _trigger_up(ev) { + if (ev.is_stopped()) { + return; + } + const payload = ev.data; + if (ev.name === "call_service") { + let args = payload.args || []; + if (payload.service === "ajax" && payload.method === "rpc") { + // ajax service uses an extra 'target' argument for rpc + args = args.concat(ev.target); + } + const service = this.env.services[payload.service]; + const result = service[payload.method].apply(service, args); + payload.callback(result); + } + } + + isProductQtyZero(qty) { + return floatIsZero(qty, this.dp["Product Unit of Measure"]); + } + + disallowLineQuantityChange() { + return false; + } + + getCurrencySymbol() { + return this.currency ? this.currency.symbol : "$"; + } + /** + * Make the products corresponding to the given ids to be available_in_pos and + * fetch them to be added on the loaded products. + */ + async _addProducts(ids, setAvailable = true) { + if (setAvailable) { + await this.orm.write("product.product", ids, { available_in_pos: true }); + } + const product = await this.orm.call("pos.session", "get_pos_ui_product_product_by_params", [ + odoo.pos_session_id, + { domain: [["id", "in", ids]] }, + ]); + this._loadProductProduct(product); + } + async refreshTotalDueOfPartner(partner) { + const partnerWithUpdatedTotalDue = await this.orm.searchRead( + "res.partner", + [["id", "=", partner.id]], + ["total_due"] + ); + this.db.update_partners(partnerWithUpdatedTotalDue); + return partnerWithUpdatedTotalDue; + } + isOpenOrderShareable() { + return this.config.trusted_config_ids.length > 0; + } + switchPane() { + this.mobile_pane = this.mobile_pane === "left" ? "right" : "left"; + } + async logEmployeeMessage(action, message) { + await this.orm.call("pos.session", "log_partner_message", [ + this.pos_session.id, + this.user.partner_id.id, + action, + message, + ]); + } showScreen(name, props) { const component = registry.category("pos_screens").get(name); this.mainScreen = { component, props }; // Save the screen to the order so that it is shown again when the order is selected. if (component.storeOnOrder ?? true) { - this.globalState.get_order()?.set_screen_data({ name, props }); + this.get_order()?.set_screen_data({ name, props }); } } // Now the printer should work in PoS without restaurant async sendOrderInPreparation(order, cancelled = false) { - if (this.globalState.printers_category_ids_set.size) { + if (this.printers_category_ids_set.size) { try { const changes = order.changesToOrder(cancelled); @@ -81,23 +1553,23 @@ export class PosStore extends Reactive { } closeScreen() { this.addOrderIfEmpty(); - const { name: screenName } = this.globalState.get_order().get_screen_data(); + const { name: screenName } = this.get_order().get_screen_data(); this.showScreen(screenName); } addOrderIfEmpty() { - if (!this.globalState.get_order()) { - this.globalState.add_new_order(); + if (!this.get_order()) { + this.add_new_order(); } } connectToProxy() { return new Promise((resolve, reject) => { this.barcodeReader?.disconnectFromProxy(); - this.globalState.loadingSkipButtonIsShown = true; - this.hardwareProxy.autoconnect({ force_ip: this.globalState.config.proxy_ip }).then( + this.loadingSkipButtonIsShown = true; + this.hardwareProxy.autoconnect({ force_ip: this.config.proxy_ip }).then( () => { - if (this.globalState.config.iface_scan_via_proxy) { + if (this.config.iface_scan_via_proxy) { this.barcodeReader?.connectToProxy(); } resolve(); @@ -127,14 +1599,14 @@ export class PosStore extends Reactive { } async closePos() { - const customerDisplayService = this.globalState.env.services.customer_display; + const customerDisplayService = this.env.services.customer_display; if (customerDisplayService) { customerDisplayService.update({ closeUI: true }); } // If pos is not properly loaded, we just go back to /web without // doing anything in the order data. - if (!this.globalState || this.globalState.db.get_orders().length === 0) { + if (!this || this.db.get_orders().length === 0) { window.location = "/web#action=point_of_sale.action_client_pos_menu"; } @@ -142,11 +1614,11 @@ export class PosStore extends Reactive { // If sync successful, close without asking. // Otherwise, ask again saying that some orders are not yet synced. try { - await this.globalState.push_orders(); + await this.push_orders(); window.location = "/web#action=point_of_sale.action_client_pos_menu"; } catch (error) { console.warn(error); - const reason = this.globalState.failed + const reason = this.failed ? _t( "Some orders could not be submitted to " + "the server due to configuration errors. " + @@ -172,7 +1644,7 @@ export class PosStore extends Reactive { } async selectPartner() { // FIXME, find order to refund when we are in the ticketscreen. - const currentOrder = this.globalState.get_order(); + const currentOrder = this.get_order(); if (!currentOrder) { return; } @@ -199,16 +1671,16 @@ export class PosStore extends Reactive { } // FIXME: POSREF, method exist only to be overrided async addProductFromUi(product, options) { - this.globalState.get_order().add_product(product, options); + this.get_order().add_product(product, options); } async addProductToCurrentOrder(product) { if (Number.isInteger(product)) { - product = this.globalState.db.get_product_by_id(product); + product = this.db.get_product_by_id(product); } - const currentOrder = this.globalState.get_order(); + const currentOrder = this.get_order(); if (!currentOrder) { - this.globalState.add_new_order(); + this.add_new_order(); } const options = await product.getAddProductOptions(); @@ -240,18 +1712,15 @@ export class PosStore extends Reactive { } } shouldShowCashControl() { - return ( - this.globalState.config.cash_control && - this.globalState.pos_session.state == "opening_control" - ); + return this.config.cash_control && this.pos_session.state == "opening_control"; } preloadImages() { - for (const product of this.globalState.db.get_product_by_category(0)) { + for (const product of this.db.get_product_by_category(0)) { const image = new Image(); image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; } - for (const category of Object.values(this.globalState.db.category_by_id)) { + for (const category of Object.values(this.db.category_by_id)) { if (category.id == 0) { continue; } @@ -270,7 +1739,7 @@ export class PosStore extends Reactive { localStorage["message"] = ""; localStorage["message"] = JSON.stringify({ message: "close_tabs", - session: this.globalState.pos_session.id, + session: this.pos_session.id, }); window.addEventListener( @@ -278,10 +1747,7 @@ export class PosStore extends Reactive { (event) => { if (event.key === "message" && event.newValue) { const msg = JSON.parse(event.newValue); - if ( - msg.message === "close_tabs" && - msg.session == this.globalState.pos_session.id - ) { + if (msg.message === "close_tabs" && msg.session == this.pos_session.id) { console.info("POS / Session opened in another window. EXITING POS"); this.closePos(); } @@ -294,11 +1760,30 @@ export class PosStore extends Reactive { showBackButton() { return ( this.mainScreen.component === PaymentScreen || - (this.mainScreen.component === ProductScreen && this.globalState.mobile_pane == "left") + (this.mainScreen.component === ProductScreen && this.mobile_pane == "left") ); } } +PosStore.prototype.electronic_payment_interfaces = {}; + +/** + * Call this function to map your PaymentInterface implementation to + * the use_payment_terminal field. When the POS loads it will take + * care of instantiating your interface and setting it on the right + * payment methods. + * + * @param {string} use_payment_terminal - value used in the + * use_payment_terminal selection field + * + * @param {Object} ImplementedPaymentInterface - implemented + * PaymentInterface + */ +export function register_payment_method(use_payment_terminal, ImplementedPaymentInterface) { + PosStore.prototype.electronic_payment_interfaces[use_payment_terminal] = + ImplementedPaymentInterface; +} + export const posService = { dependencies: PosStore.serviceDependencies, async start(env, deps) { diff --git a/addons/point_of_sale/static/src/app/utils/contextual_utils_service.js b/addons/point_of_sale/static/src/app/utils/contextual_utils_service.js index c2649e2fcc4..118f5c1ba04 100644 --- a/addons/point_of_sale/static/src/app/utils/contextual_utils_service.js +++ b/addons/point_of_sale/static/src/app/utils/contextual_utils_service.js @@ -11,8 +11,8 @@ import { registry } from "@web/core/registry"; export const contextualUtilsService = { dependencies: ["pos"], start(env, { pos }) { - const currency = pos.globalState.currency; - const productUoMDecimals = pos.globalState.dp["Product Unit of Measure"]; + const currency = pos.currency; + const productUoMDecimals = pos.dp["Product Unit of Measure"]; const formatProductQty = (qty) => { return formatFloat(qty, { digits: [true, productUoMDecimals] }); diff --git a/addons/point_of_sale/static/src/app/utils/money_details_popup/money_details_popup.js b/addons/point_of_sale/static/src/app/utils/money_details_popup/money_details_popup.js index ff61e5e2cc5..6867ce9e90b 100644 --- a/addons/point_of_sale/static/src/app/utils/money_details_popup/money_details_popup.js +++ b/addons/point_of_sale/static/src/app/utils/money_details_popup/money_details_popup.js @@ -11,11 +11,11 @@ export class MoneyDetailsPopup extends AbstractAwaitablePopup { setup() { super.setup(); this.pos = usePos(); - this.currency = this.pos.globalState.currency; + this.currency = this.pos.currency; this.state = useState({ moneyDetails: this.props.moneyDetails ? { ...this.props.moneyDetails } - : Object.fromEntries(this.pos.globalState.bills.map((bill) => [bill.value, 0])), + : Object.fromEntries(this.pos.bills.map((bill) => [bill.value, 0])), total: this.props.total ? this.props.total : 0, action: this.props.action ? this.props.action : null, }); @@ -42,7 +42,7 @@ export class MoneyDetailsPopup extends AbstractAwaitablePopup { let moneyDetailsNotes = !floatIsZero(this.state.total, this.currency.decimal_places) ? "Money details: \n" : null; - this.pos.globalState.bills.forEach((bill) => { + this.pos.bills.forEach((bill) => { if (this.state.moneyDetails[bill.value]) { moneyDetailsNotes += ` - ${ this.state.moneyDetails[bill.value] @@ -59,10 +59,10 @@ export class MoneyDetailsPopup extends AbstractAwaitablePopup { async cancel() { super.cancel(); if ( - this.pos.globalState.config.iface_cashdrawer && - this.pos.globalState.hardwareProxy.connectionInfo.status === "connected" + this.pos.config.iface_cashdrawer && + this.pos.hardwareProxy.connectionInfo.status === "connected" ) { - this.pos.globalState.logEmployeeMessage(this.state.action, "ACTION_CANCELLED"); + this.pos.logEmployeeMessage(this.state.action, "ACTION_CANCELLED"); } } } diff --git a/addons/point_of_sale/views/pos_assets_qunit.xml b/addons/point_of_sale/views/pos_assets_qunit.xml index 1ab5a06a824..d73fc2702bd 100644 --- a/addons/point_of_sale/views/pos_assets_qunit.xml +++ b/addons/point_of_sale/views/pos_assets_qunit.xml @@ -6,7 +6,7 @@ Point of Sale Tests - +