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