diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 4c1a64d1a4f..a26f2a2f26d 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -25,6 +25,7 @@ import erpnext from erpnext import get_company_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen +from erpnext.stock.doctype.price_list.price_list import is_price_list_enabled from erpnext.utilities.regional import temporary_flag try: @@ -370,12 +371,17 @@ def set_other_values(party_details, party, party_type): def get_default_price_list(party): - """Return default price list for party (Document object)""" - if party.get("default_price_list"): - return party.default_price_list + """Return the first enabled default price list for party (Document object)""" + price_list = party.get("default_price_list") + if is_price_list_enabled(price_list): + return price_list - if party.doctype == "Customer": - return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") + if party.doctype != "Customer": + return + + price_list = frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") + if is_price_list_enabled(price_list): + return price_list def set_price_list(party_details, party, party_type, given_price_list, pos=None): @@ -388,7 +394,7 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None) elif pos and party_type == "Customer": customer_price_list = frappe.get_value("Customer", party.name, "default_price_list") - if customer_price_list: + if is_price_list_enabled(customer_price_list): price_list = customer_price_list else: pos_price_list = frappe.get_value("POS Profile", pos, "selling_price_list") @@ -396,6 +402,9 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None) else: price_list = get_default_price_list(party) or given_price_list + if price_list and not is_price_list_enabled(price_list): + price_list = None + if price_list: party_details.price_list_currency = frappe.db.get_value( "Price List", price_list, "currency", cache=True diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py index 0f477103da4..5304bf40af2 100644 --- a/erpnext/accounts/test_party.py +++ b/erpnext/accounts/test_party.py @@ -1,6 +1,6 @@ import frappe -from erpnext.accounts.party import get_default_price_list +from erpnext.accounts.party import get_default_price_list, set_price_list from erpnext.tests.utils import ERPNextTestSuite @@ -16,3 +16,46 @@ class PartyTestCase(ERPNextTestSuite): customer.save() price_list = get_default_price_list(customer) assert price_list is None + + def test_disabled_party_default_should_fall_back_to_given_price_list(self): + customer = self.create_customer(default_price_list=self.create_price_list(enabled=0)) + given_price_list = self.create_price_list(enabled=1) + + party_details = frappe._dict() + set_price_list(party_details, customer, "Customer", given_price_list) + + self.assertEqual(party_details.selling_price_list, given_price_list) + + def test_disabled_given_price_list_should_not_be_set(self): + customer = self.create_customer() + + party_details = frappe._dict() + set_price_list(party_details, customer, "Customer", self.create_price_list(enabled=0)) + + self.assertIsNone(party_details.selling_price_list) + + def create_price_list(self, enabled): + price_list = frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": frappe.generate_hash(length=10), + "currency": "INR", + "selling": 1, + "enabled": enabled, + } + ).insert(ignore_permissions=True) + + return price_list.name + + def create_customer(self, **values): + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": frappe.generate_hash(length=10), + **values, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) + customer.customer_group = None + customer.save() + + return customer diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 0e5b0031078..997e87630e1 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -69,7 +69,7 @@ erpnext.buying = { if (this.frm.fields_dict.buying_price_list) { this.frm.set_query("buying_price_list", function () { return { - filters: { buying: 1 }, + filters: { buying: 1, enabled: 1 }, }; }); } diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 72a8d25af3b..6613fe3d79d 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -59,7 +59,7 @@ erpnext.sales_common = { if (this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function () { - return { filters: { selling: 1 } }; + return { filters: { selling: 1, enabled: 1 } }; }); } diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index ca9fbfc3884..aa5dd3f6dbb 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -7,6 +7,7 @@ from frappe.defaults import get_user_default from frappe.utils import cint import erpnext.accounts.utils +from erpnext.stock.doctype.price_list.price_list import is_price_list_enabled def boot_session(bootinfo): @@ -28,6 +29,8 @@ def boot_session(bootinfo): frappe.get_single_value("Accounts Settings", "disable_include_dimensions") ) + remove_disabled_price_list_defaults(bootinfo) + bootinfo.sysdefaults.quotation_valid_till = cint( frappe.db.get_single_value("CRM Settings", "default_valid_till") ) @@ -72,6 +75,18 @@ def boot_session(bootinfo): bootinfo.sysdefaults.repost_allowed_doctypes = frappe.get_hooks("repost_allowed_doctypes") +def remove_disabled_price_list_defaults(bootinfo): + user_defaults = (bootinfo.user or {}).get("defaults") or {} + + for key in ("selling_price_list", "buying_price_list"): + price_list = bootinfo.sysdefaults.get(key) or user_defaults.get(key) + if not isinstance(price_list, str) or is_price_list_enabled(price_list): + continue + + bootinfo.sysdefaults.pop(key, None) + user_defaults.pop(key, None) + + def update_page_info(bootinfo): bootinfo.page_info.update( { diff --git a/erpnext/startup/test_boot.py b/erpnext/startup/test_boot.py new file mode 100644 index 00000000000..5fa44cc86c5 --- /dev/null +++ b/erpnext/startup/test_boot.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBoot(ERPNextTestSuite): + def test_boot_session_drops_disabled_price_list_default(self): + from erpnext.startup.boot import boot_session + + price_list = frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": frappe.generate_hash(length=10), + "currency": "INR", + "selling": 1, + "enabled": 0, + } + ).insert(ignore_permissions=True) + + bootinfo = frappe._dict( + sysdefaults=frappe._dict(selling_price_list=price_list.name), + page_info=frappe._dict(), + docs=[], + ) + boot_session(bootinfo) + + self.assertIsNone(bootinfo.sysdefaults.get("selling_price_list")) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 466646775a9..2edcabba160 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -32,6 +32,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.doctype.price_list.price_list import is_price_list_enabled from erpnext.stock.get_item_details import get_default_supplier, get_price_list_rate_for from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import ( @@ -219,14 +220,20 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.validate_pp_qty() + self.set_buying_price_list() - if self.buying_price_list and not frappe.get_value("Price List", self.buying_price_list, "buying"): + def set_buying_price_list(self): + if not is_valid_buying_price_list(self.buying_price_list): self.buying_price_list = None - if not self.buying_price_list: - buying_price_list = frappe.defaults.get_defaults().buying_price_list - if frappe.has_permission("Price List", "read", buying_price_list): - self.buying_price_list = buying_price_list + if self.buying_price_list: + return + + default_price_list = frappe.defaults.get_defaults().buying_price_list + if is_valid_buying_price_list(default_price_list) and frappe.has_permission( + "Price List", "read", default_price_list + ): + self.buying_price_list = default_price_list def on_update(self): if not self.is_new() and self.buying_price_list and self.has_value_changed("buying_price_list"): @@ -488,6 +495,10 @@ class MaterialRequest(BuyingController): doc.db_set("status", doc.status) +def is_valid_buying_price_list(price_list: str | None) -> bool: + return is_price_list_enabled(price_list) and bool(frappe.get_value("Price List", price_list, "buying")) + + def update_completed_and_requested_qty(stock_entry, method): if stock_entry.doctype == "Stock Entry": material_request_map = {} diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 9ee05a3f6c7..6722bf91234 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -86,3 +86,7 @@ def get_price_list_details(price_list): frappe.cache().hset("price_list_details", price_list, price_list_details) return price_list_details or {} + + +def is_price_list_enabled(price_list: str | None) -> bool: + return bool(price_list) and bool(frappe.get_cached_value("Price List", price_list, "enabled"))