fix: prevent POS crashes by adding PosOrderLine setup patches and strengthening reward line orphan guards
This commit is contained in:
parent
855f770934
commit
2f7ec27c9a
@ -1,38 +1,59 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
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 OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
|
||||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||||
import { patch } from "@web/core/utils/patch";
|
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.
|
* 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) {
|
function purgeOrphanedRewardLines(order) {
|
||||||
if (!order || !order.lines) {
|
if (!order || !order.lines) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orphans = order.lines.filter(
|
const orphans = order.lines.filter(
|
||||||
(line) =>
|
(line) => line.is_reward_line && (!line.reward_id || !line.reward_id.program_id)
|
||||||
line.is_reward_line &&
|
|
||||||
(!line.reward_id || !line.reward_id.program_id)
|
|
||||||
);
|
);
|
||||||
for (const line of orphans) {
|
for (const line of orphans) {
|
||||||
try {
|
try {
|
||||||
line.delete();
|
line.delete();
|
||||||
} catch (e) {
|
} 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 ─────────────────────────────────────────────────────────
|
// ─── PosOrder patches ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
patch(PosOrder.prototype, {
|
patch(PosOrder.prototype, {
|
||||||
/**
|
/**
|
||||||
* getLoyaltyPoints reads coupon_id.id — guard against undefined coupon_id.
|
* getLoyaltyPoints reads coupon_id.id — guard undefined coupon_id.
|
||||||
*/
|
*/
|
||||||
getLoyaltyPoints() {
|
getLoyaltyPoints() {
|
||||||
const undefinedCouponLines = [];
|
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() {
|
_updateRewardLines() {
|
||||||
purgeOrphanedRewardLines(this);
|
purgeOrphanedRewardLines(this);
|
||||||
@ -60,8 +81,18 @@ patch(PosOrder.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* initState fires when an order is initialized (including from server restore).
|
* _validForPointsCorrection — core accesses reward.program_id.id without null check.
|
||||||
* Schedule a purge on the next microtask so reactive relations are fully resolved first.
|
* 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() {
|
initState() {
|
||||||
super.initState(...arguments);
|
super.initState(...arguments);
|
||||||
@ -94,8 +125,7 @@ patch(OrderPaymentValidation.prototype, {
|
|||||||
|
|
||||||
patch(PosStore.prototype, {
|
patch(PosStore.prototype, {
|
||||||
/**
|
/**
|
||||||
* afterProcessServerData — earliest global hook after all records are loaded.
|
* afterProcessServerData — purge orphaned lines from ALL orders after load.
|
||||||
* Purge orphaned reward lines from ALL open orders.
|
|
||||||
*/
|
*/
|
||||||
async afterProcessServerData() {
|
async afterProcessServerData() {
|
||||||
for (const order of this.models["pos.order"].getAll()) {
|
for (const order of this.models["pos.order"].getAll()) {
|
||||||
@ -105,8 +135,7 @@ patch(PosStore.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* updateRewards — called on every order-line change.
|
* updateRewards — purge before loyalty recalculation.
|
||||||
* Purge orphaned lines from the current order before loyalty recalculation.
|
|
||||||
*/
|
*/
|
||||||
updateRewards() {
|
updateRewards() {
|
||||||
const order = this.getOrder();
|
const order = this.getOrder();
|
||||||
@ -117,8 +146,7 @@ patch(PosStore.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* orderUpdateLoyaltyPrograms — called when switching tables or rescanning codes.
|
* orderUpdateLoyaltyPrograms — purge before loyalty program update (table switch path).
|
||||||
* This is the path triggered by "Check Table" → `setTableFromUi` → `updateOrder`.
|
|
||||||
*/
|
*/
|
||||||
async orderUpdateLoyaltyPrograms() {
|
async orderUpdateLoyaltyPrograms() {
|
||||||
const order = this.getOrder();
|
const order = this.getOrder();
|
||||||
@ -129,8 +157,7 @@ patch(PosStore.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setTableFromUi — the exact entry point for the "Check Table → blank screen" crash.
|
* setTableFromUi — "Check Table" entry point. Purge ALL orders before switching.
|
||||||
* Purge ALL orders before switching so the target table's order is clean.
|
|
||||||
*/
|
*/
|
||||||
async setTableFromUi(table, orderUuid = null) {
|
async setTableFromUi(table, orderUuid = null) {
|
||||||
for (const order of this.models["pos.order"].getAll()) {
|
for (const order of this.models["pos.order"].getAll()) {
|
||||||
@ -140,14 +167,11 @@ patch(PosStore.prototype, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* postProcessLoyalty — called when pushing/validating an order.
|
* postProcessLoyalty — guards reward.program_id AND coupon_id during order push.
|
||||||
* Guards both reward.program_id === undefined AND coupon_id === undefined.
|
|
||||||
*/
|
*/
|
||||||
async postProcessLoyalty(order) {
|
async postProcessLoyalty(order) {
|
||||||
// Remove lines with missing program_id (core reads reward.program_id.id)
|
|
||||||
purgeOrphanedRewardLines(order);
|
purgeOrphanedRewardLines(order);
|
||||||
|
|
||||||
// Also guard coupon_id=undefined (original safe_coupon behavior)
|
|
||||||
const undefinedCouponLines = [];
|
const undefinedCouponLines = [];
|
||||||
for (const line of order._get_reward_lines()) {
|
for (const line of order._get_reward_lines()) {
|
||||||
if (!line.coupon_id) {
|
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;
|
const originalCall = this.data.call;
|
||||||
this.data.call = function (model, method, args) {
|
this.data.call = function (model, method, args) {
|
||||||
if (model === "pos.order" && method === "confirm_coupon_programs") {
|
if (model === "pos.order" && method === "confirm_coupon_programs") {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user