First Commit
This commit is contained in:
commit
08c4e3711b
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
36
README.rst
Normal file
36
README.rst
Normal file
@ -0,0 +1,36 @@
|
||||
========================
|
||||
POS Lock Sent Products
|
||||
========================
|
||||
|
||||
This module implements a robust security lock in the Odoo 19 Point of Sale system to prevent unauthorized deletion or reduction of order lines that have already been sent to the kitchen (Preparation).
|
||||
|
||||
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.
|
||||
* **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.
|
||||
|
||||
Technical Details
|
||||
=================
|
||||
|
||||
* **Models Patched**:
|
||||
* ``pos.order.line``: Added ``x_locked_qty`` to store the snapshot of the quantity at the moment of kitchen preparation.
|
||||
* **Javascript Patches**:
|
||||
* ``PosOrderline``: Enforces quantity checks in ``setQuantity`` using both Odoo's native ``mp_qty`` and the custom ``x_locked_qty``.
|
||||
* ``PosOrder``: Intercepts ``removeOrderline`` to block full line deletions and captures snapshots during ``updateLastOrderChange``.
|
||||
* ``OrderSummary``: Intercepts the UI "Remove" action to ensure the security alert is correctly displayed to the user.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
* ``point_of_sale``
|
||||
* ``pos_employee_role``
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
1. Install the module as usual.
|
||||
2. Ensure the "Area Manager" role is configured for supervisors who require bypass authority.
|
||||
3. Refresh the POS interface to load the new security logic.
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
22
__manifest__.py
Normal file
22
__manifest__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'POS Lock Sent Product',
|
||||
'version': '1.0',
|
||||
'category': 'Sales/Point of Sale',
|
||||
'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.
|
||||
""",
|
||||
'author': "Suherdy Yacob",
|
||||
'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role'],
|
||||
'data': [],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
'pos_lock_sent_product/static/src/app/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import pos_order_line
|
||||
6
models/pos_order_line.py
Normal file
6
models/pos_order_line.py
Normal file
@ -0,0 +1,6 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
|
||||
x_locked_qty = fields.Float(string='Locked Quantity', default=0.0)
|
||||
33
static/src/app/models/pos_order.js
Normal file
33
static/src/app/models/pos_order.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(PosOrder.prototype, {
|
||||
updateLastOrderChange() {
|
||||
super.updateLastOrderChange(...arguments);
|
||||
for (const line of this.lines) {
|
||||
// Backup our lock to the persisted field
|
||||
line.x_locked_qty = line.qty;
|
||||
}
|
||||
},
|
||||
|
||||
removeOrderline(line) {
|
||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||
const isAreaManager = cashier?.pos_role === 'area_manager';
|
||||
|
||||
// Use the same helper as the line patch
|
||||
const lockedQty = line.get_locked_qty ? line.get_locked_qty() : 0;
|
||||
|
||||
if (lockedQty > 0 && !isAreaManager) {
|
||||
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."),
|
||||
};
|
||||
}
|
||||
return super.removeOrderline(...arguments);
|
||||
},
|
||||
});
|
||||
48
static/src/app/models/pos_order_line.js
Normal file
48
static/src/app/models/pos_order_line.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(PosOrderline.prototype, {
|
||||
init_from_JSON(json) {
|
||||
super.init_from_JSON(...arguments);
|
||||
this.x_locked_qty = json.x_locked_qty || 0;
|
||||
},
|
||||
|
||||
export_as_JSON() {
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
json.x_locked_qty = this.get_locked_qty();
|
||||
return json;
|
||||
},
|
||||
|
||||
get_locked_qty() {
|
||||
// Use mp_qty (native Odoo sent qty) or our persisted x_locked_qty
|
||||
const mpQty = parseFloat(this.mp_qty || 0);
|
||||
const xLockedQty = parseFloat(this.x_locked_qty || 0);
|
||||
return Math.max(mpQty, xLockedQty);
|
||||
},
|
||||
|
||||
setQuantity(quantity, keep_price) {
|
||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||
const isAreaManager = cashier?.pos_role === 'area_manager';
|
||||
|
||||
const lockedQty = this.get_locked_qty();
|
||||
const newQty = (typeof quantity === "number" || (typeof quantity === "string" && quantity !== "")) ? parseFloat(quantity) : 0;
|
||||
|
||||
if (lockedQty > 0 && !isAreaManager) {
|
||||
// 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
|
||||
if (this.qty > lockedQty) {
|
||||
return super.setQuantity(lockedQty, keep_price);
|
||||
}
|
||||
|
||||
// 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."),
|
||||
};
|
||||
}
|
||||
}
|
||||
return super.setQuantity(...arguments);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
patch(OrderSummary.prototype, {
|
||||
_setValue(val) {
|
||||
const { numpadMode } = this.pos;
|
||||
let selectedLine = this.currentOrder.getSelectedOrderline();
|
||||
|
||||
// Only intercept the "remove" action to show our custom popup.
|
||||
// We DO NOT intercept val === "" because Odoo's setQuantity("") handles it correctly.
|
||||
if (selectedLine && numpadMode === "quantity" && val === "remove") {
|
||||
if (selectedLine.combo_parent_id) {
|
||||
selectedLine = selectedLine.combo_parent_id;
|
||||
}
|
||||
|
||||
// Call the patched removeOrderline which returns an error object if restricted
|
||||
const result = this.currentOrder.removeOrderline(selectedLine);
|
||||
|
||||
if (typeof result === "object") {
|
||||
this.dialog.add(AlertDialog, result);
|
||||
this.numberBuffer.reset();
|
||||
} else if (result === false) {
|
||||
this.numberBuffer.reset();
|
||||
}
|
||||
// We fully handled "remove", do not call super.
|
||||
return;
|
||||
}
|
||||
|
||||
// For all other actions (including val === ""), let standard Odoo handle it
|
||||
super._setValue(...arguments);
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user