diff --git a/__manifest__.py b/__manifest__.py index 5e7d269..5159b1a 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -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': [ diff --git a/models/pos_order_line.py b/models/pos_order_line.py index b8f73a9..b74232d 100644 --- a/models/pos_order_line.py +++ b/models/pos_order_line.py @@ -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 + diff --git a/static/src/app/components/popups/closing_popup/closing_popup.js b/static/src/app/components/popups/closing_popup/closing_popup.js new file mode 100644 index 0000000..4283dd0 --- /dev/null +++ b/static/src/app/components/popups/closing_popup/closing_popup.js @@ -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 () => {}, + }); + } +}); diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js index c74eec1..f13c50e 100644 --- a/static/src/app/models/pos_order.js +++ b/static/src/app/models/pos_order.js @@ -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); }, }); + diff --git a/static/src/app/models/pos_order_line.js b/static/src/app/models/pos_order_line.js index d805a23..7150b45 100644 --- a/static/src/app/models/pos_order_line.js +++ b/static/src/app/models/pos_order_line.js @@ -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); }, }); + diff --git a/static/src/app/screens/ticket_screen/ticket_screen.js b/static/src/app/screens/ticket_screen/ticket_screen.js new file mode 100644 index 0000000..cb5639f --- /dev/null +++ b/static/src/app/screens/ticket_screen/ticket_screen.js @@ -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); + } +}); diff --git a/static/src/app/services/pos_store.js b/static/src/app/services/pos_store.js index c7644de..d536bfc 100644 --- a/static/src/app/services/pos_store.js +++ b/static/src/app/services/pos_store.js @@ -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; + } } }); + diff --git a/static/src/app/transfer_state.js b/static/src/app/transfer_state.js new file mode 100644 index 0000000..b12ff9b --- /dev/null +++ b/static/src/app/transfer_state.js @@ -0,0 +1,3 @@ +export const transferState = { + isTransferring: false, +};