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.
|
||||
* **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.
|
||||
* **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.
|
||||
|
||||
Technical Details
|
||||
@ -17,16 +18,21 @@ Technical Details
|
||||
|
||||
* **Models Patched**:
|
||||
* ``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**:
|
||||
* ``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``.
|
||||
* ``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
|
||||
============
|
||||
|
||||
* ``point_of_sale``
|
||||
* ``pos_hr``
|
||||
* ``pos_employee_role``
|
||||
* ``pos_restaurant``
|
||||
* ``stock``
|
||||
|
||||
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.
|
||||
""",
|
||||
'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': [],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
|
||||
@ -1 +1,2 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Use the same helper as the line patch
|
||||
|
||||
@ -41,7 +41,7 @@ patch(PosOrderline.prototype, {
|
||||
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 lockedQty = this.get_locked_qty();
|
||||
|
||||
@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(TicketScreen.prototype, {
|
||||
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);
|
||||
if (!isManager) {
|
||||
return true;
|
||||
|
||||
@ -5,6 +5,14 @@ import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { transferState } from "../transfer_state";
|
||||
|
||||
patch(PosStore.prototype, {
|
||||
async processServerData() {
|
||||
const res = await super.processServerData(...arguments);
|
||||
if (this.models) {
|
||||
this.models.pos = this;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
async beforeDeleteOrder(order, options = {}) {
|
||||
const cashier = this.getCashier();
|
||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user