From b97fdbe6fce93ec52e688a2be692a254dc13f651 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 05:56:52 +0530 Subject: [PATCH] refactor(test): use test fixture in subscription (cherry picked from commit 3bdf4f628c40d4e8ac19a41c738a8ba382d90d99) # Conflicts: # erpnext/accounts/doctype/subscription/test_subscription.py --- .../doctype/subscription/test_subscription.py | 525 ++++++++++++++++++ 1 file changed, 525 insertions(+) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index c911e7fe12d..4acddfec7e2 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils.data import ( add_days, add_months, @@ -19,8 +20,532 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") +<<<<<<< HEAD def create_plan(): if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): +======= +class TestSubscription(FrappeTestCase): + def setUp(self): + make_plans() + create_parties() + reset_settings() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() + + def test_create_subscription_with_trial_with_correct_period(self): + subscription = create_subscription( + trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1) + ) + self.assertEqual(subscription.trial_period_start, nowdate()) + self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) + self.assertEqual( + add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) + ) + self.assertEqual( + add_to_date(subscription.current_invoice_start, months=1, days=-1), + get_date_str(subscription.current_invoice_end), + ) + self.assertEqual(subscription.invoices, []) + self.assertEqual(subscription.status, "Trialling") + + def test_create_subscription_without_trial_with_correct_period(self): + subscription = create_subscription() + self.assertEqual(subscription.trial_period_start, None) + self.assertEqual(subscription.trial_period_end, None) + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + # No invoice is created + self.assertEqual(len(subscription.invoices), 0) + self.assertEqual(subscription.status, "Active") + + def test_create_subscription_trial_with_wrong_dates(self): + subscription = create_subscription( + trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True + ) + self.assertRaises(frappe.ValidationError, subscription.save) + + def test_invoice_is_generated_at_end_of_billing_period(self): + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, "2018-01-01") + self.assertEqual(subscription.current_invoice_end, "2018-01-31") + + subscription.process(posting_date="2018-01-31") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.current_invoice_start, "2018-02-01") + self.assertEqual(subscription.current_invoice_end, "2018-02-28") + self.assertEqual(subscription.status, "Unpaid") + + def test_status_goes_back_to_active_after_invoice_is_paid(self): + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(posting_date="2018-01-01") # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.get_current_invoice() + current_invoice = subscription.get_current_invoice() + + self.assertIsNotNone(current_invoice) + + current_invoice.db_set("outstanding_amount", 0) + current_invoice.db_set("status", "Paid") + subscription.process() + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) + self.assertEqual(len(subscription.invoices), 1) + + def test_subscription_cancel_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + + subscription.process(posting_date="2018-01-31") # generate first invoice + # This should change status to Cancelled since grace period is 0 + # And is backdated subscription so subscription will be cancelled after processing + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_unpaid_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_invoice_days_until_due(self): + _date = add_months(nowdate(), -1) + subscription = create_subscription(start_date=_date, days_until_due=10) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Active") + + def test_subscription_is_past_due_doesnt_change_within_grace_period(self): + settings = frappe.get_single("Subscription Settings") + grace_period = settings.grace_period + settings.grace_period = 1000 + settings.save() + + subscription = create_subscription(start_date=add_days(nowdate(), -1000)) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + # Grace period is 1000 days so status should remain as Past Due Date + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + settings.grace_period = grace_period + settings.save() + + def test_subscription_remains_active_during_invoice_period(self): + subscription = create_subscription() # no changes expected + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected yet still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + def test_subscription_cancellation(self): + subscription = create_subscription() + subscription.cancel_subscription() + + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_cancellation_invoices(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + + self.assertEqual(subscription.status, "Active") + + subscription.cancel_subscription() + # Invoice must have been generated + self.assertEqual(len(subscription.invoices), 1) + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual( + flt( + get_prorata_factor( + subscription.current_invoice_end, + subscription.current_invoice_start, + cint(subscription.generate_invoice_at == "Beginning of the current subscription period"), + ), + 2, + ), + flt(prorate_factor, 2), + ) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + self.assertEqual(subscription.status, "Cancelled") + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_false(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 0 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.grand_total, 900) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process() # generate first invoice + + # Generate an invoice for the cancelled period + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_restart_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.grace_period = 0 + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + + subscription.restart_subscription() + self.assertEqual(subscription.status, "Active") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_unpaid_back_to_active(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(subscription.current_invoice_start) # generate first invoice + # This should change status to Unpaid since grace period is 0 + self.assertEqual(subscription.status, "Unpaid") + + invoice = subscription.get_current_invoice() + invoice.db_set("outstanding_amount", 0) + invoice.db_set("status", "Paid") + + subscription.process() + self.assertEqual(subscription.status, "Active") + + # A new invoice is generated + subscription.process(posting_date=subscription.current_invoice_start) + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_restart_active_subscription(self): + subscription = create_subscription() + self.assertRaises(frappe.ValidationError, subscription.restart_subscription) + + def test_subscription_invoice_discount_percentage(self): + subscription = create_subscription(additional_discount_percentage=10) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.additional_discount_percentage, 10) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_subscription_invoice_discount_amount(self): + subscription = create_subscription(additional_discount_amount=11) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.discount_amount, 11) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_prepaid_subscriptions(self): + # Create a non pre-billed subscription, processing should not create + # invoices. + subscription = create_subscription() + subscription.process() + self.assertEqual(len(subscription.invoices), 0) + + # Change the subscription type to prebilled and process it. + # Prepaid invoice should be generated + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.save() + subscription.process() + + self.assertEqual(len(subscription.invoices), 1) + + def test_prepaid_subscriptions_with_prorate_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription( + generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process() + subscription.cancel_subscription() + + self.assertEqual(len(subscription.invoices), 1) + + current_inv = subscription.get_current_invoice() + self.assertEqual(current_inv.status, "Unpaid") + + prorate_factor = 1 + + self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_with_follow_calendar_months(self): + subscription = frappe.new_doc("Subscription") + subscription.company = "_Test Company" + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.follow_calendar_months = 1 + + # select subscription start date as "2018-01-15" + subscription.start_date = "2018-01-15" + subscription.end_date = "2018-07-15" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) + subscription.save() + + # even though subscription starts at "2018-01-15" and Billing interval is Month and count 3 + # First invoice will end at "2018-03-31" instead of "2018-04-14" + self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") + + def test_subscription_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + party_type="Supplier", + party="_Test Supplier", + generate_invoice_at="Beginning of the current subscription period", + generate_new_invoices_past_due_date=1, + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process(posting_date="2018-01-01") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Now the Subscription is unpaid + # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in + # subscription and the interval between the subscriptions is 3 months + subscription.process(posting_date="2018-04-01") + self.assertEqual(len(subscription.invoices), 2) + + def test_subscription_without_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + + def test_multi_currency_subscription(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], + party="_Test Subscription Customer", + ) + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Check the currency of the created invoice + currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency") + self.assertEqual(currency, "USD") + + def test_subscription_recovery(self): + """Test if Subscription recovers when start/end date run out of sync with created invoices.""" + subscription = create_subscription( + start_date="2021-01-01", + submit_invoice=0, + generate_new_invoices_past_due_date=1, + party="_Test Subscription Customer", + ) + + # create invoices for the first two moths + subscription.process(posting_date="2021-01-31") + + subscription.process(posting_date="2021-02-28") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + # recreate most recent invoice + subscription.process(posting_date="2022-01-31") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + def test_subscription_invoice_generation_before_days(self): + subscription = create_subscription( + start_date="2023-01-01", + generate_invoice_at="Days before the current subscription period", + number_of_days=10, + generate_new_invoices_past_due_date=1, + ) + + subscription.process(posting_date="2022-12-22") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date="2023-01-22") + self.assertEqual(len(subscription.invoices), 2) + + +def make_plans(): + create_plan(plan_name="_Test Plan Name", cost=900) + create_plan(plan_name="_Test Plan Name 2", cost=1999) + create_plan( + plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 + ) + create_plan( + plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 + ) + create_plan( + plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" + ) + + +def create_plan(**kwargs): + if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")): +>>>>>>> 3bdf4f628c (refactor(test): use test fixture in subscription) plan = frappe.new_doc("Subscription Plan") plan.plan_name = "_Test Plan Name" plan.item = "_Test Non Stock Item"