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.
|
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'],
|
'depends': ['point_of_sale', 'pos_hr', 'pos_employee_role', 'pos_restaurant'],
|
||||||
'data': [],
|
'data': [],
|
||||||
'assets': {
|
'assets': {
|
||||||
'point_of_sale._assets_pos': [
|
'point_of_sale._assets_pos': [
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
from odoo import models, fields
|
from odoo import api, fields, models
|
||||||
|
|
||||||
class PosOrderLine(models.Model):
|
class PosOrderLine(models.Model):
|
||||||
_inherit = 'pos.order.line'
|
_inherit = 'pos.order.line'
|
||||||
|
|
||||||
x_locked_qty = fields.Float(string='Locked Quantity', default=0.0)
|
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 { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { _t } from "@web/core/l10n/translation";
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { transferState } from "../transfer_state";
|
||||||
|
|
||||||
patch(PosOrder.prototype, {
|
patch(PosOrder.prototype, {
|
||||||
updateLastOrderChange() {
|
updateLastOrderChange() {
|
||||||
@ -12,6 +13,10 @@ patch(PosOrder.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
removeOrderline(line) {
|
removeOrderline(line) {
|
||||||
|
if (transferState.isTransferring) {
|
||||||
|
return super.removeOrderline(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
|
|
||||||
@ -31,3 +36,4 @@ patch(PosOrder.prototype, {
|
|||||||
return super.removeOrderline(...arguments);
|
return super.removeOrderline(...arguments);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,46 @@
|
|||||||
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { _t } from "@web/core/l10n/translation";
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { transferState } from "../transfer_state";
|
||||||
|
|
||||||
patch(PosOrderline.prototype, {
|
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) {
|
init_from_JSON(json) {
|
||||||
super.init_from_JSON(...arguments);
|
super.init_from_JSON(...arguments);
|
||||||
this.x_locked_qty = json.x_locked_qty || 0;
|
this.x_locked_qty = json.x_locked_qty || 0;
|
||||||
|
this.x_transferred_qty = json.x_transferred_qty || 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
export_as_JSON() {
|
export_as_JSON() {
|
||||||
const json = super.export_as_JSON(...arguments);
|
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;
|
return json;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
merge(orderline) {
|
||||||
|
super.merge(...arguments);
|
||||||
|
this.x_transferred_qty = (this.x_transferred_qty || 0) + (orderline.x_transferred_qty || 0);
|
||||||
|
},
|
||||||
|
|
||||||
get_locked_qty() {
|
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 mpQty = parseFloat(this.mp_qty || 0);
|
||||||
const xLockedQty = parseFloat(this.x_locked_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) {
|
setQuantity(quantity, keep_price) {
|
||||||
|
if (transferState.isTransferring) {
|
||||||
|
return super.setQuantity(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
const cashier = this.models["pos.session"].getFirst()?.cashier;
|
||||||
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
const isManager = cashier && ['area_manager', 'outlet_manager'].includes(cashier.pos_role);
|
||||||
|
|
||||||
@ -46,3 +65,4 @@ patch(PosOrderline.prototype, {
|
|||||||
return super.setQuantity(...arguments);
|
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 { patch } from "@web/core/utils/patch";
|
||||||
import { _t } from "@web/core/l10n/translation";
|
import { _t } from "@web/core/l10n/translation";
|
||||||
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||||
|
import { transferState } from "../transfer_state";
|
||||||
|
|
||||||
patch(PosStore.prototype, {
|
patch(PosStore.prototype, {
|
||||||
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);
|
||||||
|
|
||||||
// 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) {
|
if (!isManager && order && order.lines) {
|
||||||
for (const line of order.lines) {
|
for (const line of order.lines) {
|
||||||
const lockedQty = line.get_locked_qty ? line.get_locked_qty() : 0;
|
const lockedQty = line.get_locked_qty ? line.get_locked_qty() : 0;
|
||||||
if (lockedQty > 0) {
|
if (lockedQty > 0) {
|
||||||
this.dialog.add(AlertDialog, {
|
this.dialog.add(AlertDialog, {
|
||||||
title: _t("Action Restricted"),
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@ -23,5 +24,49 @@ patch(PosStore.prototype, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return super.beforeDeleteOrder(order, options);
|
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