feat: grant outlet managers bypass authority for restricted POS actions and prevent order deletion when items are locked

This commit is contained in:
Suherdy Yacob 2026-05-07 16:58:20 +07:00
parent 08c4e3711b
commit c37db8da68
5 changed files with 36 additions and 9 deletions

View File

@ -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.

View File

@ -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'],

View File

@ -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);

View File

@ -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."),
};
}
}

View File

@ -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);
}
});