From 87e8305753d5c26371dfa6b0ad73891c3973e22d Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Fri, 7 Nov 2025 16:03:04 +0530 Subject: [PATCH 1/6] feat(pos): prevent disabling POS Profile when open POS sessions exist (cherry picked from commit c5219278fb379de6f635a487de7f0db3bb8b6e55) --- .../accounts/doctype/pos_profile/pos_profile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 143407eb750..2928782a647 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -70,6 +70,7 @@ class POSProfile(Document): # end: auto-generated types def validate(self): + self.validate_disabled() self.validate_default_profile() self.validate_all_link_fields() self.validate_duplicate_groups() @@ -94,6 +95,21 @@ class POSProfile(Document): title=_("Mandatory Accounting Dimension"), ) + def validate_disabled(self): + old_doc = self.get_doc_before_save() + + if ( + old_doc + and self.disabled + and old_doc.disabled != self.disabled + and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"}) + ): + frappe.throw( + _("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format( + frappe.bold(self.name) + ) + ) + def validate_default_profile(self): for row in self.applicable_for_users: res = frappe.db.sql( From 38848ff43b4ce792313dc651077cc1d91c968aa0 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 8 Nov 2025 12:31:09 +0530 Subject: [PATCH 2/6] test: added test to validate disabled pos profile (cherry picked from commit 69016a284f5f4f644e75723a4c47908a003ba81c) # Conflicts: # erpnext/accounts/doctype/pos_profile/test_pos_profile.py --- .../doctype/pos_profile/test_pos_profile.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index f2e3c8fcf59..427748627ed 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -4,6 +4,11 @@ import unittest import frappe +<<<<<<< HEAD +======= +from frappe.tests import IntegrationTestCase +from frappe.utils import cint +>>>>>>> 69016a284f (test: added test to validate disabled pos profile) from erpnext.accounts.doctype.pos_profile.pos_profile import ( get_child_nodes, @@ -38,6 +43,50 @@ class TestPOSProfile(unittest.TestCase): frappe.db.sql("delete from `tabPOS Profile`") + def test_disabled_pos_profile_creation(self): + make_pos_profile(name="_Test POS Profile 001", disabled=1) + + pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001") + + if pos_profile: + self.assertEqual(pos_profile.disabled, 1) + + def test_disabled_pos_profile_after_opening(self): + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry + + test_user, pos_profile = init_user_and_profile() + + if pos_profile: + create_opening_entry(pos_profile, test_user.name) + self.assertEqual(pos_profile.disabled, 0) + + pos_profile.disabled = 1 + self.assertRaises(frappe.ValidationError, pos_profile.save) + + def test_disabled_pos_profile_after_completing_session(self): + from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( + make_closing_entry_from_opening, + ) + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import ( + create_opening_entry, + ) + + test_user, pos_profile = init_user_and_profile() + + if pos_profile: + opening_entry = create_opening_entry(pos_profile, test_user.name) + + closing_entry = make_closing_entry_from_opening(opening_entry) + closing_entry.submit() + + pos_profile.disabled = 1 + pos_profile.save() + pos_profile.reload() + + self.assertEqual(pos_profile.disabled, 1) + def get_customers_list(pos_profile=None): if pos_profile is None: @@ -117,6 +166,7 @@ def make_pos_profile(**args): "write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC", "location": "Block 1" if not args.do_not_set_accounting_dimension else None, + "disabled": cint(args.disabled) or 0, } ) From 68747b5818e10cbe2a92df2b1d6f0d9e236e76e7 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 8 Nov 2025 12:31:40 +0530 Subject: [PATCH 3/6] fix: prevent pos opening entry creation for disabled pos profile (cherry picked from commit e35e8968f0825256e09b7beba8615270b9559e4d) # Conflicts: # erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py --- .../pos_opening_entry/pos_opening_entry.py | 14 ++- .../test_pos_opening_entry.py | 99 +++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 7f1890ceabf..d7965c58831 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater): self.set_status() def validate_pos_profile_and_cashier(self): - if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): + if not frappe.db.exists("POS Profile", self.pos_profile): + frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile)) + + pos_profile_company, pos_profile_disabled = frappe.db.get_value( + "POS Profile", self.pos_profile, ["company", "disabled"] + ) + + if pos_profile_disabled: + frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile))) + + if self.company != pos_profile_company: frappe.throw( - _("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company) + _("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company) ) if not cint(frappe.db.get_value("User", self.user, "enabled")): diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index 64c658ab151..e0276f8bb62 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -6,8 +6,107 @@ import unittest import frappe +<<<<<<< HEAD class TestPOSOpeningEntry(unittest.TestCase): pass +======= +class TestPOSOpeningEntry(IntegrationTestCase): + @classmethod + def setUpClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"})) + + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabPOS Opening Entry`") + + def setUp(self): + # Make stock available for POS Sales + frappe.db.sql("delete from `tabPOS Opening Entry`") + make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + + self.init_user_and_profile = init_user_and_profile + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + + def test_pos_opening_entry(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry.status, "Open") + self.assertNotEqual(opening_entry.docstatus, 0) + + def test_pos_opening_entry_on_disabled_pos(self): + test_user, pos_profile = self.init_user_and_profile(disabled=1) + + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, test_user.name) + + def test_multiple_pos_opening_entries_for_same_pos_profile(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry.status, "Open") + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, test_user.name) + + def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry_1 = create_opening_entry(pos_profile, test_user.name) + + self.assertEqual(opening_entry_1.status, "Open") + self.assertEqual(opening_entry_1.user, test_user.name) + + cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") + frappe.set_user(cashier_user.name) + + pos_profile2 = make_pos_profile(name="_Test POS Profile 2") + opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name) + + self.assertEqual(opening_entry_2.status, "Open") + self.assertEqual(opening_entry_2.user, cashier_user.name) + + def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self): + test_user, pos_profile = self.init_user_and_profile() + cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") + + opening_entry = create_opening_entry(pos_profile, test_user.name) + self.assertEqual(opening_entry.status, "Open") + + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile, cashier_user.name) + + def test_user_assignment_to_multiple_pos_profile(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry_1 = create_opening_entry(pos_profile, test_user.name) + self.assertEqual(opening_entry_1.user, test_user.name) + + pos_profile2 = make_pos_profile(name="_Test POS Profile 2") + with self.assertRaises(frappe.ValidationError): + create_opening_entry(pos_profile2, test_user.name) + + def test_cancel_pos_opening_entry_without_invoices(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) + + opening_entry.cancel() + self.assertEqual(opening_entry.status, "Cancelled") + self.assertNotEqual(opening_entry.docstatus, 1) + + def test_cancel_pos_opening_entry_with_invoice(self): + test_user, pos_profile = self.init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) + + pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) + pos_inv1.save() + pos_inv1.submit() + + self.assertRaises(frappe.ValidationError, opening_entry.cancel) +>>>>>>> e35e8968f0 (fix: prevent pos opening entry creation for disabled pos profile) def create_opening_entry(pos_profile, user): From 650d2f74ba2246b77ff17a6abb059c7efb2c5fd5 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 11:33:56 +0530 Subject: [PATCH 4/6] chore: resolve conflict --- .../test_pos_opening_entry.py | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py index e0276f8bb62..64c658ab151 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py @@ -6,107 +6,8 @@ import unittest import frappe -<<<<<<< HEAD class TestPOSOpeningEntry(unittest.TestCase): pass -======= -class TestPOSOpeningEntry(IntegrationTestCase): - @classmethod - def setUpClass(cls): - frappe.db.sql("delete from `tabPOS Opening Entry`") - cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"})) - - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabPOS Opening Entry`") - - def setUp(self): - # Make stock available for POS Sales - frappe.db.sql("delete from `tabPOS Opening Entry`") - make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) - from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile - - self.init_user_and_profile = init_user_and_profile - - def tearDown(self): - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - - def test_pos_opening_entry(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry.status, "Open") - self.assertNotEqual(opening_entry.docstatus, 0) - - def test_pos_opening_entry_on_disabled_pos(self): - test_user, pos_profile = self.init_user_and_profile(disabled=1) - - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, test_user.name) - - def test_multiple_pos_opening_entries_for_same_pos_profile(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry.status, "Open") - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, test_user.name) - - def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry_1 = create_opening_entry(pos_profile, test_user.name) - - self.assertEqual(opening_entry_1.status, "Open") - self.assertEqual(opening_entry_1.user, test_user.name) - - cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") - frappe.set_user(cashier_user.name) - - pos_profile2 = make_pos_profile(name="_Test POS Profile 2") - opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name) - - self.assertEqual(opening_entry_2.status, "Open") - self.assertEqual(opening_entry_2.user, cashier_user.name) - - def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self): - test_user, pos_profile = self.init_user_and_profile() - cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager") - - opening_entry = create_opening_entry(pos_profile, test_user.name) - self.assertEqual(opening_entry.status, "Open") - - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile, cashier_user.name) - - def test_user_assignment_to_multiple_pos_profile(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry_1 = create_opening_entry(pos_profile, test_user.name) - self.assertEqual(opening_entry_1.user, test_user.name) - - pos_profile2 = make_pos_profile(name="_Test POS Profile 2") - with self.assertRaises(frappe.ValidationError): - create_opening_entry(pos_profile2, test_user.name) - - def test_cancel_pos_opening_entry_without_invoices(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) - - opening_entry.cancel() - self.assertEqual(opening_entry.status, "Cancelled") - self.assertNotEqual(opening_entry.docstatus, 1) - - def test_cancel_pos_opening_entry_with_invoice(self): - test_user, pos_profile = self.init_user_and_profile() - opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True) - - pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1) - pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) - pos_inv1.save() - pos_inv1.submit() - - self.assertRaises(frappe.ValidationError, opening_entry.cancel) ->>>>>>> e35e8968f0 (fix: prevent pos opening entry creation for disabled pos profile) def create_opening_entry(pos_profile, user): From af19b81343bfbeea87bb6215a711577a71399cfe Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 11:35:02 +0530 Subject: [PATCH 5/6] chore: resolve conflict --- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 427748627ed..5efe3bd5380 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -4,11 +4,7 @@ import unittest import frappe -<<<<<<< HEAD -======= -from frappe.tests import IntegrationTestCase from frappe.utils import cint ->>>>>>> 69016a284f (test: added test to validate disabled pos profile) from erpnext.accounts.doctype.pos_profile.pos_profile import ( get_child_nodes, From d5160c4c86749a7e37775953f97c29ef11412b88 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Wed, 12 Nov 2025 12:26:26 +0530 Subject: [PATCH 6/6] test: delete outdated pos opening entry --- erpnext/accounts/doctype/pos_profile/test_pos_profile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 5efe3bd5380..20ee2faadcf 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -70,6 +70,7 @@ class TestPOSProfile(unittest.TestCase): ) test_user, pos_profile = init_user_and_profile() + frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name}) if pos_profile: opening_entry = create_opening_entry(pos_profile, test_user.name)