From c37db8da687ef15c429a589c29f3fb982a04067e Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 7 May 2026 16:58:20 +0700 Subject: [PATCH] feat: grant outlet managers bypass authority for restricted POS actions and prevent order deletion when items are locked --- README.rst | 4 ++-- __manifest__.py | 2 +- static/src/app/models/pos_order.js | 6 +++--- static/src/app/models/pos_order_line.js | 6 +++--- static/src/app/services/pos_store.js | 27 +++++++++++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 static/src/app/services/pos_store.js diff --git a/README.rst b/README.rst index cac577b..3da7fbc 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ This module implements a robust security lock in the Odoo 19 Point of Sale syste Features ======== -* **Role-Based Security**: Only users with the "Area Manager" role (integrated with `pos_employee_role`) can delete or reduce the quantity of items already sent to the kitchen. +* **Role-Based Security**: Only users with the "Area Manager" or "Store Manager" (technically "outlet_manager") roles can delete or reduce the quantity of items already sent to the kitchen. * **Persistent Locking**: Uses a database-backed field `x_locked_qty` on `pos.order.line` to ensure the lock state persists across browser refreshes, device synchronizations, and session restarts. * **Graceful Reduction**: Allows cashiers to freely add and remove "new" quantities on an existing line. For example, if 1 item is sent and 1 is newly added (Total 2), the cashier can safely backspace to reduce the quantity to 1, but cannot drop it below the sent amount. * **User-Friendly Alerts**: Displays a clear "Action Restricted" popup when an unauthorized deletion attempt is blocked. @@ -32,5 +32,5 @@ Installation ============ 1. Install the module as usual. -2. Ensure the "Area Manager" role is configured for supervisors who require bypass authority. +2. Ensure the "Area Manager" or "Store Manager" roles are configured for supervisors who require bypass authority. 3. Refresh the POS interface to load the new security logic. diff --git a/__manifest__.py b/__manifest__.py index 5ac0da9..5e7d269 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -6,7 +6,7 @@ 'summary': 'Lock product deletion/reduction after order is sent to kitchen', 'description': """ This module prevents cashiers from deleting or reducing the quantity of products that have already been sent to the kitchen. -Only users with the 'Area Manager' role can perform these actions. +Only users with the 'Area Manager' or 'Store Manager' role can perform these actions. """, 'author': "Suherdy Yacob", 'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role'], diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js index 023e94a..c74eec1 100644 --- a/static/src/app/models/pos_order.js +++ b/static/src/app/models/pos_order.js @@ -13,19 +13,19 @@ patch(PosOrder.prototype, { removeOrderline(line) { const cashier = this.models["pos.session"].getFirst()?.cashier; - const isAreaManager = cashier?.pos_role === 'area_manager'; + const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role); // Use the same helper as the line patch const lockedQty = line.get_locked_qty ? line.get_locked_qty() : 0; - if (lockedQty > 0 && !isAreaManager) { + if (lockedQty > 0 && !isManager) { if (line.qty > lockedQty) { line.setQuantity(lockedQty); return false; } return { title: _t("Action Restricted"), - body: _t("You cannot delete a product that has already been sent to the kitchen. Please call an Area Manager."), + body: _t("You cannot delete a product that has already been sent to the kitchen. Please call an Area Manager or Store Manager."), }; } return super.removeOrderline(...arguments); diff --git a/static/src/app/models/pos_order_line.js b/static/src/app/models/pos_order_line.js index a870d4e..d805a23 100644 --- a/static/src/app/models/pos_order_line.js +++ b/static/src/app/models/pos_order_line.js @@ -23,12 +23,12 @@ patch(PosOrderline.prototype, { setQuantity(quantity, keep_price) { const cashier = this.models["pos.session"].getFirst()?.cashier; - const isAreaManager = cashier?.pos_role === 'area_manager'; + const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role); const lockedQty = this.get_locked_qty(); const newQty = (typeof quantity === "number" || (typeof quantity === "string" && quantity !== "")) ? parseFloat(quantity) : 0; - if (lockedQty > 0 && !isAreaManager) { + if (lockedQty > 0 && !isManager) { // If trying to delete or reduce below locked qty if (quantity === "" || isNaN(parseFloat(quantity)) || newQty < lockedQty) { // If there's unsent quantity, gracefully reduce it to the locked quantity @@ -39,7 +39,7 @@ patch(PosOrderline.prototype, { // Otherwise, they are trying to delete the sent quantity, so block it return { title: _t("Action Restricted"), - body: _t("You cannot reduce the quantity below what was already sent to the kitchen. Please call an Area Manager."), + body: _t("You cannot reduce the quantity below what was already sent to the kitchen. Please call an Area Manager or Store Manager."), }; } } diff --git a/static/src/app/services/pos_store.js b/static/src/app/services/pos_store.js new file mode 100644 index 0000000..c7644de --- /dev/null +++ b/static/src/app/services/pos_store.js @@ -0,0 +1,27 @@ +import { PosStore } from "@point_of_sale/app/services/pos_store"; +import { patch } from "@web/core/utils/patch"; +import { _t } from "@web/core/l10n/translation"; +import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; + +patch(PosStore.prototype, { + async beforeDeleteOrder(order, options = {}) { + const cashier = this.getCashier(); + const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role); + + // Check if the order has any locked items (sent to kitchen) + if (!isManager && order && order.lines) { + for (const line of order.lines) { + const lockedQty = line.get_locked_qty ? line.get_locked_qty() : 0; + if (lockedQty > 0) { + this.dialog.add(AlertDialog, { + title: _t("Action Restricted"), + body: _t("You cannot delete this order because some items have already been sent to the kitchen. Please call an Area Manager or Store Manager."), + }); + return false; + } + } + } + + return super.beforeDeleteOrder(order, options); + } +});