fix(stock): add company filter while fetching batches (backport #53369) (#53581)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix(stock): add company filter while fetching batches (#53369)
This commit is contained in:
mergify[bot]
2026-03-18 13:30:29 +05:30
committed by GitHub
parent dd0013e844
commit 91ee45a698
6 changed files with 204 additions and 20 deletions

View File

@@ -297,6 +297,7 @@ frappe.ui.form.on("BOM", {
bom_no: frm.doc.name,
item: item,
qty: data.qty || 0.0,
company: frm.doc.company,
project: frm.doc.project,
variant_items: variant_items,
use_multi_level_bom: frm.doc?.track_semi_finished_goods ? 0 : use_multi_level_bom,

View File

@@ -2261,7 +2261,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
@frappe.whitelist()
def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None):
def make_work_order(
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
):
from erpnext import get_default_company
if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
@@ -2277,6 +2281,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
wo_doc = frappe.new_doc("Work Order")
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
wo_doc.production_item = item
wo_doc.company = company or get_default_company()
wo_doc.update(item_details)
wo_doc.bom_no = bom_no
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)

View File

@@ -28,6 +28,15 @@ from erpnext.stock.serial_batch_bundle import (
)
from erpnext.utilities.transaction_base import TransactionBase
class MissingWarehouseValidationError(frappe.ValidationError):
pass
class IncorrectWarehouseValidationError(frappe.ValidationError):
pass
# TODO: Prioritize SO or WO group warehouse
@@ -108,6 +117,7 @@ class PickList(TransactionBase):
if self.get("locations"):
self.validate_sales_order_percentage()
self.validate_warehouses()
def validate_stock_qty(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -152,6 +162,31 @@ class PickList(TransactionBase):
title=_("Insufficient Stock"),
)
def validate_warehouses(self):
for location in self.locations:
if not location.warehouse:
frappe.throw(
_("Row {0}: Warehouse is required").format(location.idx),
title=_("Missing Warehouse"),
exc=MissingWarehouseValidationError,
)
company = frappe.get_cached_value("Warehouse", location.warehouse, "company")
if company != self.company:
frappe.throw(
_(
"Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}."
).format(
location.idx,
frappe.bold(location.warehouse),
frappe.bold(company),
frappe.bold(self.company),
),
title=_("Incorrect Warehouse"),
exc=IncorrectWarehouseValidationError,
)
def check_serial_no_status(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -958,6 +993,7 @@ def get_available_item_locations(
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
else:
@@ -1058,6 +1094,7 @@ def get_available_item_locations_for_serial_and_batched_item(
locations = get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=consider_rejected_warehouses,
)
@@ -1138,6 +1175,7 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item(
item_code,
from_warehouses,
company,
consider_rejected_warehouses=False,
):
locations = []
@@ -1146,6 +1184,7 @@ def get_available_item_locations_for_batched_item(
{
"item_code": item_code,
"warehouse": from_warehouses,
"company": company,
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
}
)

View File

@@ -211,6 +211,7 @@ class TestPickList(IntegrationTestCase):
"qty": 1000,
"stock_qty": 1000,
"conversion_factor": 1,
"warehouse": "_Test Warehouse - _TC",
"sales_order": so.name,
"sales_order_item": so.items[0].name,
}
@@ -268,6 +269,119 @@ class TestPickList(IntegrationTestCase):
pr1.cancel()
pr2.cancel()
def test_pick_list_warehouse_for_batched_item(self):
"""
Test that pick list respects company based warehouse assignment for batched items.
This test verifies that when creating a pick list for a batched item,
the system correctly identifies and assigns the appropriate warehouse
based on the company.
"""
from erpnext.stock.doctype.batch.test_batch import make_new_batch
batch_company = frappe.get_doc(
{"doctype": "Company", "company_name": "Batch Company", "default_currency": "INR"}
)
batch_company.insert()
batch_warehouse = frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": "Batch Warehouse",
"company": batch_company.name,
}
)
batch_warehouse.insert()
batch_item = frappe.db.exists("Item", "Batch Warehouse Item")
if not batch_item:
batch_item = create_item("Batch Warehouse Item")
batch_item.has_batch_no = 1
batch_item.create_new_batch = 1
batch_item.save()
else:
batch_item = frappe.get_doc("Item", "Batch Warehouse Item")
batch_no = make_new_batch(item_code=batch_item.name, batch_id="B-WH-ITEM-001")
make_stock_entry(
item_code=batch_item.name,
qty=5,
company=batch_company.name,
to_warehouse=batch_warehouse.name,
batch_no=batch_no.name,
rate=100.0,
)
make_stock_entry(
item_code=batch_item.name,
qty=5,
to_warehouse="_Test Warehouse - _TC",
batch_no=batch_no.name,
rate=100.0,
)
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
"company": batch_company.name,
"purpose": "Material Transfer",
"locations": [
{
"item_code": batch_item.name,
"qty": 10,
"stock_qty": 10,
"conversion_factor": 1,
}
],
}
)
pick_list.set_item_locations()
self.assertEqual(len(pick_list.locations), 1)
self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].batch_no, batch_no.name)
self.assertEqual(pick_list.locations[0].warehouse, batch_warehouse.name)
def test_pick_list_warehouse_validation(self):
"""check if the warehouse validations are triggered"""
from erpnext.stock.doctype.pick_list.pick_list import (
IncorrectWarehouseValidationError,
MissingWarehouseValidationError,
)
warehouse_item = create_item("Warehouse Item")
temp_company = frappe.get_doc(
{"doctype": "Company", "company_name": "Temp Company", "default_currency": "INR"}
).insert()
temp_warehouse = frappe.get_doc(
{"doctype": "Warehouse", "warehouse_name": "Temp Warehouse", "company": temp_company.name}
).insert()
make_stock_entry(item_code=warehouse_item.name, qty=10, rate=100.0, to_warehouse=temp_warehouse.name)
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
"company": temp_company.name,
"purpose": "Material Transfer",
"pick_manually": 1,
"locations": [
{
"item_code": warehouse_item.name,
"qty": 5,
"stock_qty": 5,
"conversion_factor": 1,
}
],
}
)
self.assertRaises(MissingWarehouseValidationError, pick_list.insert)
pick_list.locations[0].warehouse = "_Test Warehouse - _TC"
self.assertRaises(IncorrectWarehouseValidationError, pick_list.insert)
pick_list.locations[0].warehouse = temp_warehouse.name
pick_list.insert()
def test_pick_list_for_batched_and_serialised_item(self):
# check if oldest batch no and serial nos are picked
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})

View File

@@ -62,6 +62,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Warehouse",
"mandatory_depends_on": "eval: parent.pick_manually",
"options": "Warehouse",
"read_only": 1
},
@@ -284,7 +285,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-18 21:09:12.737036",
"modified": "2026-03-17 16:25:10.358013",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -2673,26 +2673,38 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
pos_batches = frappe._dict()
pos_invoices = frappe.get_all(
"POS Invoice",
fields=[
"`tabPOS Invoice Item`.batch_no",
"`tabPOS Invoice Item`.qty",
"`tabPOS Invoice`.is_return",
"`tabPOS Invoice Item`.warehouse",
"`tabPOS Invoice Item`.name as child_docname",
"`tabPOS Invoice`.name as parent_docname",
"`tabPOS Invoice Item`.use_serial_batch_fields",
"`tabPOS Invoice Item`.serial_and_batch_bundle",
],
filters=[
["POS Invoice", "consolidated_invoice", "is", "not set"],
["POS Invoice", "docstatus", "=", 1],
["POS Invoice Item", "item_code", "=", kwargs.item_code],
["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos],
],
POS_Invoice = frappe.qb.DocType("POS Invoice")
POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item")
pos_invoices = (
frappe.qb.from_(POS_Invoice)
.inner_join(POS_Invoice_Item)
.on(POS_Invoice.name == POS_Invoice_Item.parent)
.select(
POS_Invoice_Item.batch_no,
POS_Invoice_Item.qty,
POS_Invoice.is_return,
POS_Invoice_Item.warehouse,
POS_Invoice_Item.name.as_("child_docname"),
POS_Invoice.name.as_("parent_docname"),
POS_Invoice_Item.use_serial_batch_fields,
POS_Invoice_Item.serial_and_batch_bundle,
)
.where(
(POS_Invoice.consolidated_invoice.isnull())
& (POS_Invoice.docstatus == 1)
& (POS_Invoice_Item.item_code == kwargs.item_code)
)
)
if kwargs.get("company"):
pos_invoices = pos_invoices.where(POS_Invoice.company == kwargs.get("company"))
if kwargs.get("ignore_voucher_nos"):
pos_invoices = pos_invoices.where(POS_Invoice.name.notin(kwargs.get("ignore_voucher_nos")))
pos_invoices = pos_invoices.run(as_dict=True)
ids = [
pos_invoice.serial_and_batch_bundle
for pos_invoice in pos_invoices
@@ -2755,6 +2767,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
.groupby(sb_entry.batch_no, sre.warehouse)
)
if kwargs.get("company"):
query = query.where(sre.company == kwargs.get("company"))
if kwargs.batch_no:
if isinstance(kwargs.batch_no, list):
query = query.where(sb_entry.batch_no.isin(kwargs.batch_no))
@@ -2979,6 +2994,9 @@ def get_available_batches(kwargs):
.groupby(batch_ledger.batch_no, batch_ledger.warehouse)
)
if kwargs.get("company"):
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
if not kwargs.get("for_stock_levels"):
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
@@ -3088,6 +3106,9 @@ def get_picked_batches(kwargs) -> dict[str, dict]:
)
)
if kwargs.get("company"):
query = query.where(table.company == kwargs.get("company"))
if kwargs.get("item_code"):
query = query.where(table.item_code == kwargs.get("item_code"))
@@ -3304,6 +3325,9 @@ def get_stock_ledgers_batches(kwargs):
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
)
if kwargs.get("company"):
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
for field in ["warehouse", "item_code", "batch_no"]:
if not kwargs.get(field):
continue