From 455251abe18bf0a520eb20e452e82e81d7ddd846 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jul 2026 19:41:29 +0530 Subject: [PATCH] fix: throw when the transaction company has no sample retention warehouse Item.retain_sample is validated against any company having one configured, since Item is not company-scoped. The transaction company may still not be, in which case get_batch_qty received warehouse=None and returned its batch list, which was then compared numerically -> TypeError. Resolve company -> retention warehouse through one helper that throws a clear message instead, covering both the sample stock entry and the whitelisted quantity validation. --- .../stock_entry/services/manufacturing.py | 16 +++++++++++-- .../doctype/stock_entry/test_stock_entry.py | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index 9fc10358802..4d450d4764d 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_cached_value("Company", company, "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" @@ -1281,10 +1281,22 @@ def validate_sample_quantity( _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty) ) - retention_warehouse = frappe.get_cached_value("Company", company, "sample_retention_warehouse") + retention_warehouse = get_sample_retention_warehouse(company) return _adjust_sample_quantity(item_code, sample_quantity, batch_no, get_batch_qty, retention_warehouse) +def get_sample_retention_warehouse(company: str) -> str: + 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") diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 9c3cd3eac45..c4e9cb3dfae 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -3236,6 +3236,30 @@ class TestStockEntryCoverage(ERPNextTestSuite): ) 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", + ) + # ── get_expired_batches ──────────────────────────────────────────────────── def test_get_expired_batches_includes_expired_batch(self):