From 3167e8ba77c78dff76281cb67a7f6a132d0a968e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 11:30:35 +0530 Subject: [PATCH 1/3] test: add coverage for Subscription Plan --- .../test_subscription_plan.py | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py index 76328f9e4c3..062f924a96f 100644 --- a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py @@ -1,8 +1,58 @@ -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import unittest + +import frappe + +from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.tests.utils import ERPNextTestSuite class TestSubscriptionPlan(ERPNextTestSuite): - pass + """Subscription Plan validates its interval and computes a rate. The Monthly + Rate branch multiplies cost by the number of months in the billing window.""" + + def setUp(self): + frappe.set_user("Administrator") + + def make_plan(self, **args): + args = frappe._dict(args) + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = f"_Test Plan {frappe.generate_hash(length=6)}" + plan.item = args.item or "_Test Item" + plan.currency = args.currency or "INR" + plan.price_determination = args.price_determination + plan.cost = args.cost or 0 + plan.billing_interval = args.billing_interval or "Month" + plan.billing_interval_count = ( + args.billing_interval_count if args.billing_interval_count is not None else 1 + ) + return plan + + def test_billing_interval_count_must_be_positive(self): + plan = self.make_plan(price_determination="Fixed Rate", cost=100, billing_interval_count=0) + self.assertRaises(frappe.ValidationError, plan.insert) + + def test_fixed_rate_applies_prorate_factor(self): + plan = self.make_plan(price_determination="Fixed Rate", cost=100) + plan.insert() + self.assertEqual(get_plan_rate(plan.name), 100) + self.assertEqual(get_plan_rate(plan.name, prorate_factor=0.5), 50) + + def test_monthly_rate_within_year(self): + plan = self.make_plan(price_determination="Monthly Rate", cost=100) + plan.insert() + # Jan 1 - Mar 31 is 3 whole months; month-aligned so proration is 0 + rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2026-03-31") + self.assertEqual(rate, 300) + + @unittest.expectedFailure + def test_monthly_rate_across_year_boundary(self): + # SUSPECTED BUG: no_of_months uses relativedelta(end, start).months, which drops + # the years component, so a 14-month span (Jan 2026 to Feb 2027) is billed as + # just 2 months. Asserts the correct 14-month total; drop the xfail once fixed. + plan = self.make_plan(price_determination="Monthly Rate", cost=100) + plan.insert() + rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2027-02-28") + self.assertEqual(rate, 1400) From 832b5a56bf1181f4c448eacbfeb9ce734e0459d3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 12:05:31 +0530 Subject: [PATCH 2/3] test: lock current cross-year monthly-rate underbilling value --- .../subscription_plan/test_subscription_plan.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py index 062f924a96f..48bf885a813 100644 --- a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py @@ -1,8 +1,6 @@ # Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate @@ -47,12 +45,12 @@ class TestSubscriptionPlan(ERPNextTestSuite): rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2026-03-31") self.assertEqual(rate, 300) - @unittest.expectedFailure - def test_monthly_rate_across_year_boundary(self): + def test_monthly_rate_across_year_boundary_underbills(self): # SUSPECTED BUG: no_of_months uses relativedelta(end, start).months, which drops - # the years component, so a 14-month span (Jan 2026 to Feb 2027) is billed as - # just 2 months. Asserts the correct 14-month total; drop the xfail once fixed. + # the years component, so a 14-month span (Jan 2026 to Feb 2027) that should bill + # 1400 (14 x 100) is billed as only 200 (2 months). Locking the current (wrong) + # value so a fix trips this test; the correct expectation is 1400. plan = self.make_plan(price_determination="Monthly Rate", cost=100) plan.insert() rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2027-02-28") - self.assertEqual(rate, 1400) + self.assertEqual(rate, 200) From 196730c53563f2fdcfe62df21c1718f15f59e2e0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 3 Jul 2026 14:08:20 +0530 Subject: [PATCH 3/3] fix: bill all months across a year boundary in Monthly Rate plans --- .../doctype/subscription_plan/subscription_plan.py | 4 +++- .../subscription_plan/test_subscription_plan.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 932caaa2db2..630cc39b0b1 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -79,7 +79,9 @@ def get_plan_rate( start_date = getdate(start_date) end_date = getdate(end_date) - no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1 + delta = relativedelta.relativedelta(end_date, start_date) + # include the years component so cross-year spans aren't under-counted + no_of_months = delta.years * 12 + delta.months + 1 cost = plan.cost * no_of_months # Adjust cost if start or end date is not month start or end diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py index 48bf885a813..057e083eda0 100644 --- a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py @@ -45,12 +45,10 @@ class TestSubscriptionPlan(ERPNextTestSuite): rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2026-03-31") self.assertEqual(rate, 300) - def test_monthly_rate_across_year_boundary_underbills(self): - # SUSPECTED BUG: no_of_months uses relativedelta(end, start).months, which drops - # the years component, so a 14-month span (Jan 2026 to Feb 2027) that should bill - # 1400 (14 x 100) is billed as only 200 (2 months). Locking the current (wrong) - # value so a fix trips this test; the correct expectation is 1400. + def test_monthly_rate_across_year_boundary(self): + # a 14-month span (Jan 2026 to Feb 2027) bills all 14 months, not just the + # 2-month remainder that relativedelta.months alone would give plan = self.make_plan(price_determination="Monthly Rate", cost=100) plan.insert() rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2027-02-28") - self.assertEqual(rate, 200) + self.assertEqual(rate, 1400)