feat: implement backend stock move write overrides and unify cashier role verification via PosStore global binding.
This commit is contained in:
parent
f25a550871
commit
508d9f7d87
@ -10,6 +10,7 @@ Features
|
|||||||
* **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.
|
* **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.
|
* **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.
|
* **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.
|
||||||
|
* **Backend Checkout Resilience**: Bypasses the default stock validation constraint ("You cannot change a cancelled stock move") during checkout. When a manager deletes or reduces a sent orderline, the picking's corresponding cancelled stock moves are safely ignored during quantity updates, preventing synchronization errors on checkout.
|
||||||
* **User-Friendly Alerts**: Displays a clear "Action Restricted" popup when an unauthorized deletion attempt is blocked.
|
* **User-Friendly Alerts**: Displays a clear "Action Restricted" popup when an unauthorized deletion attempt is blocked.
|
||||||
|
|
||||||
Technical Details
|
Technical Details
|
||||||
@ -17,16 +18,21 @@ Technical Details
|
|||||||
|
|
||||||
* **Models Patched**:
|
* **Models Patched**:
|
||||||
* ``pos.order.line``: Added ``x_locked_qty`` to store the snapshot of the quantity at the moment of kitchen preparation.
|
* ``pos.order.line``: Added ``x_locked_qty`` to store the snapshot of the quantity at the moment of kitchen preparation.
|
||||||
|
* ``stock.move``: Overrides ``write`` to separate writes on cancelled and active moves, avoiding stock validation exceptions when managers cancel sent quantities.
|
||||||
* **Javascript Patches**:
|
* **Javascript Patches**:
|
||||||
|
* ``PosStore``: Binds the active ``PosStore`` service globally to `this.models.pos` during server data loading to ensure consistent, reactive cashier role checks across all prototype record layers.
|
||||||
* ``PosOrderline``: Enforces quantity checks in ``setQuantity`` using both Odoo's native ``mp_qty`` and the custom ``x_locked_qty``.
|
* ``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``.
|
* ``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.
|
* ``TicketScreen``: Intercepts the UI delete/refund actions using ``getCashier()`` to dynamically enforce supervisor authorization.
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
============
|
============
|
||||||
|
|
||||||
* ``point_of_sale``
|
* ``point_of_sale``
|
||||||
|
* ``pos_hr``
|
||||||
* ``pos_employee_role``
|
* ``pos_employee_role``
|
||||||
|
* ``pos_restaurant``
|
||||||
|
* ``stock``
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|||||||
@ -9,7 +9,7 @@ This module prevents cashiers from deleting or reducing the quantity of products
|
|||||||
Only users with the 'Area Manager' or 'Store Manager' role can perform these actions.
|
Only users with the 'Area Manager' or 'Store Manager' role can perform these actions.
|
||||||
""",
|
""",
|
||||||
'author': "Suherdy Yacob",
|
'author': "Suherdy Yacob",
|
||||||
'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role', 'pos_restaurant'],
|
'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role', 'pos_restaurant', 'stock'],
|
||||||
'data': [],
|
'data': [],
|
||||||
'assets': {
|
'assets': {
|
||||||
'point_of_sale._assets_pos': [
|
'point_of_sale._assets_pos': [
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
from . import pos_order_line
|
from . import pos_order_line
|
||||||
|
from . import stock_move
|
||||||
|
|||||||
22
models/stock_move.py
Normal file
22
models/stock_move.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = 'stock.move'
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'quantity' in vals and any(move.state == 'cancel' for move in self):
|
||||||
|
non_cancelled_moves = self.filtered(lambda m: m.state != 'cancel')
|
||||||
|
cancelled_moves = self - non_cancelled_moves
|
||||||
|
|
||||||
|
res = True
|
||||||
|
if non_cancelled_moves:
|
||||||
|
res = super(StockMove, non_cancelled_moves).write(vals)
|
||||||
|
|
||||||
|
if cancelled_moves and len(vals) > 1:
|
||||||
|
vals_no_qty = {k: v for k, v in vals.items() if k != 'quantity'}
|
||||||
|
super(StockMove, cancelled_moves).write(vals_no_qty)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
return super(StockMove, self).write(vals)
|
||||||
@ -17,7 +17,7 @@ patch(PosOrder.prototype, {
|
|||||||
return super.removeOrderline(...arguments);
|
return super.removeOrderline(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
const cashier = this.models.pos?.getCashier();
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
|
|
||||||
// Use the same helper as the line patch
|
// Use the same helper as the line patch
|
||||||
|
|||||||
@ -41,7 +41,7 @@ patch(PosOrderline.prototype, {
|
|||||||
return super.setQuantity(...arguments);
|
return super.setQuantity(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
const cashier = this.models.pos?.getCashier();
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
|
|
||||||
const lockedQty = this.get_locked_qty();
|
const lockedQty = this.get_locked_qty();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch";
|
|||||||
|
|
||||||
patch(TicketScreen.prototype, {
|
patch(TicketScreen.prototype, {
|
||||||
shouldHideDeleteButton(order) {
|
shouldHideDeleteButton(order) {
|
||||||
const cashier = this.pos.models["pos.session"].getFirst()?.cashier;
|
const cashier = this.pos.getCashier();
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
if (!isManager) {
|
if (!isManager) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -5,6 +5,14 @@ import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
|||||||
import { transferState } from "../transfer_state";
|
import { transferState } from "../transfer_state";
|
||||||
|
|
||||||
patch(PosStore.prototype, {
|
patch(PosStore.prototype, {
|
||||||
|
async processServerData() {
|
||||||
|
const res = await super.processServerData(...arguments);
|
||||||
|
if (this.models) {
|
||||||
|
this.models.pos = this;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
async beforeDeleteOrder(order, options = {}) {
|
async beforeDeleteOrder(order, options = {}) {
|
||||||
const cashier = this.getCashier();
|
const cashier = this.getCashier();
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user