From 35e53b04bb34847007c0ac5fabd7aa4dbda7b95f Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Thu, 23 Oct 2025 10:11:19 +0200 Subject: [PATCH 1/6] feat: handle credit refund on backend --- .../doctype/booking_credit_usage/booking_credit_usage.py | 3 +++ erpnext/venue/doctype/item_booking/item_booking.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/erpnext/venue/doctype/booking_credit_usage/booking_credit_usage.py b/erpnext/venue/doctype/booking_credit_usage/booking_credit_usage.py index cd54749643..4d1b02d5fe 100644 --- a/erpnext/venue/doctype/booking_credit_usage/booking_credit_usage.py +++ b/erpnext/venue/doctype/booking_credit_usage/booking_credit_usage.py @@ -54,6 +54,9 @@ def add_booking_credit_usage(doc, method): if not doc.deduct_booking_credits: return + + if doc.status == "Cancelled": + return if frappe.db.exists("Booking Credit Usage", {"item_booking": doc.name, "docstatus": 1}): return diff --git a/erpnext/venue/doctype/item_booking/item_booking.py b/erpnext/venue/doctype/item_booking/item_booking.py index ca53055ad2..7a1f507fa4 100644 --- a/erpnext/venue/doctype/item_booking/item_booking.py +++ b/erpnext/venue/doctype/item_booking/item_booking.py @@ -496,9 +496,18 @@ class ItemBooking(Document): formatted_duration ) ) + + if self.deduct_booking_credits and frappe.db.get_single_value("Venue Settings", "refund_credit"): + self.refund_credit() self.set_status("Cancelled") + def refund_credit(self): + for bcu in frappe.db.get_all("Booking Credit Usage", filters = {"item_booking": self.name, "docstatus": 1}): + doc = frappe.get_doc("Booking Credit Usage", bcu.name) + doc.flags.ignore_permissions = True + doc.cancel() + @frappe.whitelist() def end_now(self): new_end = now_datetime() -- GitLab From 1956b852b841590bbcb9557160d203be587dd9be Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Thu, 23 Oct 2025 10:11:40 +0200 Subject: [PATCH 2/6] feat: credit refund setting in venue --- erpnext/venue/doctype/venue_settings/venue_settings.json | 8 ++++++++ erpnext/venue/doctype/venue_settings/venue_settings.py | 1 + 2 files changed, 9 insertions(+) diff --git a/erpnext/venue/doctype/venue_settings/venue_settings.json b/erpnext/venue/doctype/venue_settings/venue_settings.json index bfa33cb20f..8ad7663b22 100644 --- a/erpnext/venue/doctype/venue_settings/venue_settings.json +++ b/erpnext/venue/doctype/venue_settings/venue_settings.json @@ -15,6 +15,7 @@ "allow_event_cancellation", "display_subscriptions_in_calendar", "cancellation_delay", + "refund_credit", "role_allowed_to_skip_cart", "event_registration_section", "registration_item_code", @@ -98,6 +99,13 @@ "fieldtype": "Duration", "label": "Cancellation delay" }, + { + "default": "0", + "depends_on": "eval:doc.allow_event_cancellation", + "fieldname": "refund_credit", + "fieldtype": "Check", + "label": "Refund credit on cancellation" + }, { "default": "0", "description": "If not set, the booking will be considered confirmed when the order is placed", diff --git a/erpnext/venue/doctype/venue_settings/venue_settings.py b/erpnext/venue/doctype/venue_settings/venue_settings.py index 62facbdc1b..3fd089c80b 100644 --- a/erpnext/venue/doctype/venue_settings/venue_settings.py +++ b/erpnext/venue/doctype/venue_settings/venue_settings.py @@ -36,6 +36,7 @@ class VenueSettings(Document): allow_event_cancellation: DF.Check cancellation_delay: DF.Duration | None cart_settings_overrides: DF.Table[VenueCartSettings] + refund_credit: DF.Check clear_item_booking_draft_duration: DF.Int confirm_booking_after_payment: DF.Check day_uom: DF.Link | None -- GitLab From b5777cb8a18ae182cf3a4c4b23abaa13e0a1759f Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Thu, 23 Oct 2025 10:40:36 +0200 Subject: [PATCH 3/6] test: integration test item booking --- .../doctype/item_booking/test_item_booking.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/erpnext/venue/doctype/item_booking/test_item_booking.py b/erpnext/venue/doctype/item_booking/test_item_booking.py index 4b26252228..bb1d82301a 100644 --- a/erpnext/venue/doctype/item_booking/test_item_booking.py +++ b/erpnext/venue/doctype/item_booking/test_item_booking.py @@ -174,6 +174,7 @@ class BaseTestWithBookableItem(FrappeTestCase): all_day=False, uom="Hour", status="Confirmed", + deduct_booking_credits=0, ): booking = frappe.get_doc( { @@ -186,6 +187,7 @@ class BaseTestWithBookableItem(FrappeTestCase): "all_day": all_day, "uom": uom, "sync_with_google_calendar": False, + "deduct_booking_credits": deduct_booking_credits } ) booking.insert() @@ -544,6 +546,171 @@ class TestItemBooking(BaseTestWithBookableItem): frappe.db.rollback() + def _setup_credit_test(self, credit_type_label="Test Credit Type", credits_per_hour=10, initial_credits=20, customer=TEST_CUSTOMER): + from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + + credit_type = frappe.get_doc({ + "doctype": "Booking Credit Type", + "label": credit_type_label, + "credits": credits_per_hour, + "uom": "Hour", + "item": self.ITEM_BOOKABLE_1.name, + "conversion_table": [{ + "item": self.ITEM_BOOKABLE_1.name, + "uom": "Hour", + "credits": credits_per_hour + }] + }) + credit_type.insert(ignore_permissions=True) + + credit_doc = frappe.get_doc({ + "doctype": "Booking Credit", + "date": frappe.utils.nowdate(), + "customer": customer, + "booking_credit_type": credit_type.name, + "quantity": initial_credits, + }) + credit_doc.insert(ignore_permissions=True) + credit_doc.submit() + + # Verify initial balance + initial_balance = get_balance(customer) + self.assertEqual(initial_balance, {credit_type.name: initial_credits}) + + return credit_type + + def _create_booking_with_credits(self, credit_type, duration_hours=1, customer=TEST_CUSTOMER): + from bookings.bookings.doctype.booking_credit_usage.booking_credit_usage import add_booking_credit_usage + from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + + dt_start = add_to_date(getdate(), days=4, hours=7) + dt_end = add_to_date(dt_start, hours=duration_hours) + + booking = self.makeBookingWithAutocleanup( + self.ITEM_BOOKABLE_1.name, dt_start, dt_end, user=TEST_USER_1, deduct_booking_credits=1 + ) + booking.party_type = "Customer" + booking.party_name = customer + booking.save() + + add_booking_credit_usage(booking, None) + + # Calculate expected remaining balance + credits_used = credit_type.credits * duration_hours + expected_balance = frappe.db.get_value("Booking Credit", + {"customer": customer, "booking_credit_type": credit_type.name}, "quantity") - credits_used + + # Verify credits were deducted + balance = get_balance(customer) + self.assertEqual(balance, {credit_type.name: expected_balance}) + + return booking + + @change_settings( + "Venue Settings", { + "minute_uom": "Minute", + "enable_simultaneous_booking": 1, + "allow_event_cancellation": 1, + "cancellation_delay": 0, + "refund_credit": 1 + } + ) + def test_booking_cancellation_with_credit_refund(self): + """Test that credits are refunded when refund_credit setting is enabled""" + from contextlib import contextmanager + + from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + from bookings.bookings.doctype.resource_booking.resource_booking import cancel_appointment + + @contextmanager + def rollback_test(): + frappe.db.savepoint("test_booking_cancellation_with_credit_refund") + frappe.set_user("Administrator") + try: + yield + finally: + frappe.set_user("Administrator") + frappe.db.rollback(save_point="test_booking_cancellation_with_credit_refund") + + @contextmanager + def user_context(user): + old_user = frappe.session.user + frappe.set_user(user) + try: + yield + finally: + frappe.set_user(old_user) + + with rollback_test(): + credit_type = self._setup_credit_test( + credits_per_hour=10, initial_credits=20 + ) + booking = self._create_booking_with_credits(credit_type, duration_hours=1) + + with user_context(TEST_USER_1): + cancel_appointment(booking.name) + + # Verify credits were refunded (back to initial 20) + balance = get_balance(TEST_CUSTOMER) + self.assertEqual(balance, {credit_type.name: 20}) + + booking.reload() + self.assertEqual(booking.status, "Cancelled") + + @change_settings( + "Venue Settings", { + "minute_uom": "Minute", + "enable_simultaneous_booking": 1, + "allow_event_cancellation": 1, + "cancellation_delay": 0, + "refund_credit": 0 + } + ) + def test_booking_cancellation_without_credit_refund(self): + """Test that credits are NOT refunded when refund_credit setting is disabled""" + from contextlib import contextmanager + + from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + from bookings.bookings.doctype.resource_booking.resource_booking import cancel_appointment + + @contextmanager + def rollback_test(): + frappe.db.savepoint("test_booking_cancellation_without_credit_refund") + frappe.set_user("Administrator") + try: + yield + finally: + frappe.set_user("Administrator") + frappe.db.rollback(save_point="test_booking_cancellation_without_credit_refund") + + @contextmanager + def user_context(user): + old_user = frappe.session.user + frappe.set_user(user) + try: + yield + finally: + frappe.set_user(old_user) + + with rollback_test(): + credit_type = self._setup_credit_test( + credit_type_label="Test Credit Type No Refund", + credits_per_hour=10, + initial_credits=20 + ) + booking = self._create_booking_with_credits(credit_type, duration_hours=1) + + with user_context(TEST_USER_1): + cancel_appointment(booking.name) + + # Verify credits were NOT refunded (remains at 10 after deduction) + balance = get_balance(TEST_CUSTOMER) + self.assertEqual(balance, {credit_type.name: 10}) + + booking.reload() + self.assertEqual(booking.status, "Cancelled") + + class TestItemBookingWithSubscription(BaseTestWithSubscriptionForBookableItem): @change_settings("Venue Settings", {"minute_uom": "Minute", "enable_simultaneous_booking": 1}) def test_subscription_decreases_availability1(self): -- GitLab From 4a60308abc2dc72305a1e098d714039ccbb6300d Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Thu, 23 Oct 2025 10:47:46 +0200 Subject: [PATCH 4/6] test: fix path --- .../doctype/item_booking/test_item_booking.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/venue/doctype/item_booking/test_item_booking.py b/erpnext/venue/doctype/item_booking/test_item_booking.py index bb1d82301a..7a36a48cc5 100644 --- a/erpnext/venue/doctype/item_booking/test_item_booking.py +++ b/erpnext/venue/doctype/item_booking/test_item_booking.py @@ -547,7 +547,7 @@ class TestItemBooking(BaseTestWithBookableItem): def _setup_credit_test(self, credit_type_label="Test Credit Type", credits_per_hour=10, initial_credits=20, customer=TEST_CUSTOMER): - from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance credit_type = frappe.get_doc({ "doctype": "Booking Credit Type", @@ -580,8 +580,8 @@ class TestItemBooking(BaseTestWithBookableItem): return credit_type def _create_booking_with_credits(self, credit_type, duration_hours=1, customer=TEST_CUSTOMER): - from bookings.bookings.doctype.booking_credit_usage.booking_credit_usage import add_booking_credit_usage - from bookings.bookings.doctype.booking_credit.booking_credit import get_balance + from erpnext.erpnext.doctype.booking_credit_usage.booking_credit_usage import add_booking_credit_usage + from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance dt_start = add_to_date(getdate(), days=4, hours=7) dt_end = add_to_date(dt_start, hours=duration_hours) @@ -619,8 +619,8 @@ class TestItemBooking(BaseTestWithBookableItem): """Test that credits are refunded when refund_credit setting is enabled""" from contextlib import contextmanager - from bookings.bookings.doctype.booking_credit.booking_credit import get_balance - from bookings.bookings.doctype.resource_booking.resource_booking import cancel_appointment + from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance + from erpnext.erpnext.doctype.item_booking.item_booking import cancel_appointment @contextmanager def rollback_test(): @@ -670,8 +670,8 @@ class TestItemBooking(BaseTestWithBookableItem): """Test that credits are NOT refunded when refund_credit setting is disabled""" from contextlib import contextmanager - from bookings.bookings.doctype.booking_credit.booking_credit import get_balance - from bookings.bookings.doctype.resource_booking.resource_booking import cancel_appointment + from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance + from erpnext.erpnext.doctype.item_booking.item_booking import cancel_appointment @contextmanager def rollback_test(): -- GitLab From 83d558ffbd861252ca9f6d8362a8b71653d93d28 Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Thu, 23 Oct 2025 13:07:08 +0200 Subject: [PATCH 5/6] test: fix path (again) --- erpnext/venue/doctype/item_booking/test_item_booking.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/venue/doctype/item_booking/test_item_booking.py b/erpnext/venue/doctype/item_booking/test_item_booking.py index 7a36a48cc5..fc5a9225f7 100644 --- a/erpnext/venue/doctype/item_booking/test_item_booking.py +++ b/erpnext/venue/doctype/item_booking/test_item_booking.py @@ -619,8 +619,8 @@ class TestItemBooking(BaseTestWithBookableItem): """Test that credits are refunded when refund_credit setting is enabled""" from contextlib import contextmanager - from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance - from erpnext.erpnext.doctype.item_booking.item_booking import cancel_appointment + from erpnext.venue.doctype.booking_credit.booking_credit import get_balance + from erpnext.venue.doctype.item_booking.item_booking import cancel_appointment @contextmanager def rollback_test(): @@ -670,8 +670,8 @@ class TestItemBooking(BaseTestWithBookableItem): """Test that credits are NOT refunded when refund_credit setting is disabled""" from contextlib import contextmanager - from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance - from erpnext.erpnext.doctype.item_booking.item_booking import cancel_appointment + from erpnext.venue.doctype.booking_credit.booking_credit import get_balance + from erpnext.venue.doctype.item_booking.item_booking import cancel_appointment @contextmanager def rollback_test(): -- GitLab From edf8359381111543aa573ffbe22e1186bb8ed6b6 Mon Sep 17 00:00:00 2001 From: Antoine Maas Date: Fri, 24 Oct 2025 11:17:50 +0200 Subject: [PATCH 6/6] test: fix path --- erpnext/venue/doctype/item_booking/test_item_booking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/venue/doctype/item_booking/test_item_booking.py b/erpnext/venue/doctype/item_booking/test_item_booking.py index fc5a9225f7..dfe0ae5bc3 100644 --- a/erpnext/venue/doctype/item_booking/test_item_booking.py +++ b/erpnext/venue/doctype/item_booking/test_item_booking.py @@ -547,7 +547,7 @@ class TestItemBooking(BaseTestWithBookableItem): def _setup_credit_test(self, credit_type_label="Test Credit Type", credits_per_hour=10, initial_credits=20, customer=TEST_CUSTOMER): - from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance + from erpnext.venue.doctype.booking_credit.booking_credit import get_balance credit_type = frappe.get_doc({ "doctype": "Booking Credit Type", @@ -580,8 +580,8 @@ class TestItemBooking(BaseTestWithBookableItem): return credit_type def _create_booking_with_credits(self, credit_type, duration_hours=1, customer=TEST_CUSTOMER): - from erpnext.erpnext.doctype.booking_credit_usage.booking_credit_usage import add_booking_credit_usage - from erpnext.erpnext.doctype.booking_credit.booking_credit import get_balance + from erpnext.venue.doctype.booking_credit_usage.booking_credit_usage import add_booking_credit_usage + from erpnext.venue.doctype.booking_credit.booking_credit import get_balance dt_start = add_to_date(getdate(), days=4, hours=7) dt_end = add_to_date(dt_start, hours=duration_hours) -- GitLab