From eaab71a99ec36207c9d6881662b4006aca43754e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Jun 2026 14:20:41 +0530 Subject: [PATCH 1/2] test(coupon_code): cover coupon validation and usage-count edges Add tests for the previously-untested branches of validate_coupon_code (not-yet-valid, expired, maximum-use exhausted) and update_coupon_code_count (releasing a use on cancel, and rejecting use beyond the maximum). Both functions are now fully covered. --- .../doctype/coupon_code/test_coupon_code.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index ba4e7c0b191..98bdfb80f43 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -173,3 +173,66 @@ class TestCouponCode(ERPNextTestSuite): # Clean up coupon.delete() + + def test_validate_coupon_code_rejections(self): + from frappe.utils import add_days, nowdate + + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + + pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}) + + def make_coupon(name, **kwargs): + frappe.delete_doc_if_exists("Coupon Code", name) + return frappe.get_doc( + { + "doctype": "Coupon Code", + "coupon_name": name, + "coupon_code": name, + "coupon_type": "Promotional", + "pricing_rule": pricing_rule, + **kwargs, + } + ).insert(ignore_permissions=True) + + with self.subTest("validity not yet started"): + make_coupon("_Test Coupon Future", valid_from=add_days(nowdate(), 5)) + self.assertRaises(frappe.ValidationError, validate_coupon_code, "_Test Coupon Future") + + with self.subTest("validity expired"): + make_coupon("_Test Coupon Expired", valid_upto=add_days(nowdate(), -5)) + self.assertRaises(frappe.ValidationError, validate_coupon_code, "_Test Coupon Expired") + + with self.subTest("maximum use exhausted"): + make_coupon("_Test Coupon Exhausted", maximum_use=2, used=2) + self.assertRaises(frappe.ValidationError, validate_coupon_code, "_Test Coupon Exhausted") + + with self.subTest("valid coupon passes"): + make_coupon("_Test Coupon Valid", maximum_use=5, used=1, valid_upto=add_days(nowdate(), 5)) + validate_coupon_code("_Test Coupon Valid") # no raise + + def test_update_coupon_code_count_cancel_and_exhaust(self): + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + + pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}) + frappe.delete_doc_if_exists("Coupon Code", "_Test Coupon Count") + frappe.get_doc( + { + "doctype": "Coupon Code", + "coupon_name": "_Test Coupon Count", + "coupon_code": "_Test Coupon Count", + "coupon_type": "Promotional", + "pricing_rule": pricing_rule, + "maximum_use": 2, + "used": 1, + } + ).insert(ignore_permissions=True) + + # cancelling a transaction releases one use + update_coupon_code_count("_Test Coupon Count", "cancelled") + self.assertEqual(frappe.db.get_value("Coupon Code", "_Test Coupon Count", "used"), 0) + + # using up to the maximum is allowed, beyond it is rejected + update_coupon_code_count("_Test Coupon Count", "used") + update_coupon_code_count("_Test Coupon Count", "used") + self.assertEqual(frappe.db.get_value("Coupon Code", "_Test Coupon Count", "used"), 2) + self.assertRaises(frappe.ValidationError, update_coupon_code_count, "_Test Coupon Count", "used") From c5ff32aa2faf0ac8f6d60557709241f3c1659989 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Jun 2026 14:27:16 +0530 Subject: [PATCH 2/2] refactor: tidy update_coupon_code_count Drop the dead 'if coupon:' guard (get_doc would have thrown) and collapse the duplicate increment branches into a single exhausted-check plus increment. No behaviour change. --- .../accounts/doctype/pricing_rule/utils.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index d41038027a7..ca1e6f2f129 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -756,21 +756,16 @@ def validate_coupon_code(coupon_name): def update_coupon_code_count(coupon_name, transaction_type): coupon = frappe.get_doc("Coupon Code", coupon_name) - if coupon: - if transaction_type == "used": - if not coupon.maximum_use: - coupon.used = coupon.used + 1 - coupon.save(ignore_permissions=True) - elif coupon.used < coupon.maximum_use: - coupon.used = coupon.used + 1 - coupon.save(ignore_permissions=True) - else: - frappe.throw( - _("{0} Coupon used are {1}. Allowed quantity is exhausted").format( - coupon.coupon_code, coupon.used - ) + if transaction_type == "used": + if coupon.maximum_use and coupon.used >= coupon.maximum_use: + frappe.throw( + _("{0} Coupon used are {1}. Allowed quantity is exhausted").format( + coupon.coupon_code, coupon.used ) - elif transaction_type == "cancelled": - if coupon.used > 0: - coupon.used = coupon.used - 1 - coupon.save(ignore_permissions=True) + ) + coupon.used = coupon.used + 1 + coupon.save(ignore_permissions=True) + elif transaction_type == "cancelled": + if coupon.used > 0: + coupon.used = coupon.used - 1 + coupon.save(ignore_permissions=True)