first commit

This commit is contained in:
Suherdy Yacob 2026-05-21 11:08:20 +07:00
commit e567862552
4 changed files with 113 additions and 0 deletions

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# POS Loyalty Safe Coupon Patch
This custom Odoo module resolves a core Odoo 19 bug in the POS Loyalty module where standard Odoo code makes unsafe direct reads on `coupon_id.id` of reward lines.
When an order is restored (e.g., during table switching in a restaurant) and references a coupon that is not yet fully loaded in the local client registry, Odoo leaves `coupon_id` as `undefined`. Accessing `.id` on this undefined object crashes the POS UI.
## How it works
Without modifying any core Odoo code, this module dynamically patches:
1. `PosOrder.getLoyaltyPoints()`
2. `OrderPaymentValidation.validateOrder()`
3. `PosStore.postProcessLoyalty()`
During their respective execution, it temporarily mocks any missing `coupon_id` references with `{ id: 0 }`. It also wraps `this.data.call` during `postProcessLoyalty` to ensure dummy coupon records with ID `0` are filtered out before being communicated back to the Odoo backend server. After execution, the original `undefined` state of the coupon is safely restored.
This ensures zero side effects, maximum stability, and clean compatibility with standard/custom addons.

2
__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

21
__manifest__.py Normal file
View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
{
'name': 'POS Loyalty Safe Coupon Patch',
'version': '1.0',
'category': 'Point of Sale',
'summary': 'Fixes TypeError on undefined coupon_id in Odoo POS Loyalty',
'description': """
POS Loyalty Safe Coupon Patch
=============================
This module overrides POS models and helper methods inside the POS loyalty app to safely handle undefined coupon_id structures, preventing POS interface crashes (e.g. when changing tables or updating loyalty).
""",
'author': 'Antigravity',
'depends': ['pos_loyalty'],
'assets': {
'point_of_sale._assets_pos': [
'pos_loyalty_safe_coupon/static/src/app/pos_loyalty_safe_coupon_patch.js',
],
},
'installable': True,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,75 @@
import { PosOrder } from "@point_of_sale/app/models/pos_order";
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";
patch(PosOrder.prototype, {
getLoyaltyPoints() {
const undefinedCouponLines = [];
for (const line of this._get_reward_lines()) {
if (!line.coupon_id) {
line.coupon_id = { id: 0 };
undefinedCouponLines.push(line);
}
}
try {
return super.getLoyaltyPoints(...arguments);
} finally {
for (const line of undefinedCouponLines) {
line.coupon_id = undefined;
}
}
}
});
patch(OrderPaymentValidation.prototype, {
async validateOrder(isForceValidate) {
const undefinedCouponLines = [];
for (const line of this.order._get_reward_lines()) {
if (!line.coupon_id) {
line.coupon_id = { id: 0 };
undefinedCouponLines.push(line);
}
}
try {
return await super.validateOrder(...arguments);
} finally {
for (const line of undefinedCouponLines) {
line.coupon_id = undefined;
}
}
}
});
patch(PosStore.prototype, {
async postProcessLoyalty(order) {
const undefinedCouponLines = [];
for (const line of order._get_reward_lines()) {
if (!line.coupon_id) {
line.coupon_id = { id: 0 };
undefinedCouponLines.push(line);
}
}
// Intercept data.call to strip out the dummy coupon data with key "0"
const originalCall = this.data.call;
this.data.call = function (model, method, args) {
if (model === "pos.order" && method === "confirm_coupon_programs") {
const couponData = args[1];
if (couponData && "0" in couponData) {
delete couponData["0"];
}
}
return originalCall.apply(this, arguments);
};
try {
return await super.postProcessLoyalty(...arguments);
} finally {
this.data.call = originalCall;
for (const line of undefinedCouponLines) {
line.coupon_id = undefined;
}
}
}
});