feat: implement order transfer protection and restrict modifications for sent items
This commit is contained in:
parent
c37db8da68
commit
f25a550871
@ -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'],
|
||||
'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role', 'pos_restaurant'],
|
||||
'data': [],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
from odoo import models, fields
|
||||
from odoo import api, fields, models
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
|
||||
x_locked_qty = fields.Float(string='Locked Quantity', default=0.0)
|
||||
x_transferred_qty = fields.Float(string='Transferred Quantity', default=0.0)
|
||||
|
||||
@api.model
|
||||
def _load_pos_data_fields(self, config):
|
||||
fields = super()._load_pos_data_fields(config)
|
||||
if 'x_locked_qty' not in fields:
|
||||
fields.append('x_locked_qty')
|
||||
if 'x_transferred_qty' not in fields:
|
||||
fields.append('x_transferred_qty')
|
||||
return fields
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(ClosePosPopup.prototype, {
|
||||
async handleClosingError(response) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
title: response.title || "Error",
|
||||
body: response.message,
|
||||
confirmLabel: _t("Review Orders"),
|
||||
confirm: () => {
|
||||
if (!response.redirect) {
|
||||
this.props.close();
|
||||
this.pos.navigate("TicketScreen");
|
||||
}
|
||||
},
|
||||
dismiss: async () => {},
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { transferState } from "../transfer_state";
|
||||
|
||||
patch(PosOrder.prototype, {
|
||||
updateLastOrderChange() {
|
||||
@ -12,6 +13,10 @@ patch(PosOrder.prototype, {
|
||||
},
|
||||
|
||||
removeOrderline(line) {
|
||||
if (transferState.isTransferring) {
|
||||
return super.removeOrderline(...arguments);
|
||||
}
|
||||
|
||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||
|
||||
@ -31,3 +36,4 @@ patch(PosOrder.prototype, {
|
||||
return super.removeOrderline(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,27 +1,46 @@
|
||||
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";
|
||||
import { transferState } from "../transfer_state";
|
||||
|
||||
patch(PosOrderline.prototype, {
|
||||
setup(vals) {
|
||||
super.setup(...arguments);
|
||||
this.x_transferred_qty = vals.x_transferred_qty || 0;
|
||||
this.x_locked_qty = vals.x_locked_qty || 0;
|
||||
},
|
||||
|
||||
init_from_JSON(json) {
|
||||
super.init_from_JSON(...arguments);
|
||||
this.x_locked_qty = json.x_locked_qty || 0;
|
||||
this.x_transferred_qty = json.x_transferred_qty || 0;
|
||||
},
|
||||
|
||||
export_as_JSON() {
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
json.x_locked_qty = this.get_locked_qty();
|
||||
json.x_locked_qty = this.x_locked_qty || 0;
|
||||
json.x_transferred_qty = this.x_transferred_qty || 0;
|
||||
return json;
|
||||
},
|
||||
|
||||
merge(orderline) {
|
||||
super.merge(...arguments);
|
||||
this.x_transferred_qty = (this.x_transferred_qty || 0) + (orderline.x_transferred_qty || 0);
|
||||
},
|
||||
|
||||
get_locked_qty() {
|
||||
// Use mp_qty (native Odoo sent qty) or our persisted x_locked_qty
|
||||
// Use mp_qty (native Odoo sent qty), our persisted x_locked_qty, or x_transferred_qty
|
||||
const mpQty = parseFloat(this.mp_qty || 0);
|
||||
const xLockedQty = parseFloat(this.x_locked_qty || 0);
|
||||
return Math.max(mpQty, xLockedQty);
|
||||
const xTransferredQty = parseFloat(this.x_transferred_qty || 0);
|
||||
return Math.max(mpQty, xLockedQty, xTransferredQty);
|
||||
},
|
||||
|
||||
setQuantity(quantity, keep_price) {
|
||||
if (transferState.isTransferring) {
|
||||
return super.setQuantity(...arguments);
|
||||
}
|
||||
|
||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||
|
||||
@ -46,3 +65,4 @@ patch(PosOrderline.prototype, {
|
||||
return super.setQuantity(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
13
static/src/app/screens/ticket_screen/ticket_screen.js
Normal file
13
static/src/app/screens/ticket_screen/ticket_screen.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(TicketScreen.prototype, {
|
||||
shouldHideDeleteButton(order) {
|
||||
const cashier = this.pos.models["pos.session"].getFirst()?.cashier;
|
||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||
if (!isManager) {
|
||||
return true;
|
||||
}
|
||||
return super.shouldHideDeleteButton(...arguments);
|
||||
}
|
||||
});
|
||||
@ -2,20 +2,21 @@ 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";
|
||||
import { transferState } from "../transfer_state";
|
||||
|
||||
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)
|
||||
// Check if the order has any locked items (sent to kitchen or transferred)
|
||||
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."),
|
||||
body: _t("You cannot delete this order because some items have already been sent to the kitchen or transferred. Please call an Area Manager or Store Manager."),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@ -23,5 +24,49 @@ patch(PosStore.prototype, {
|
||||
}
|
||||
|
||||
return super.beforeDeleteOrder(order, options);
|
||||
},
|
||||
|
||||
prepareOrderTransfer(order, destinationTable) {
|
||||
const originalTable = order.table_id;
|
||||
const res = super.prepareOrderTransfer(...arguments);
|
||||
if (res === false && originalTable && destinationTable && originalTable.id !== destinationTable.rootTable.id) {
|
||||
// This is a transfer to an empty table!
|
||||
for (const line of order.lines) {
|
||||
line.x_transferred_qty = line.qty;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
async mergeOrders(sourceOrder, destOrder) {
|
||||
transferState.isTransferring = true;
|
||||
try {
|
||||
// Mark all source lines with x_transferred_qty = line.qty before merging
|
||||
const sourceLinesMap = {};
|
||||
for (const line of sourceOrder.lines) {
|
||||
if (!line.x_transferred_qty) {
|
||||
line.x_transferred_qty = line.qty;
|
||||
}
|
||||
sourceLinesMap[line.uuid] = line.x_transferred_qty;
|
||||
}
|
||||
|
||||
const res = await super.mergeOrders(...arguments);
|
||||
|
||||
// Use the unmerge tracking to map transferred quantities to newly created destination lines
|
||||
if (destOrder && destOrder.uiState && destOrder.uiState.unmerge) {
|
||||
for (const [newUuid, unmergeInfo] of Object.entries(destOrder.uiState.unmerge)) {
|
||||
const formerUuid = unmergeInfo.formerUuid;
|
||||
const newLine = destOrder.lines.find(l => l.uuid === newUuid);
|
||||
if (newLine && formerUuid && sourceLinesMap[formerUuid]) {
|
||||
newLine.x_transferred_qty = sourceLinesMap[formerUuid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
} finally {
|
||||
transferState.isTransferring = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
3
static/src/app/transfer_state.js
Normal file
3
static/src/app/transfer_state.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const transferState = {
|
||||
isTransferring: false,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user