Skip to content

Commit 91ee45a

Browse files
mergify[bot]Sudharsanan11mihir-kandoi
authored
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)
1 parent dd0013e commit 91ee45a

6 files changed

Lines changed: 204 additions & 20 deletions

File tree

erpnext/manufacturing/doctype/bom/bom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ frappe.ui.form.on("BOM", {
297297
bom_no: frm.doc.name,
298298
item: item,
299299
qty: data.qty || 0.0,
300+
company: frm.doc.company,
300301
project: frm.doc.project,
301302
variant_items: variant_items,
302303
use_multi_level_bom: frm.doc?.track_semi_finished_goods ? 0 : use_multi_level_bom,

erpnext/manufacturing/doctype/work_order/work_order.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2261,7 +2261,11 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True):
22612261

22622262

22632263
@frappe.whitelist()
2264-
def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None):
2264+
def make_work_order(
2265+
bom_no, item, qty=0, company=None, project=None, variant_items=None, use_multi_level_bom=None
2266+
):
2267+
from erpnext import get_default_company
2268+
22652269
if not frappe.has_permission("Work Order", "write"):
22662270
frappe.throw(_("Not permitted"), frappe.PermissionError)
22672271

@@ -2277,6 +2281,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
22772281
wo_doc = frappe.new_doc("Work Order")
22782282
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
22792283
wo_doc.production_item = item
2284+
wo_doc.company = company or get_default_company()
22802285
wo_doc.update(item_details)
22812286
wo_doc.bom_no = bom_no
22822287
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)

erpnext/stock/doctype/pick_list/pick_list.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
)
2929
from erpnext.utilities.transaction_base import TransactionBase
3030

31+
32+
class MissingWarehouseValidationError(frappe.ValidationError):
33+
pass
34+
35+
36+
class IncorrectWarehouseValidationError(frappe.ValidationError):
37+
pass
38+
39+
3140
# TODO: Prioritize SO or WO group warehouse
3241

3342

@@ -108,6 +117,7 @@ def before_save(self):
108117

109118
if self.get("locations"):
110119
self.validate_sales_order_percentage()
120+
self.validate_warehouses()
111121

112122
def validate_stock_qty(self):
113123
from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -152,6 +162,31 @@ def validate_stock_qty(self):
152162
title=_("Insufficient Stock"),
153163
)
154164

165+
def validate_warehouses(self):
166+
for location in self.locations:
167+
if not location.warehouse:
168+
frappe.throw(
169+
_("Row {0}: Warehouse is required").format(location.idx),
170+
title=_("Missing Warehouse"),
171+
exc=MissingWarehouseValidationError,
172+
)
173+
174+
company = frappe.get_cached_value("Warehouse", location.warehouse, "company")
175+
176+
if company != self.company:
177+
frappe.throw(
178+
_(
179+
"Row {0}: Warehouse {1} is linked to company {2}. Please select a warehouse belonging to company {3}."
180+
).format(
181+
location.idx,
182+
frappe.bold(location.warehouse),
183+
frappe.bold(company),
184+
frappe.bold(self.company),
185+
),
186+
title=_("Incorrect Warehouse"),
187+
exc=IncorrectWarehouseValidationError,
188+
)
189+
155190
def check_serial_no_status(self):
156191
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
157192

@@ -958,6 +993,7 @@ def get_available_item_locations(
958993
locations = get_available_item_locations_for_batched_item(
959994
item_code,
960995
from_warehouses,
996+
company,
961997
consider_rejected_warehouses=consider_rejected_warehouses,
962998
)
963999
else:
@@ -1058,6 +1094,7 @@ def get_available_item_locations_for_serial_and_batched_item(
10581094
locations = get_available_item_locations_for_batched_item(
10591095
item_code,
10601096
from_warehouses,
1097+
company,
10611098
consider_rejected_warehouses=consider_rejected_warehouses,
10621099
)
10631100

@@ -1138,6 +1175,7 @@ def get_available_item_locations_for_serialized_item(
11381175
def get_available_item_locations_for_batched_item(
11391176
item_code,
11401177
from_warehouses,
1178+
company,
11411179
consider_rejected_warehouses=False,
11421180
):
11431181
locations = []
@@ -1146,6 +1184,7 @@ def get_available_item_locations_for_batched_item(
11461184
{
11471185
"item_code": item_code,
11481186
"warehouse": from_warehouses,
1187+
"company": company,
11491188
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
11501189
}
11511190
)

erpnext/stock/doctype/pick_list/test_pick_list.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def test_pick_list_shows_serial_no_for_serialized_item(self):
211211
"qty": 1000,
212212
"stock_qty": 1000,
213213
"conversion_factor": 1,
214+
"warehouse": "_Test Warehouse - _TC",
214215
"sales_order": so.name,
215216
"sales_order_item": so.items[0].name,
216217
}
@@ -268,6 +269,119 @@ def test_pick_list_shows_batch_no_for_batched_item(self):
268269
pr1.cancel()
269270
pr2.cancel()
270271

272+
def test_pick_list_warehouse_for_batched_item(self):
273+
"""
274+
Test that pick list respects company based warehouse assignment for batched items.
275+
276+
This test verifies that when creating a pick list for a batched item,
277+
the system correctly identifies and assigns the appropriate warehouse
278+
based on the company.
279+
"""
280+
from erpnext.stock.doctype.batch.test_batch import make_new_batch
281+
282+
batch_company = frappe.get_doc(
283+
{"doctype": "Company", "company_name": "Batch Company", "default_currency": "INR"}
284+
)
285+
batch_company.insert()
286+
287+
batch_warehouse = frappe.get_doc(
288+
{
289+
"doctype": "Warehouse",
290+
"warehouse_name": "Batch Warehouse",
291+
"company": batch_company.name,
292+
}
293+
)
294+
batch_warehouse.insert()
295+
296+
batch_item = frappe.db.exists("Item", "Batch Warehouse Item")
297+
if not batch_item:
298+
batch_item = create_item("Batch Warehouse Item")
299+
batch_item.has_batch_no = 1
300+
batch_item.create_new_batch = 1
301+
batch_item.save()
302+
else:
303+
batch_item = frappe.get_doc("Item", "Batch Warehouse Item")
304+
305+
batch_no = make_new_batch(item_code=batch_item.name, batch_id="B-WH-ITEM-001")
306+
307+
make_stock_entry(
308+
item_code=batch_item.name,
309+
qty=5,
310+
company=batch_company.name,
311+
to_warehouse=batch_warehouse.name,
312+
batch_no=batch_no.name,
313+
rate=100.0,
314+
)
315+
make_stock_entry(
316+
item_code=batch_item.name,
317+
qty=5,
318+
to_warehouse="_Test Warehouse - _TC",
319+
batch_no=batch_no.name,
320+
rate=100.0,
321+
)
322+
323+
pick_list = frappe.get_doc(
324+
{
325+
"doctype": "Pick List",
326+
"company": batch_company.name,
327+
"purpose": "Material Transfer",
328+
"locations": [
329+
{
330+
"item_code": batch_item.name,
331+
"qty": 10,
332+
"stock_qty": 10,
333+
"conversion_factor": 1,
334+
}
335+
],
336+
}
337+
)
338+
339+
pick_list.set_item_locations()
340+
self.assertEqual(len(pick_list.locations), 1)
341+
self.assertEqual(pick_list.locations[0].qty, 5)
342+
self.assertEqual(pick_list.locations[0].batch_no, batch_no.name)
343+
self.assertEqual(pick_list.locations[0].warehouse, batch_warehouse.name)
344+
345+
def test_pick_list_warehouse_validation(self):
346+
"""check if the warehouse validations are triggered"""
347+
from erpnext.stock.doctype.pick_list.pick_list import (
348+
IncorrectWarehouseValidationError,
349+
MissingWarehouseValidationError,
350+
)
351+
352+
warehouse_item = create_item("Warehouse Item")
353+
temp_company = frappe.get_doc(
354+
{"doctype": "Company", "company_name": "Temp Company", "default_currency": "INR"}
355+
).insert()
356+
temp_warehouse = frappe.get_doc(
357+
{"doctype": "Warehouse", "warehouse_name": "Temp Warehouse", "company": temp_company.name}
358+
).insert()
359+
360+
make_stock_entry(item_code=warehouse_item.name, qty=10, rate=100.0, to_warehouse=temp_warehouse.name)
361+
362+
pick_list = frappe.get_doc(
363+
{
364+
"doctype": "Pick List",
365+
"company": temp_company.name,
366+
"purpose": "Material Transfer",
367+
"pick_manually": 1,
368+
"locations": [
369+
{
370+
"item_code": warehouse_item.name,
371+
"qty": 5,
372+
"stock_qty": 5,
373+
"conversion_factor": 1,
374+
}
375+
],
376+
}
377+
)
378+
379+
self.assertRaises(MissingWarehouseValidationError, pick_list.insert)
380+
pick_list.locations[0].warehouse = "_Test Warehouse - _TC"
381+
self.assertRaises(IncorrectWarehouseValidationError, pick_list.insert)
382+
pick_list.locations[0].warehouse = temp_warehouse.name
383+
pick_list.insert()
384+
271385
def test_pick_list_for_batched_and_serialised_item(self):
272386
# check if oldest batch no and serial nos are picked
273387
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})

erpnext/stock/doctype/pick_list_item/pick_list_item.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"fieldtype": "Link",
6363
"in_list_view": 1,
6464
"label": "Warehouse",
65+
"mandatory_depends_on": "eval: parent.pick_manually",
6566
"options": "Warehouse",
6667
"read_only": 1
6768
},
@@ -284,7 +285,7 @@
284285
],
285286
"istable": 1,
286287
"links": [],
287-
"modified": "2025-12-18 21:09:12.737036",
288+
"modified": "2026-03-17 16:25:10.358013",
288289
"modified_by": "Administrator",
289290
"module": "Stock",
290291
"name": "Pick List Item",

erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,26 +2673,38 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
26732673
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
26742674

26752675
pos_batches = frappe._dict()
2676-
pos_invoices = frappe.get_all(
2677-
"POS Invoice",
2678-
fields=[
2679-
"`tabPOS Invoice Item`.batch_no",
2680-
"`tabPOS Invoice Item`.qty",
2681-
"`tabPOS Invoice`.is_return",
2682-
"`tabPOS Invoice Item`.warehouse",
2683-
"`tabPOS Invoice Item`.name as child_docname",
2684-
"`tabPOS Invoice`.name as parent_docname",
2685-
"`tabPOS Invoice Item`.use_serial_batch_fields",
2686-
"`tabPOS Invoice Item`.serial_and_batch_bundle",
2687-
],
2688-
filters=[
2689-
["POS Invoice", "consolidated_invoice", "is", "not set"],
2690-
["POS Invoice", "docstatus", "=", 1],
2691-
["POS Invoice Item", "item_code", "=", kwargs.item_code],
2692-
["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos],
2693-
],
2676+
POS_Invoice = frappe.qb.DocType("POS Invoice")
2677+
POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item")
2678+
2679+
pos_invoices = (
2680+
frappe.qb.from_(POS_Invoice)
2681+
.inner_join(POS_Invoice_Item)
2682+
.on(POS_Invoice.name == POS_Invoice_Item.parent)
2683+
.select(
2684+
POS_Invoice_Item.batch_no,
2685+
POS_Invoice_Item.qty,
2686+
POS_Invoice.is_return,
2687+
POS_Invoice_Item.warehouse,
2688+
POS_Invoice_Item.name.as_("child_docname"),
2689+
POS_Invoice.name.as_("parent_docname"),
2690+
POS_Invoice_Item.use_serial_batch_fields,
2691+
POS_Invoice_Item.serial_and_batch_bundle,
2692+
)
2693+
.where(
2694+
(POS_Invoice.consolidated_invoice.isnull())
2695+
& (POS_Invoice.docstatus == 1)
2696+
& (POS_Invoice_Item.item_code == kwargs.item_code)
2697+
)
26942698
)
26952699

2700+
if kwargs.get("company"):
2701+
pos_invoices = pos_invoices.where(POS_Invoice.company == kwargs.get("company"))
2702+
2703+
if kwargs.get("ignore_voucher_nos"):
2704+
pos_invoices = pos_invoices.where(POS_Invoice.name.notin(kwargs.get("ignore_voucher_nos")))
2705+
2706+
pos_invoices = pos_invoices.run(as_dict=True)
2707+
26962708
ids = [
26972709
pos_invoice.serial_and_batch_bundle
26982710
for pos_invoice in pos_invoices
@@ -2755,6 +2767,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
27552767
.groupby(sb_entry.batch_no, sre.warehouse)
27562768
)
27572769

2770+
if kwargs.get("company"):
2771+
query = query.where(sre.company == kwargs.get("company"))
2772+
27582773
if kwargs.batch_no:
27592774
if isinstance(kwargs.batch_no, list):
27602775
query = query.where(sb_entry.batch_no.isin(kwargs.batch_no))
@@ -2979,6 +2994,9 @@ def get_available_batches(kwargs):
29792994
.groupby(batch_ledger.batch_no, batch_ledger.warehouse)
29802995
)
29812996

2997+
if kwargs.get("company"):
2998+
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
2999+
29823000
if not kwargs.get("for_stock_levels"):
29833001
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
29843002

@@ -3088,6 +3106,9 @@ def get_picked_batches(kwargs) -> dict[str, dict]:
30883106
)
30893107
)
30903108

3109+
if kwargs.get("company"):
3110+
query = query.where(table.company == kwargs.get("company"))
3111+
30913112
if kwargs.get("item_code"):
30923113
query = query.where(table.item_code == kwargs.get("item_code"))
30933114

@@ -3304,6 +3325,9 @@ def get_stock_ledgers_batches(kwargs):
33043325
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
33053326
)
33063327

3328+
if kwargs.get("company"):
3329+
query = query.where(stock_ledger_entry.company == kwargs.get("company"))
3330+
33073331
for field in ["warehouse", "item_code", "batch_no"]:
33083332
if not kwargs.get(field):
33093333
continue

0 commit comments

Comments
 (0)