diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 9ae05611b6c..454f1934e13 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -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, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 3c27d502088..4c317203295 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -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) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 472502bd80e..ef80966c2a6 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -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"), } ) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 87c2275d3d1..12c92310147 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -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"}) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index e4ee15f04b0..01630278168 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -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", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 76031397b01..aa7f276ddc1 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -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