From 2f7ec27c9a2344b80ef24d3f95658ca634a4894a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 29 May 2026 16:14:27 +0700 Subject: [PATCH] fix: prevent POS crashes by adding PosOrderLine setup patches and strengthening reward line orphan guards --- .../src/app/pos_loyalty_safe_coupon_patch.js | 69 ++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/static/src/app/pos_loyalty_safe_coupon_patch.js b/static/src/app/pos_loyalty_safe_coupon_patch.js index a84a9bf..3574cc8 100644 --- a/static/src/app/pos_loyalty_safe_coupon_patch.js +++ b/static/src/app/pos_loyalty_safe_coupon_patch.js @@ -1,38 +1,59 @@ /** @odoo-module **/ import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { PosOrderLine } from "@point_of_sale/app/models/pos_order_line"; import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation"; import { PosStore } from "@point_of_sale/app/services/pos_store"; import { patch } from "@web/core/utils/patch"; /** * Helper: delete all reward lines in an order where reward_id or reward_id.program_id is missing. - * This prevents TypeError crashes when the core code accesses reward.program_id.xxx - * on lines whose loyalty program was archived, deleted, or not yet loaded into the POS cache. */ function purgeOrphanedRewardLines(order) { if (!order || !order.lines) { return; } const orphans = order.lines.filter( - (line) => - line.is_reward_line && - (!line.reward_id || !line.reward_id.program_id) + (line) => line.is_reward_line && (!line.reward_id || !line.reward_id.program_id) ); for (const line of orphans) { try { line.delete(); } catch (e) { - // Ignore deletion errors — the line may already be gone + // ignore } } } +// ─── PosOrderLine: earliest possible interception ───────────────────────────── +// The crash happens at Proxy.create → renderRecords, which fires DURING line creation. +// Patching setup() lets us mark orphaned lines for deletion synchronously, +// before OWL components re-render and crash on reward_id.program_id. + +patch(PosOrderLine.prototype, { + setup(vals) { + super.setup(...arguments); + // If this reward line has no valid program, schedule deletion immediately. + // We use Promise.resolve (microtask) because we cannot delete during setup(). + if (this.is_reward_line && (!this.reward_id || !this.reward_id.program_id)) { + Promise.resolve().then(() => { + try { + if (this.is_reward_line && (!this.reward_id || !this.reward_id.program_id)) { + this.delete(); + } + } catch (e) { + // ignore + } + }); + } + }, +}); + // ─── PosOrder patches ───────────────────────────────────────────────────────── patch(PosOrder.prototype, { /** - * getLoyaltyPoints reads coupon_id.id — guard against undefined coupon_id. + * getLoyaltyPoints reads coupon_id.id — guard undefined coupon_id. */ getLoyaltyPoints() { const undefinedCouponLines = []; @@ -52,7 +73,7 @@ patch(PosOrder.prototype, { }, /** - * _updateRewardLines rebuilds reward lines — purge orphans before core touches them. + * _updateRewardLines — purge orphans before core re-processes them. */ _updateRewardLines() { purgeOrphanedRewardLines(this); @@ -60,8 +81,18 @@ patch(PosOrder.prototype, { }, /** - * initState fires when an order is initialized (including from server restore). - * Schedule a purge on the next microtask so reactive relations are fully resolved first. + * _validForPointsCorrection — core accesses reward.program_id.id without null check. + * Guard it here. + */ + _validForPointsCorrection(reward, line, rule) { + if (!reward || !reward.program_id || !rule.program_id) { + return false; + } + return super._validForPointsCorrection(...arguments); + }, + + /** + * initState — purge on next microtask after order initialization. */ initState() { super.initState(...arguments); @@ -94,8 +125,7 @@ patch(OrderPaymentValidation.prototype, { patch(PosStore.prototype, { /** - * afterProcessServerData — earliest global hook after all records are loaded. - * Purge orphaned reward lines from ALL open orders. + * afterProcessServerData — purge orphaned lines from ALL orders after load. */ async afterProcessServerData() { for (const order of this.models["pos.order"].getAll()) { @@ -105,8 +135,7 @@ patch(PosStore.prototype, { }, /** - * updateRewards — called on every order-line change. - * Purge orphaned lines from the current order before loyalty recalculation. + * updateRewards — purge before loyalty recalculation. */ updateRewards() { const order = this.getOrder(); @@ -117,8 +146,7 @@ patch(PosStore.prototype, { }, /** - * orderUpdateLoyaltyPrograms — called when switching tables or rescanning codes. - * This is the path triggered by "Check Table" → `setTableFromUi` → `updateOrder`. + * orderUpdateLoyaltyPrograms — purge before loyalty program update (table switch path). */ async orderUpdateLoyaltyPrograms() { const order = this.getOrder(); @@ -129,8 +157,7 @@ patch(PosStore.prototype, { }, /** - * setTableFromUi — the exact entry point for the "Check Table → blank screen" crash. - * Purge ALL orders before switching so the target table's order is clean. + * setTableFromUi — "Check Table" entry point. Purge ALL orders before switching. */ async setTableFromUi(table, orderUuid = null) { for (const order of this.models["pos.order"].getAll()) { @@ -140,14 +167,11 @@ patch(PosStore.prototype, { }, /** - * postProcessLoyalty — called when pushing/validating an order. - * Guards both reward.program_id === undefined AND coupon_id === undefined. + * postProcessLoyalty — guards reward.program_id AND coupon_id during order push. */ async postProcessLoyalty(order) { - // Remove lines with missing program_id (core reads reward.program_id.id) purgeOrphanedRewardLines(order); - // Also guard coupon_id=undefined (original safe_coupon behavior) const undefinedCouponLines = []; for (const line of order._get_reward_lines()) { if (!line.coupon_id) { @@ -156,7 +180,6 @@ patch(PosStore.prototype, { } } - // Strip dummy coupon key "0" from confirm_coupon_programs calls const originalCall = this.data.call; this.data.call = function (model, method, args) { if (model === "pos.order" && method === "confirm_coupon_programs") {