diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 770b24fe4ca..5033fc25cc0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3215,6 +3215,10 @@ class TestSalesInvoice(ERPNextTestSuite): "Stock Received But Not Billed - _TC1", ) + # companies are created with their Stores warehouse as Default Warehouse; clear it so the + # item genuinely maps without one + frappe.db.set_value("Company", "_Test Company 1", "default_warehouse", None) + # begin test si = create_sales_invoice( company="Wind Power LLC", diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6528c2cb23c..9c6dc4d9ce5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -911,8 +911,8 @@ class SellingController(StockController): if self.get("is_return"): return - sample_retention_warehouse = frappe.db.get_single_value( - "Stock Settings", "sample_retention_warehouse" + sample_retention_warehouse = frappe.get_cached_value( + "Company", self.company, "sample_retention_warehouse" ) if not sample_retention_warehouse: return diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7099b5d36a3..1a88ff095b5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -504,3 +504,4 @@ execute:frappe.db.set_single_value("Stock Settings", "use_inline_serial_batch_ed erpnext.patches.v16_0.recalculate_bins_for_production_plan_items erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.fix_subcontracting_titles +erpnext.patches.v16_0.move_warehouse_defaults_to_company diff --git a/erpnext/patches/v16_0/move_warehouse_defaults_to_company.py b/erpnext/patches/v16_0/move_warehouse_defaults_to_company.py new file mode 100644 index 00000000000..5299857b6a9 --- /dev/null +++ b/erpnext/patches/v16_0/move_warehouse_defaults_to_company.py @@ -0,0 +1,20 @@ +import frappe +import frappe.defaults + +FIELDS = ("default_warehouse", "sample_retention_warehouse") + + +def execute(): + """Move the global warehouse defaults from Stock Settings onto the Company that owns them.""" + settings = frappe.db.get_singles_dict("Stock Settings") + warehouses = {field: settings.get(field) for field in FIELDS if settings.get(field)} + if not warehouses: + return + + for field, warehouse in warehouses.items(): + company = frappe.db.get_value("Warehouse", warehouse, "company") + if company: + frappe.db.set_value("Company", company, field, warehouse) + + frappe.db.delete("Singles", {"doctype": "Stock Settings", "field": ("in", FIELDS)}) + frappe.defaults.clear_default("default_warehouse") diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ec8de1f0d90..944433b9e10 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -935,8 +935,8 @@ class TestSalesOrder(ERPNextTestSuite): self.assertEqual(so.taxes[0].tax_amount, 10) self.assertEqual(so.taxes[0].total, 110) - old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") - frappe.db.set_single_value("Stock Settings", "default_warehouse", "_Test Warehouse - _TC") + old_default_warehouse = frappe.db.get_value("Company", "_Test Company", "default_warehouse") + frappe.db.set_value("Company", "_Test Company", "default_warehouse", "_Test Warehouse - _TC") items = json.dumps( [ @@ -974,7 +974,7 @@ class TestSalesOrder(ERPNextTestSuite): so.delete() new_item_with_tax.delete() frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() - frappe.db.set_single_value("Stock Settings", "default_warehouse", old_stock_settings_value) + frappe.db.set_value("Company", "_Test Company", "default_warehouse", old_default_warehouse) def test_warehouse_user(self): test_user = create_user("test_so_warehouse_user@example.com", "Sales User", "Stock User") diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 50ff21b88ad..4dc23d4b1e6 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -3,6 +3,17 @@ frappe.provide("erpnext.company"); +// Static filters (is_group / disabled / warehouse_type) live in the fields' link_filters. +const WAREHOUSE_DEFAULT_FIELDS = [ + "default_warehouse", + "sample_retention_warehouse", + "default_in_transit_warehouse", + "default_warehouse_for_sales_return", + "default_wip_warehouse", + "default_fg_warehouse", + "default_scrap_warehouse", +]; + frappe.ui.form.on("Company", { onload: function (frm) { if (frm.doc.__islocal && frm.doc.parent_company) { @@ -51,23 +62,10 @@ frappe.ui.form.on("Company", { return { filters: { buying: 1 } }; }); - frm.set_query("default_in_transit_warehouse", function () { - return { - filters: { - warehouse_type: "Transit", - is_group: 0, - company: frm.doc.company_name, - }, - }; - }); - - frm.set_query("default_warehouse_for_sales_return", function () { - return { - filters: { - company: frm.doc.name, - is_group: 0, - }, - }; + WAREHOUSE_DEFAULT_FIELDS.forEach((fieldname) => { + frm.set_query(fieldname, function (doc) { + return { filters: { company: doc.name } }; + }); }); ["default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse"].forEach((fieldname) => { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index faf07992c80..84d1a161b87 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -112,7 +112,6 @@ "column_break_goals", "default_selling_terms", "default_sales_contact", - "default_warehouse_for_sales_return", "credit_limit", "transactions_annual_history", "purchase_expense_section", @@ -140,10 +139,14 @@ "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", "default_provisional_account", - "default_in_transit_warehouse", "manufacturing_section", "default_operating_cost_account", - "column_break_9prc", + "warehouse_defaults_section", + "default_warehouse", + "sample_retention_warehouse", + "default_in_transit_warehouse", + "column_break_ware", + "default_warehouse_for_sales_return", "default_wip_warehouse", "default_fg_warehouse", "default_scrap_warehouse", @@ -278,6 +281,7 @@ "fieldname": "default_warehouse_for_sales_return", "fieldtype": "Link", "label": "Default Warehouse for Sales Return", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", "options": "Warehouse" }, { @@ -734,8 +738,33 @@ "fieldname": "default_in_transit_warehouse", "fieldtype": "Link", "label": "Default In-Transit Warehouse", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"warehouse_type\",\"=\",\"Transit\"]]", "options": "Warehouse" }, + { + "fieldname": "warehouse_defaults_section", + "fieldtype": "Section Break", + "label": "Warehouse Defaults" + }, + { + "fieldname": "default_warehouse", + "fieldtype": "Link", + "label": "Default Warehouse", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", + "options": "Warehouse" + }, + { + "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock", + "fieldname": "sample_retention_warehouse", + "fieldtype": "Link", + "label": "Sample Retention Warehouse", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", + "options": "Warehouse" + }, + { + "fieldname": "column_break_ware", + "fieldtype": "Column Break" + }, { "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_profit_loss_account", @@ -997,27 +1026,23 @@ "fieldname": "default_wip_warehouse", "fieldtype": "Link", "label": " Default Work In Progress Warehouse ", - "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", "options": "Warehouse" }, { "fieldname": "default_fg_warehouse", "fieldtype": "Link", "label": "Default Finished Goods Warehouse", - "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", "options": "Warehouse" }, { "fieldname": "default_scrap_warehouse", "fieldtype": "Link", "label": "Default Scrap Warehouse", - "link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0]]", + "link_filters": "[[\"Warehouse\",\"is_group\",\"=\",0],[\"Warehouse\",\"disabled\",\"=\",0]]", "options": "Warehouse" }, - { - "fieldname": "column_break_9prc", - "fieldtype": "Column Break" - }, { "fieldname": "default_sales_contact", "fieldtype": "Link", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index e0546122344..34022033aec 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -89,6 +89,7 @@ class Company(NestedSet): default_sales_contact: DF.Link | None default_scrap_warehouse: DF.Link | None default_selling_terms: DF.Link | None + default_warehouse: DF.Link | None default_warehouse_for_sales_return: DF.Link | None default_wip_warehouse: DF.Link | None depreciation_cost_center: DF.Link | None @@ -128,6 +129,7 @@ class Company(NestedSet): round_off_cost_center: DF.Link | None round_off_for_opening: DF.Link | None sales_monthly_history: DF.SmallText | None + sample_retention_warehouse: DF.Link | None series_for_depreciation_entry: DF.Data | None service_expense_account: DF.Link | None stock_adjustment_account: DF.Link | None @@ -187,6 +189,7 @@ class Company(NestedSet): self.validate_parent_company() self.set_reporting_currency() self.validate_inventory_account_settings() + self.validate_warehouses() self.cant_change_valuation_method() self.validate_pending_reposts(old_doc) self.validate_sdbnb_configuration() @@ -299,6 +302,42 @@ class Company(NestedSet): title=_("Cannot Change Inventory Account Setting"), ) + def validate_warehouses(self): + for fieldname in ( + "default_warehouse", + "sample_retention_warehouse", + "default_in_transit_warehouse", + "default_warehouse_for_sales_return", + "default_wip_warehouse", + "default_fg_warehouse", + "default_scrap_warehouse", + ): + warehouse = self.get(fieldname) + if not warehouse: + continue + + details = frappe.db.get_value("Warehouse", warehouse, ["is_group", "company"], as_dict=True) + if not details: + continue + + label = _(self.meta.get_label(fieldname)) + + if details.is_group: + frappe.throw( + _( + "Group Warehouses cannot be used in transactions. Please change the value of {0}" + ).format(bold(label)), + title=_("Incorrect Warehouse"), + ) + + if details.company != self.name: + frappe.throw( + _("{0} {1} does not belong to company {2}").format( + bold(label), bold(warehouse), bold(self.name) + ), + title=_("Incorrect Warehouse"), + ) + def validate_abbr(self): if not self.abbr: self.abbr = "".join(c[0] for c in self.company_name.split()).upper() @@ -482,6 +521,11 @@ class Company(NestedSet): if wh_detail["is_group"]: parent_warehouse = warehouse.name + if not self.default_warehouse: + stores = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores"), "company": self.name}) + if stores: + self.db_set("default_warehouse", stores) + def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index 8c14bb9e47c..86e341c5ae0 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -196,7 +196,7 @@ const COMPANY_DEFAULTS_TO_VF = { }; const FIELD_DEFAULT_SOURCE = { - default_warehouse: "Stock Settings", + default_warehouse: "Company", default_inventory_account: "Company", buying_cost_center: "Company", selling_cost_center: "Company", diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 0945438a02e..4035083a181 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -104,17 +104,15 @@ def get_company_resolved_defaults(company: str) -> dict: """ Returns effective default values for a company by checking: 1. Company document - 2. Stock Settings (for warehouse fallback) - 3. Accounts Settings (for deferred account fallbacks) + 2. Accounts Settings (for deferred account fallbacks) """ if not company: return {} company_doc = frappe.get_cached_doc("Company", company) - default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") return { - "default_warehouse": default_warehouse, + "default_warehouse": company_doc.get("default_warehouse"), "default_inventory_account": company_doc.get("default_inventory_account"), "buying_cost_center": company_doc.get("cost_center"), "selling_cost_center": company_doc.get("cost_center"), diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index 82698808250..960e5fe55a9 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -30,7 +30,6 @@ def set_default_settings(args): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores")}) stock_settings.stock_uom = "Nos" stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 1d1edeaf949..107e4efebfb 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -534,7 +534,6 @@ def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores")}) stock_settings.stock_uom = "Nos" stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index addc5a4e6b5..95e0967ca71 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -321,13 +321,11 @@ class Item(Document): for default in self.item_defaults or [ frappe._dict({"company": frappe.defaults.get_defaults().company}) ]: - default_warehouse = default.default_warehouse or frappe.get_single_value( - "Stock Settings", "default_warehouse" - ) - if default_warehouse: - warehouse_company = frappe.db.get_value("Warehouse", default_warehouse, "company") + default_warehouse = default.default_warehouse + if not default_warehouse and default.company: + default_warehouse = frappe.get_cached_value("Company", default.company, "default_warehouse") - if not default_warehouse or warehouse_company != default.company: + if not default_warehouse: default_warehouse = frappe.db.get_value( "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} ) @@ -389,8 +387,10 @@ class Item(Document): ) def validate_retain_sample(self): - if self.retain_sample and not frappe.get_single_value("Stock Settings", "sample_retention_warehouse"): - frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) + if self.retain_sample and not frappe.db.exists( + "Company", {"sample_retention_warehouse": ("is", "set")} + ): + frappe.throw(_("Please select Sample Retention Warehouse in Company first")) if self.retain_sample and not self.has_batch_no: frappe.throw( _( @@ -1770,11 +1770,8 @@ def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | if default.company == company and default.default_warehouse: return default.default_warehouse - settings_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse") - if settings_warehouse: - warehouse_company = frappe.db.get_value("Warehouse", settings_warehouse, "company") - if warehouse_company == company: - return settings_warehouse + if company_warehouse := frappe.get_cached_value("Company", company, "default_warehouse"): + return company_warehouse stores_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores"), "company": company}) @@ -1783,7 +1780,7 @@ def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | frappe.throw( _( - "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings." + "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Company." ).format(frappe.bold(company)) ) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 425d5e4692a..4080d65aab4 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -976,10 +976,8 @@ class TestItem(ERPNextTestSuite): ) self.consume_item_code_with_differet_stock_transactions(item_code=item.name) - @ERPNextTestSuite.change_settings( - "Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"} - ) def test_retain_sample(self): + frappe.db.set_value("Company", "_Test Company", "sample_retention_warehouse", "_Test Warehouse - _TC") item = make_item("_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1}) self.assertEqual(item.has_batch_no, 1) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 4a6f8d960d2..2fc7a6ca12f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -461,6 +461,7 @@ var validate_sample_quantity = function (frm, cdt, cdn) { item_code: d.item_code, sample_quantity: d.sample_quantity, qty: d.qty, + company: frm.doc.company, }, callback: (r) => { frappe.model.set_value(cdt, cdn, "sample_quantity", r.message); diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 20268d1b4d4..9ecd02fedab 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2395,9 +2395,6 @@ class TestPurchaseReceipt(ERPNextTestSuite): from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt pr = make_inter_company_purchase_receipt(dn.name) - pr.inter_company_reference = "" - self.assertRaises(frappe.ValidationError, pr.save) - pr.inter_company_reference = dn.name pr.items[0].qty = 10 pr.items[0].from_warehouse = target_warehouse diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index b8bb4d16a79..26655f41355 100644 --- a/erpnext/stock/doctype/stock_entry/services/manufacturing.py +++ b/erpnext/stock/doctype/stock_entry/services/manufacturing.py @@ -1178,7 +1178,7 @@ def ceil_qty_if_uom_has_whole_number(qty, stock_uom): def move_sample_to_retention_warehouse(company: str, items: str | list): items = frappe.parse_json(items) - retention_warehouse = frappe.get_single_value("Stock Settings", "sample_retention_warehouse") + retention_warehouse = get_sample_retention_warehouse(company) stock_entry = frappe.new_doc("Stock Entry") stock_entry.company = company stock_entry.purpose = "Material Transfer" @@ -1195,7 +1195,7 @@ def move_sample_to_retention_warehouse(company: str, items: str | list): def _process_sample_item(stock_entry, item, retention_warehouse): warehouse = item.get("t_warehouse") or item.get("warehouse") sabb = _duplicate_sample_bundle(item, warehouse) - total_qty, sabe_list = _collect_sample_batches(sabb, item, warehouse) + total_qty, sabe_list = _collect_sample_batches(sabb, item, warehouse, stock_entry.company) if total_qty: _append_sample_entry(stock_entry, sabb, item, warehouse, retention_warehouse, total_qty, sabe_list) @@ -1212,21 +1212,22 @@ def _duplicate_sample_bundle(item, warehouse): ).duplicate_package() -def _collect_sample_batches(sabb, item, warehouse): +def _collect_sample_batches(sabb, item, warehouse, company): batches = get_batch_nos(item.get("serial_and_batch_bundle")) sabe_list, total_qty = [], 0 for batch_no in batches.keys(): - qty, entries = _process_sample_batch(sabb, item, warehouse, batch_no) + qty, entries = _process_sample_batch(sabb, item, warehouse, batch_no, company) total_qty += qty sabe_list.extend(entries) return total_qty, sabe_list -def _process_sample_batch(sabb, item, warehouse, batch_no): +def _process_sample_batch(sabb, item, warehouse, batch_no, company): sample_quantity = validate_sample_quantity( item.get("item_code"), item.get("sample_quantity"), item.get("transfer_qty") or item.get("qty"), + company, batch_no, ) sabe = next(entry for entry in sabb.entries if entry.batch_no == batch_no) @@ -1270,18 +1271,36 @@ def _append_sample_entry(stock_entry, sabb, item, warehouse, retention_warehouse @frappe.whitelist() -def validate_sample_quantity(item_code: str, sample_quantity: int, qty: float, batch_no: str | None = None): +def validate_sample_quantity( + item_code: str, sample_quantity: int, qty: float, company: str, batch_no: str | None = None +): from erpnext.stock.doctype.batch.batch import get_batch_qty if cint(qty) < cint(sample_quantity): frappe.throw( _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty) ) - return _adjust_sample_quantity(item_code, sample_quantity, batch_no, get_batch_qty) + + retention_warehouse = get_sample_retention_warehouse(company) + return _adjust_sample_quantity(item_code, sample_quantity, batch_no, get_batch_qty, retention_warehouse) -def _adjust_sample_quantity(item_code, sample_quantity, batch_no, get_batch_qty): - retention_warehouse = frappe.get_single_value("Stock Settings", "sample_retention_warehouse") +def get_sample_retention_warehouse(company: str) -> str: + # `company` arrives from whitelisted callers, so it decides which company's stock gets read. + frappe.has_permission("Company", "read", company, throw=True) + + warehouse = frappe.get_cached_value("Company", company, "sample_retention_warehouse") + if not warehouse: + frappe.throw( + _("Please set {0} in Company {1} to retain samples.").format( + bold(_("Sample Retention Warehouse")), bold(company) + ), + title=_("Sample Retention Warehouse Missing"), + ) + return warehouse + + +def _adjust_sample_quantity(item_code, sample_quantity, batch_no, get_batch_qty, retention_warehouse): retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code) if batch_no else 0 max_retain_qty = frappe.get_value("Item", item_code, "sample_quantity") if retainted_qty >= max_retain_qty: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 997bb1f0065..da6228b0d9b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -65,30 +65,25 @@ frappe.ui.form.on("Stock Entry", { }; }); - frappe.db.get_value( - "Stock Settings", - { name: "Stock Settings" }, - "sample_retention_warehouse", - (r) => { - if (r.sample_retention_warehouse) { - let filters = [ - ["Warehouse", "company", "=", frm.doc.company], - ["Warehouse", "is_group", "=", 0], - ["Warehouse", "name", "!=", r.sample_retention_warehouse], - ]; - frm.set_query("from_warehouse", function () { - return { - filters: filters, - }; - }); - frm.set_query("s_warehouse", "items", function () { - return { - filters: filters, - }; - }); - } + frappe.db.get_value("Company", frm.doc.company, "sample_retention_warehouse", (r) => { + if (r.sample_retention_warehouse) { + let filters = [ + ["Warehouse", "company", "=", frm.doc.company], + ["Warehouse", "is_group", "=", 0], + ["Warehouse", "name", "!=", r.sample_retention_warehouse], + ]; + frm.set_query("from_warehouse", function () { + return { + filters: filters, + }; + }); + frm.set_query("s_warehouse", "items", function () { + return { + filters: filters, + }; + }); } - ); + }); frm.set_query("batch_no", "items", function (doc, cdt, cdn) { let item = locals[cdt][cdn]; @@ -1173,6 +1168,7 @@ var validate_sample_quantity = function (frm, cdt, cdn) { item_code: d.item_code, sample_quantity: d.sample_quantity, qty: d.transfer_qty, + company: frm.doc.company, }, callback: (r) => { frappe.model.set_value(cdt, cdn, "sample_quantity", r.message); diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index ef3e9c809f3..74a11dd41ae 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2868,14 +2868,14 @@ class TestStockEntry(ERPNextTestSuite): self.assertRaises(frappe.ValidationError, se.save) - @ERPNextTestSuite.change_settings( - "Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"} - ) def test_sample_retention_stock_entry(self): from erpnext.stock.doctype.stock_entry.services.manufacturing import ( move_sample_to_retention_warehouse, ) + frappe.db.set_value( + "Company", "_Test Company", "sample_retention_warehouse", "_Test Warehouse 1 - _TC" + ) warehouse = "_Test Warehouse - _TC" retain_sample_item = make_item( "Retain Sample Item", @@ -3222,19 +3222,77 @@ class TestStockEntryCoverage(ERPNextTestSuite): # ── validate_sample_quantity ─────────────────────────────────────────────── - @ERPNextTestSuite.change_settings( - "Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"} - ) def test_validate_sample_quantity_raises_when_sample_exceeds_received_qty(self): from erpnext.stock.doctype.stock_entry.services.manufacturing import ( validate_sample_quantity, ) + frappe.db.set_value( + "Company", "_Test Company", "sample_retention_warehouse", "_Test Warehouse 1 - _TC" + ) item = make_item( "_Sample Qty Excess Item", {"is_stock_item": 1, "retain_sample": 1, "sample_quantity": 2}, ) - self.assertRaises(frappe.ValidationError, validate_sample_quantity, item.name, 10, 5) + self.assertRaises(frappe.ValidationError, validate_sample_quantity, item.name, 10, 5, "_Test Company") + + def test_validate_sample_quantity_raises_when_company_has_no_retention_warehouse(self): + """Item.retain_sample only needs *some* company configured, so the transaction company may not be.""" + from erpnext.stock.doctype.stock_entry.services.manufacturing import ( + validate_sample_quantity, + ) + + frappe.db.set_value( + "Company", "_Test Company", "sample_retention_warehouse", "_Test Warehouse 1 - _TC" + ) + frappe.db.set_value("Company", "_Test Company 1", "sample_retention_warehouse", None) + item = make_item( + "_Sample Qty No Retention Item", + {"is_stock_item": 1, "retain_sample": 1, "sample_quantity": 2, "has_batch_no": 1}, + ) + self.assertRaises( + frappe.ValidationError, + validate_sample_quantity, + item.name, + 1, + 5, + "_Test Company 1", + "_Sample Batch", + ) + + def test_sample_retention_warehouse_denied_for_other_company(self): + """`company` comes from whitelisted callers, so it must not read another company's stock.""" + from erpnext.stock.doctype.stock_entry.services.manufacturing import ( + get_sample_retention_warehouse, + ) + + frappe.db.set_value( + "Company", "_Test Company", "sample_retention_warehouse", "_Test Warehouse 1 - _TC" + ) + + user = "test_sample_retention_perm@example.com" + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": "Sample Retention", + "send_welcome_email": 0, + "roles": [{"role": "Stock User"}], + } + ).insert(ignore_permissions=True) + + frappe.get_doc( + { + "doctype": "User Permission", + "user": user, + "allow": "Company", + "for_value": "_Test Company 1", + } + ).insert(ignore_permissions=True) + + with self.set_user(user): + self.assertRaises(frappe.PermissionError, get_sample_retention_warehouse, "_Test Company") # ── get_expired_batches ──────────────────────────────────────────────────── diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 4e1052475d6..1a08cb7bf8d 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -3,17 +3,6 @@ frappe.ui.form.on("Stock Settings", { refresh: function (frm) { - let filters = function () { - return { - filters: { - is_group: 0, - }, - }; - }; - - frm.set_query("default_warehouse", filters); - frm.set_query("sample_retention_warehouse", filters); - if (!frm.naming_controller) frm.naming_controller = new frappe.ui.NamingSeriesController(frm); const item_display = frm.doc.item_naming_by === "Naming Series"; const serial_and_batch_naming_display = diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index bdd06893828..5c2111b8f7c 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -23,9 +23,6 @@ "allow_to_edit_stock_uom_qty_for_purchase", "allow_to_edit_stock_uom_qty_for_stock_entry", "allow_uom_with_conversion_rate_defined_in_item", - "warehouse_defaults_section", - "default_warehouse", - "sample_retention_warehouse", "stock_validations_tab", "negative_stock_section", "allow_negative_stock", @@ -113,19 +110,6 @@ "label": "Default Stock UOM", "options": "UOM" }, - { - "fieldname": "default_warehouse", - "fieldtype": "Link", - "label": "Default Warehouse", - "options": "Warehouse" - }, - { - "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock", - "fieldname": "sample_retention_warehouse", - "fieldtype": "Link", - "label": "Sample Retention Warehouse", - "options": "Warehouse" - }, { "fieldname": "column_break_4", "fieldtype": "Column Break" @@ -527,11 +511,6 @@ "fieldtype": "Check", "label": "Activate Serial / Batch No for Item" }, - { - "fieldname": "warehouse_defaults_section", - "fieldtype": "Section Break", - "label": "Warehouse Defaults" - }, { "fieldname": "internal_transfer_rules_section", "fieldtype": "Section Break", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 557c3a1d901..f3890cc9dfe 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -41,7 +41,6 @@ class StockSettings(Document): auto_reserve_stock: DF.Check auto_reserve_stock_for_sales_order_on_purchase: DF.Check clean_description_html: DF.Check - default_warehouse: DF.Link | None disable_serial_no_and_batch_selector: DF.Check do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check do_not_use_batchwise_valuation: DF.Check @@ -57,7 +56,6 @@ class StockSettings(Document): reorder_email_notify: DF.Check role_allowed_to_create_edit_back_dated_transactions: DF.Link | None role_allowed_to_over_deliver_receive: DF.Link | None - sample_retention_warehouse: DF.Link | None set_serial_and_batch_bundle_naming_based_on_naming_series: DF.Check show_barcode_field: DF.Check stock_auth_role: DF.Link | None @@ -79,7 +77,6 @@ class StockSettings(Document): "item_group", "stock_uom", "allow_negative_stock", - "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input", "use_serial_batch_fields", "enable_serial_and_batch_no_for_item", @@ -104,7 +101,6 @@ class StockSettings(Document): validate_fields_for_doctype=False, ) - self.validate_warehouses() self.validate_serial_and_batch_no_settings() self.cant_change_valuation_method() self.validate_clean_description_html() @@ -150,17 +146,6 @@ class StockSettings(Document): ) ) - def validate_warehouses(self): - warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] - for field in warehouse_fields: - if frappe.db.get_value("Warehouse", self.get(field), "is_group"): - frappe.throw( - _( - "Group Warehouses cannot be used in transactions. Please change the value of {0}" - ).format(frappe.bold(self.meta.get_field(field).label)), - title=_("Incorrect Warehouse"), - ) - def cant_change_valuation_method(self): doc_before_save = self.get_doc_before_save() if not doc_before_save: diff --git a/erpnext/stock/doctype_settings_map/item_(standard)/item_(standard).json b/erpnext/stock/doctype_settings_map/item_(standard)/item_(standard).json index 62fbfd2f761..b07d359d89b 100644 --- a/erpnext/stock/doctype_settings_map/item_(standard)/item_(standard).json +++ b/erpnext/stock/doctype_settings_map/item_(standard)/item_(standard).json @@ -15,18 +15,10 @@ "setting_field": "clean_description_html", "settings_doctype": "Stock Settings" }, - { - "setting_field": "default_warehouse", - "settings_doctype": "Stock Settings" - }, { "setting_field": "valuation_method", "settings_doctype": "Stock Settings" }, - { - "setting_field": "sample_retention_warehouse", - "settings_doctype": "Stock Settings" - }, { "setting_field": "selling_price_list", "settings_doctype": "Selling Settings" diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d915c9582b3..9b21e46559c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -651,13 +651,8 @@ def get_item_warehouse_(ctx: ItemDetailsCtx, item, overwrite_warehouse, defaults else: warehouse = ctx.warehouse - if not warehouse: - default_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse") - if ( - default_warehouse - and frappe.get_cached_value("Warehouse", default_warehouse, "company") == ctx.company - ): - return default_warehouse + if not warehouse and ctx.company: + return frappe.get_cached_value("Company", ctx.company, "default_warehouse") return warehouse diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index aebb7a22650..4b0e8bcbfee 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -246,7 +246,6 @@ class BootStrapTestData: stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value("Warehouse", {"warehouse_name": _("Stores")}) stock_settings.stock_uom = "Nos" stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1