mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-08 17:35:08 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user