From 8f86c1b3e9aaf313066f6010d5980b4fe0087809 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 4 Dec 2025 22:05:21 +0530 Subject: [PATCH 1/4] fix: validate available stock with multiple dimensions --- .../stock_ledger_entry/stock_ledger_entry.py | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 993dad8ca05..87e070b0b05 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -113,17 +113,15 @@ class StockLedgerEntry(Document): return flt_precision = cint(frappe.db.get_default("float_precision")) or 2 - for dimension, values in dimensions.items(): - dimension_value = values.get("value") - available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value) + available_qty = self.get_available_qty_after_prev_transaction(dimensions) - diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction - if diff < 0 and abs(diff) > 0.0001: - self.throw_validation_error(diff, dimension, dimension_value) + diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) - def get_available_qty_after_prev_transaction(self, dimension, dimension_value): + def get_available_qty_after_prev_transaction(self, dimensions): sle = frappe.qb.DocType("Stock Ledger Entry") - available_qty = ( + available_qty_query = ( frappe.qb.from_(sle) .select(Sum(sle.actual_qty)) .where( @@ -132,21 +130,27 @@ class StockLedgerEntry(Document): & (sle.posting_datetime < self.posting_datetime) & (sle.company == self.company) & (sle.is_cancelled == 0) - & (sle[dimension] == dimension_value) ) - ).run() + ) + + for dimension, values in dimensions.items(): + dimension_value = values.get("value") + available_qty_query = available_qty_query.where(sle[dimension] == dimension_value) + + available_qty = available_qty_query.run() return available_qty[0][0] or 0 - def throw_validation_error(self, diff, dimension, dimension_value): + def throw_validation_error(self, diff, dimensions): msg = _( - "{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction." + "{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction." ).format( abs(diff), frappe.get_desk_link("Item", self.item_code), frappe.get_desk_link("Warehouse", self.warehouse), - frappe.bold(dimension), - frappe.bold(dimension_value), + frappe.bold( + ", ".join([f"{dimension}: {values.get('value')}" for dimension, values in dimensions.items()]) + ), self.posting_date, self.posting_time, frappe.get_desk_link(self.voucher_type, self.voucher_no), From 1e2c56874f82b88db6220545315df0c95650988a Mon Sep 17 00:00:00 2001 From: venkat102 Date: Thu, 4 Dec 2025 23:58:25 +0530 Subject: [PATCH 2/4] test: validate negative stock with multiple inventory dimensions --- .../test_inventory_dimension.py | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 6adb5932f24..488bdbaa375 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -3,7 +3,7 @@ import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.tests import IntegrationTestCase +from frappe.tests import IntegrationTestCase, change_settings from frappe.utils import nowdate, nowtime from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -497,6 +497,57 @@ class TestInventoryDimension(IntegrationTestCase): self.assertEqual(site_name, "Site 1") + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_validate_negative_stock_with_multiple_dimension(self): + item_code = "Test Negative Multi Inventory Dimension Item" + create_item(item_code) + + create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + + create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Rack", + reference_document="Rack", + document_type="Rack", + validate_negative_stock=1, + ) + + pr_doc = make_purchase_receipt(qty=30, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 1" + pr_doc.items[0].rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(qty=15, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 1" + pr_doc.items[0].rack = "Rack 2" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(qty=30, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 2" + pr_doc.items[0].rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc = make_purchase_receipt(qty=25, do_not_submit=True) + pr_doc.items[0].inv_site = "Site 2" + pr_doc.items[0].rack = "Rack 2" + pr_doc.save() + pr_doc.submit() + + dn_doc = create_delivery_note(qty=35, do_not_submit=True) + dn_doc.items[0].inv_site = "Site 2" + dn_doc.items[0].rack = "Rack 1" + dn_doc.save() + self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit) + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -586,7 +637,7 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) - for rack in ["Rack 1"]: + for rack in ["Rack 1", "Rack 2"]: if not frappe.db.exists("Rack", rack): frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) From 66f56eea3300648720a42352d8cf6780cb8870eb Mon Sep 17 00:00:00 2001 From: venkat102 Date: Sat, 6 Dec 2025 00:24:20 +0530 Subject: [PATCH 3/4] fix: enable validate_negative_stock in existing dimensions --- .../doctype/inventory_dimension/test_inventory_dimension.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 488bdbaa375..6bdcc99a3b8 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -502,21 +502,23 @@ class TestInventoryDimension(IntegrationTestCase): item_code = "Test Negative Multi Inventory Dimension Item" create_item(item_code) - create_inventory_dimension( + inv_dimension_1 = create_inventory_dimension( apply_to_all_doctypes=1, dimension_name="Inv Site", reference_document="Inv Site", document_type="Inv Site", validate_negative_stock=1, ) + inv_dimension_1.db_set("validate_negative_stock", 1) - create_inventory_dimension( + inv_dimension_2 = create_inventory_dimension( apply_to_all_doctypes=1, dimension_name="Rack", reference_document="Rack", document_type="Rack", validate_negative_stock=1, ) + inv_dimension_2.db_set("validate_negative_stock", 1) pr_doc = make_purchase_receipt(qty=30, do_not_submit=True) pr_doc.items[0].inv_site = "Site 1" From 6e44951a965589d920165fa459e4c201487add54 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Sat, 6 Dec 2025 14:00:51 +0530 Subject: [PATCH 4/4] fix: use separate item --- .../inventory_dimension/test_inventory_dimension.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 6bdcc99a3b8..fd9f12ddd46 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -520,31 +520,31 @@ class TestInventoryDimension(IntegrationTestCase): ) inv_dimension_2.db_set("validate_negative_stock", 1) - pr_doc = make_purchase_receipt(qty=30, do_not_submit=True) + pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True) pr_doc.items[0].inv_site = "Site 1" pr_doc.items[0].rack = "Rack 1" pr_doc.save() pr_doc.submit() - pr_doc = make_purchase_receipt(qty=15, do_not_submit=True) + pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True) pr_doc.items[0].inv_site = "Site 1" pr_doc.items[0].rack = "Rack 2" pr_doc.save() pr_doc.submit() - pr_doc = make_purchase_receipt(qty=30, do_not_submit=True) + pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True) pr_doc.items[0].inv_site = "Site 2" pr_doc.items[0].rack = "Rack 1" pr_doc.save() pr_doc.submit() - pr_doc = make_purchase_receipt(qty=25, do_not_submit=True) + pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True) pr_doc.items[0].inv_site = "Site 2" pr_doc.items[0].rack = "Rack 2" pr_doc.save() pr_doc.submit() - dn_doc = create_delivery_note(qty=35, do_not_submit=True) + dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True) dn_doc.items[0].inv_site = "Site 2" dn_doc.items[0].rack = "Rack 1" dn_doc.save()