mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-12 03:15:07 +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,
|
bom_no: frm.doc.name,
|
||||||
item: item,
|
item: item,
|
||||||
qty: data.qty || 0.0,
|
qty: data.qty || 0.0,
|
||||||
|
company: frm.doc.company,
|
||||||
project: frm.doc.project,
|
project: frm.doc.project,
|
||||||
variant_items: variant_items,
|
variant_items: variant_items,
|
||||||
use_multi_level_bom: frm.doc?.track_semi_finished_goods ? 0 : use_multi_level_bom,
|
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()
|
@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"):
|
if not frappe.has_permission("Work Order", "write"):
|
||||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
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 = frappe.new_doc("Work Order")
|
||||||
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
|
wo_doc.track_semi_finished_goods = frappe.db.get_value("BOM", bom_no, "track_semi_finished_goods")
|
||||||
wo_doc.production_item = item
|
wo_doc.production_item = item
|
||||||
|
wo_doc.company = company or get_default_company()
|
||||||
wo_doc.update(item_details)
|
wo_doc.update(item_details)
|
||||||
wo_doc.bom_no = bom_no
|
wo_doc.bom_no = bom_no
|
||||||
wo_doc.use_multi_level_bom = cint(use_multi_level_bom)
|
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
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
|
||||||
|
|
||||||
|
class MissingWarehouseValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IncorrectWarehouseValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# TODO: Prioritize SO or WO group warehouse
|
# TODO: Prioritize SO or WO group warehouse
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +117,7 @@ class PickList(TransactionBase):
|
|||||||
|
|
||||||
if self.get("locations"):
|
if self.get("locations"):
|
||||||
self.validate_sales_order_percentage()
|
self.validate_sales_order_percentage()
|
||||||
|
self.validate_warehouses()
|
||||||
|
|
||||||
def validate_stock_qty(self):
|
def validate_stock_qty(self):
|
||||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||||
@@ -152,6 +162,31 @@ class PickList(TransactionBase):
|
|||||||
title=_("Insufficient Stock"),
|
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):
|
def check_serial_no_status(self):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
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(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1058,6 +1094,7 @@ def get_available_item_locations_for_serial_and_batched_item(
|
|||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
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(
|
def get_available_item_locations_for_batched_item(
|
||||||
item_code,
|
item_code,
|
||||||
from_warehouses,
|
from_warehouses,
|
||||||
|
company,
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
@@ -1146,6 +1184,7 @@ def get_available_item_locations_for_batched_item(
|
|||||||
{
|
{
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"warehouse": from_warehouses,
|
"warehouse": from_warehouses,
|
||||||
|
"company": company,
|
||||||
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class TestPickList(IntegrationTestCase):
|
|||||||
"qty": 1000,
|
"qty": 1000,
|
||||||
"stock_qty": 1000,
|
"stock_qty": 1000,
|
||||||
"conversion_factor": 1,
|
"conversion_factor": 1,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
"sales_order": so.name,
|
"sales_order": so.name,
|
||||||
"sales_order_item": so.items[0].name,
|
"sales_order_item": so.items[0].name,
|
||||||
}
|
}
|
||||||
@@ -268,6 +269,119 @@ class TestPickList(IntegrationTestCase):
|
|||||||
pr1.cancel()
|
pr1.cancel()
|
||||||
pr2.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):
|
def test_pick_list_for_batched_and_serialised_item(self):
|
||||||
# check if oldest batch no and serial nos are picked
|
# check if oldest batch no and serial nos are picked
|
||||||
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"})
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Warehouse",
|
"label": "Warehouse",
|
||||||
|
"mandatory_depends_on": "eval: parent.pick_manually",
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -284,7 +285,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-18 21:09:12.737036",
|
"modified": "2026-03-17 16:25:10.358013",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"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."""
|
"""Returns a dict of `Batch No` followed by the `Qty` reserved in POS Invoices."""
|
||||||
|
|
||||||
pos_batches = frappe._dict()
|
pos_batches = frappe._dict()
|
||||||
pos_invoices = frappe.get_all(
|
POS_Invoice = frappe.qb.DocType("POS Invoice")
|
||||||
"POS Invoice",
|
POS_Invoice_Item = frappe.qb.DocType("POS Invoice Item")
|
||||||
fields=[
|
|
||||||
"`tabPOS Invoice Item`.batch_no",
|
pos_invoices = (
|
||||||
"`tabPOS Invoice Item`.qty",
|
frappe.qb.from_(POS_Invoice)
|
||||||
"`tabPOS Invoice`.is_return",
|
.inner_join(POS_Invoice_Item)
|
||||||
"`tabPOS Invoice Item`.warehouse",
|
.on(POS_Invoice.name == POS_Invoice_Item.parent)
|
||||||
"`tabPOS Invoice Item`.name as child_docname",
|
.select(
|
||||||
"`tabPOS Invoice`.name as parent_docname",
|
POS_Invoice_Item.batch_no,
|
||||||
"`tabPOS Invoice Item`.use_serial_batch_fields",
|
POS_Invoice_Item.qty,
|
||||||
"`tabPOS Invoice Item`.serial_and_batch_bundle",
|
POS_Invoice.is_return,
|
||||||
],
|
POS_Invoice_Item.warehouse,
|
||||||
filters=[
|
POS_Invoice_Item.name.as_("child_docname"),
|
||||||
["POS Invoice", "consolidated_invoice", "is", "not set"],
|
POS_Invoice.name.as_("parent_docname"),
|
||||||
["POS Invoice", "docstatus", "=", 1],
|
POS_Invoice_Item.use_serial_batch_fields,
|
||||||
["POS Invoice Item", "item_code", "=", kwargs.item_code],
|
POS_Invoice_Item.serial_and_batch_bundle,
|
||||||
["POS Invoice", "name", "not in", kwargs.ignore_voucher_nos],
|
)
|
||||||
],
|
.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 = [
|
ids = [
|
||||||
pos_invoice.serial_and_batch_bundle
|
pos_invoice.serial_and_batch_bundle
|
||||||
for pos_invoice in pos_invoices
|
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)
|
.groupby(sb_entry.batch_no, sre.warehouse)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if kwargs.get("company"):
|
||||||
|
query = query.where(sre.company == kwargs.get("company"))
|
||||||
|
|
||||||
if kwargs.batch_no:
|
if kwargs.batch_no:
|
||||||
if isinstance(kwargs.batch_no, list):
|
if isinstance(kwargs.batch_no, list):
|
||||||
query = query.where(sb_entry.batch_no.isin(kwargs.batch_no))
|
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)
|
.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"):
|
if not kwargs.get("for_stock_levels"):
|
||||||
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
|
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"):
|
if kwargs.get("item_code"):
|
||||||
query = query.where(table.item_code == 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)
|
.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"]:
|
for field in ["warehouse", "item_code", "batch_no"]:
|
||||||
if not kwargs.get(field):
|
if not kwargs.get(field):
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user