mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-29 14:48:26 +00:00
Merge pull request #56948 from aerele/feat/production-plan-group-rm-warehouse
feat(manufacturing): allow group warehouse for raw material availability in production plan
This commit is contained in:
@@ -40,10 +40,29 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("for_warehouse", function (doc) {
|
frm.set_query("for_warehouse", function (doc) {
|
||||||
|
// when a group is chosen, For Warehouse must be one of its child warehouses
|
||||||
|
if (doc.raw_material_group_warehouse) {
|
||||||
|
return {
|
||||||
|
query: "erpnext.manufacturing.doctype.production_plan.production_plan.get_child_warehouses",
|
||||||
|
filters: {
|
||||||
|
group_warehouse: doc.raw_material_group_warehouse,
|
||||||
|
company: doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
filters: [
|
||||||
|
["Warehouse", "company", "=", doc.company],
|
||||||
|
["Warehouse", "is_group", "=", 0],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("raw_material_group_warehouse", function (doc) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
company: doc.company,
|
company: doc.company,
|
||||||
is_group: 0,
|
is_group: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -102,6 +121,13 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
raw_material_group_warehouse(frm) {
|
||||||
|
// For Warehouse must sit inside the chosen group, so drop a stale selection
|
||||||
|
if (frm.doc.for_warehouse) {
|
||||||
|
frm.set_value("for_warehouse", null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.trigger("show_progress");
|
frm.trigger("show_progress");
|
||||||
@@ -451,6 +477,7 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
frm.events.get_items_for_material_requests(frm);
|
frm.events.get_items_for_material_requests(frm);
|
||||||
} else {
|
} else {
|
||||||
const title = __("Transfer Materials For Warehouse {0}", [frm.doc.for_warehouse]);
|
const title = __("Transfer Materials For Warehouse {0}", [frm.doc.for_warehouse]);
|
||||||
|
const source_warehouse = frm.doc.raw_material_group_warehouse;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: title,
|
title: title,
|
||||||
fields: [
|
fields: [
|
||||||
@@ -459,6 +486,7 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
fieldtype: "Table MultiSelect",
|
fieldtype: "Table MultiSelect",
|
||||||
fieldname: "warehouses",
|
fieldname: "warehouses",
|
||||||
options: "Production Plan Material Request Warehouse",
|
options: "Production Plan Material Request Warehouse",
|
||||||
|
default: source_warehouse ? [{ warehouse: source_warehouse }] : [],
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -515,8 +543,9 @@ frappe.ui.form.on("Production Plan", {
|
|||||||
download_materials_required(frm) {
|
download_materials_required(frm) {
|
||||||
const warehouses_data = [];
|
const warehouses_data = [];
|
||||||
|
|
||||||
if (frm.doc.for_warehouse) {
|
const availability_warehouse = frm.doc.raw_material_group_warehouse || frm.doc.for_warehouse;
|
||||||
warehouses_data.push({ warehouse: frm.doc.for_warehouse });
|
if (availability_warehouse) {
|
||||||
|
warehouses_data.push({ warehouse: availability_warehouse });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"include_safety_stock",
|
"include_safety_stock",
|
||||||
"ignore_existing_ordered_qty",
|
"ignore_existing_ordered_qty",
|
||||||
"column_break_25",
|
"column_break_25",
|
||||||
|
"raw_material_group_warehouse",
|
||||||
"for_warehouse",
|
"for_warehouse",
|
||||||
"get_items_for_mr",
|
"get_items_for_mr",
|
||||||
"transfer_materials",
|
"transfer_materials",
|
||||||
@@ -318,6 +319,13 @@
|
|||||||
"label": "For Warehouse",
|
"label": "For Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Optional group warehouse. Raw material availability is checked across its child warehouses; material is still received into For Warehouse.",
|
||||||
|
"fieldname": "raw_material_group_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Raw Material Group Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "warehouses",
|
"fieldname": "warehouses",
|
||||||
"fieldtype": "Table MultiSelect",
|
"fieldtype": "Table MultiSelect",
|
||||||
@@ -445,7 +453,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-12 19:48:09.302503",
|
"modified": "2026-07-07 00:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class ProductionPlan(Document):
|
|||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
prod_plan_references: DF.Table[ProductionPlanItemReference]
|
prod_plan_references: DF.Table[ProductionPlanItemReference]
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
|
raw_material_group_warehouse: DF.Link | None
|
||||||
reserve_stock: DF.Check
|
reserve_stock: DF.Check
|
||||||
sales_order_status: DF.Literal["", "To Deliver and Bill", "To Bill", "To Deliver"]
|
sales_order_status: DF.Literal["", "To Deliver and Bill", "To Bill", "To Deliver"]
|
||||||
sales_orders: DF.Table[ProductionPlanSalesOrder]
|
sales_orders: DF.Table[ProductionPlanSalesOrder]
|
||||||
@@ -144,8 +145,30 @@ class ProductionPlan(Document):
|
|||||||
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
||||||
self.validate_sales_orders()
|
self.validate_sales_orders()
|
||||||
self.validate_material_request_type()
|
self.validate_material_request_type()
|
||||||
|
self.validate_raw_material_group_warehouse()
|
||||||
self.enable_auto_reserve_stock()
|
self.enable_auto_reserve_stock()
|
||||||
|
|
||||||
|
def validate_raw_material_group_warehouse(self):
|
||||||
|
if not self.raw_material_group_warehouse:
|
||||||
|
return
|
||||||
|
|
||||||
|
group = frappe.db.get_value(
|
||||||
|
"Warehouse", self.raw_material_group_warehouse, ["lft", "rgt", "is_group"], as_dict=True
|
||||||
|
)
|
||||||
|
if not group.is_group:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} must be a group warehouse.").format(frappe.bold(_("Raw Material Group Warehouse")))
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.for_warehouse:
|
||||||
|
child = frappe.db.get_value("Warehouse", self.for_warehouse, ["lft", "rgt"], as_dict=True)
|
||||||
|
if not (group.lft <= child.lft and child.rgt <= group.rgt):
|
||||||
|
frappe.throw(
|
||||||
|
_("For Warehouse {0} must be a child of the group warehouse {1}.").format(
|
||||||
|
frappe.bold(self.for_warehouse), frappe.bold(self.raw_material_group_warehouse)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def enable_auto_reserve_stock(self):
|
def enable_auto_reserve_stock(self):
|
||||||
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
|
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
|
||||||
self.reserve_stock = 1
|
self.reserve_stock = 1
|
||||||
@@ -466,3 +489,26 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
def all_items_completed(self):
|
def all_items_completed(self):
|
||||||
return SubAssemblyService(self).all_items_completed()
|
return SubAssemblyService(self).all_items_completed()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
def get_child_warehouses(
|
||||||
|
doctype: str | None, txt: str, searchfield: str | None, start: int, page_len: int, filters: dict
|
||||||
|
):
|
||||||
|
"Leaf warehouses under the given group warehouse, for the For Warehouse link query."
|
||||||
|
bounds = frappe.db.get_value("Warehouse", filters.get("group_warehouse"), ["lft", "rgt"], as_dict=True)
|
||||||
|
if not bounds:
|
||||||
|
return []
|
||||||
|
|
||||||
|
wh = frappe.qb.DocType("Warehouse")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(wh)
|
||||||
|
.select(wh.name)
|
||||||
|
.where((wh.is_group == 0) & (wh.lft >= bounds.lft) & (wh.rgt <= bounds.rgt))
|
||||||
|
)
|
||||||
|
if filters.get("company"):
|
||||||
|
query = query.where(wh.company == filters.get("company"))
|
||||||
|
if txt:
|
||||||
|
query = query.where(wh[searchfield].like(f"%{txt}%"))
|
||||||
|
return query.limit(page_len).offset(start).run()
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ class MaterialRequestService:
|
|||||||
|
|
||||||
def _material_request_item(self, item, material_request_type, schedule_date):
|
def _material_request_item(self, item, material_request_type, schedule_date):
|
||||||
from_warehouse = item.from_warehouse if material_request_type == "Material Transfer" else None
|
from_warehouse = item.from_warehouse if material_request_type == "Material Transfer" else None
|
||||||
|
# a group warehouse cannot receive stock; it must never reach a Material Request line
|
||||||
|
if item.warehouse and frappe.get_cached_value("Warehouse", item.warehouse, "is_group"):
|
||||||
|
frappe.throw(
|
||||||
|
_("Cannot create Material Request for item {0} in group warehouse {1}.").format(
|
||||||
|
frappe.bold(item.item_code), frappe.bold(item.warehouse)
|
||||||
|
)
|
||||||
|
)
|
||||||
project = (
|
project = (
|
||||||
frappe.db.get_value("Sales Order", item.sales_order, "project") if item.sales_order else None
|
frappe.db.get_value("Sales Order", item.sales_order, "project") if item.sales_order else None
|
||||||
)
|
)
|
||||||
@@ -132,13 +139,14 @@ class MaterialRequestService:
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items_for_material_requests(
|
def get_items_for_material_requests(
|
||||||
doc: str | frappe._dict | Document,
|
doc: str | dict | Document,
|
||||||
warehouses: str | list | None = None,
|
warehouses: str | list | None = None,
|
||||||
get_parent_warehouse_data: bool | int | None = None,
|
get_parent_warehouse_data: bool | int | None = None,
|
||||||
):
|
):
|
||||||
frappe.has_permission("Production Plan", "read", throw=True)
|
frappe.has_permission("Production Plan", "read", throw=True)
|
||||||
|
|
||||||
doc = _normalize_mr_doc(doc)
|
doc = _normalize_mr_doc(doc)
|
||||||
|
_validate_group_warehouse_target(doc)
|
||||||
warehouses = _filter_warehouses(doc, warehouses, get_parent_warehouse_data)
|
warehouses = _filter_warehouses(doc, warehouses, get_parent_warehouse_data)
|
||||||
doc["mr_items"] = []
|
doc["mr_items"] = []
|
||||||
|
|
||||||
@@ -163,6 +171,17 @@ def _normalize_mr_doc(doc):
|
|||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_group_warehouse_target(doc):
|
||||||
|
# the group only scopes availability; raw materials still need a concrete
|
||||||
|
# receiving warehouse, so for_warehouse is required once we generate items.
|
||||||
|
if doc.get("raw_material_group_warehouse") and not doc.get("for_warehouse"):
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} is required to get raw materials when {1} is set.").format(
|
||||||
|
frappe.bold(_("For Warehouse")), frappe.bold(_("Raw Material Group Warehouse"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _filter_warehouses(doc, warehouses, get_parent_warehouse_data):
|
def _filter_warehouses(doc, warehouses, get_parent_warehouse_data):
|
||||||
if not warehouses:
|
if not warehouses:
|
||||||
return warehouses
|
return warehouses
|
||||||
@@ -355,13 +374,18 @@ def _accumulate_so_items(so_item_details, sales_order, item_details, qty_precisi
|
|||||||
def _build_mr_items(doc, so_item_details, ignore_ordered_qty):
|
def _build_mr_items(doc, so_item_details, ignore_ordered_qty):
|
||||||
mr_items = []
|
mr_items = []
|
||||||
consumed_qty = defaultdict(float)
|
consumed_qty = defaultdict(float)
|
||||||
warehouse = doc.get("for_warehouse")
|
# raw_material_group_warehouse (optional, group) only widens the availability
|
||||||
|
# scope to its child warehouses; material is still received into for_warehouse.
|
||||||
|
target_warehouse = doc.get("for_warehouse")
|
||||||
|
scope_warehouse = doc.get("raw_material_group_warehouse") or target_warehouse
|
||||||
company = doc.get("company")
|
company = doc.get("company")
|
||||||
include_safety_stock = doc.get("include_safety_stock")
|
include_safety_stock = doc.get("include_safety_stock")
|
||||||
|
|
||||||
for sales_order, item_dict in so_item_details.items():
|
for sales_order, item_dict in so_item_details.items():
|
||||||
for details in item_dict.values():
|
for details in item_dict.values():
|
||||||
warehouse = warehouse or details.get("source_warehouse") or details.get("default_warehouse")
|
fallback = details.get("source_warehouse") or details.get("default_warehouse")
|
||||||
|
scope_warehouse = scope_warehouse or fallback
|
||||||
|
target_warehouse = target_warehouse or fallback
|
||||||
row = _mr_item_for_details(
|
row = _mr_item_for_details(
|
||||||
doc,
|
doc,
|
||||||
details,
|
details,
|
||||||
@@ -369,7 +393,8 @@ def _build_mr_items(doc, so_item_details, ignore_ordered_qty):
|
|||||||
company,
|
company,
|
||||||
ignore_ordered_qty,
|
ignore_ordered_qty,
|
||||||
include_safety_stock,
|
include_safety_stock,
|
||||||
warehouse,
|
scope_warehouse,
|
||||||
|
target_warehouse,
|
||||||
consumed_qty,
|
consumed_qty,
|
||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
@@ -378,10 +403,19 @@ def _build_mr_items(doc, so_item_details, ignore_ordered_qty):
|
|||||||
|
|
||||||
|
|
||||||
def _mr_item_for_details(
|
def _mr_item_for_details(
|
||||||
doc, details, sales_order, company, ignore_ordered_qty, include_safety_stock, warehouse, consumed_qty
|
doc,
|
||||||
|
details,
|
||||||
|
sales_order,
|
||||||
|
company,
|
||||||
|
ignore_ordered_qty,
|
||||||
|
include_safety_stock,
|
||||||
|
warehouse,
|
||||||
|
target_warehouse,
|
||||||
|
consumed_qty,
|
||||||
):
|
):
|
||||||
bin_dict = get_bin_details(details, doc.company, warehouse)
|
# get_bin_details scopes to the warehouse's descendants, returning one row per
|
||||||
bin_dict = bin_dict[0] if bin_dict else {}
|
# child warehouse; sum them so a group warehouse reflects combined child stock.
|
||||||
|
bin_dict = _aggregate_bin_details(get_bin_details(details, doc.company, warehouse))
|
||||||
if details.qty <= 0:
|
if details.qty <= 0:
|
||||||
return None
|
return None
|
||||||
return get_material_request_items(
|
return get_material_request_items(
|
||||||
@@ -392,11 +426,27 @@ def _mr_item_for_details(
|
|||||||
ignore_ordered_qty,
|
ignore_ordered_qty,
|
||||||
include_safety_stock,
|
include_safety_stock,
|
||||||
warehouse,
|
warehouse,
|
||||||
|
target_warehouse,
|
||||||
bin_dict,
|
bin_dict,
|
||||||
consumed_qty,
|
consumed_qty,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_bin_details(bin_list):
|
||||||
|
qty_fields = (
|
||||||
|
"projected_qty",
|
||||||
|
"actual_qty",
|
||||||
|
"ordered_qty",
|
||||||
|
"reserved_qty_for_production",
|
||||||
|
"planned_qty",
|
||||||
|
)
|
||||||
|
aggregated = {field: 0 for field in qty_fields}
|
||||||
|
for row in bin_list or []:
|
||||||
|
for field in qty_fields:
|
||||||
|
aggregated[field] += flt(row.get(field))
|
||||||
|
return aggregated
|
||||||
|
|
||||||
|
|
||||||
def _apply_other_locations(doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data):
|
def _apply_other_locations(doc, mr_items, warehouses, ignore_ordered_qty, get_parent_warehouse_data):
|
||||||
if not ((ignore_ordered_qty or get_parent_warehouse_data) and warehouses):
|
if not ((ignore_ordered_qty or get_parent_warehouse_data) and warehouses):
|
||||||
return mr_items
|
return mr_items
|
||||||
@@ -428,6 +478,7 @@ def get_material_request_items(
|
|||||||
ignore_existing_ordered_qty,
|
ignore_existing_ordered_qty,
|
||||||
include_safety_stock,
|
include_safety_stock,
|
||||||
warehouse,
|
warehouse,
|
||||||
|
target_warehouse,
|
||||||
bin_dict,
|
bin_dict,
|
||||||
consumed_qty,
|
consumed_qty,
|
||||||
):
|
):
|
||||||
@@ -438,7 +489,7 @@ def get_material_request_items(
|
|||||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||||
conversion_factor = _mr_purchase_conversion_factor(row)
|
conversion_factor = _mr_purchase_conversion_factor(row)
|
||||||
return _material_request_item_row(
|
return _material_request_item_row(
|
||||||
row, sales_order, warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults
|
row, sales_order, target_warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1592,6 +1592,104 @@ class TestProductionPlan(ERPNextTestSuite):
|
|||||||
for row in plan.mr_items:
|
for row in plan.mr_items:
|
||||||
self.assertFalse(row.from_warehouse)
|
self.assertFalse(row.from_warehouse)
|
||||||
|
|
||||||
|
def _setup_group_rm_warehouse(self):
|
||||||
|
"""FG + RM with a group raw-material warehouse (C1, C2) partially stocked (3 + 4)."""
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
|
|
||||||
|
group_warehouse = "_Test Warehouse Group - _TC"
|
||||||
|
child_1 = "_Test Warehouse Group-C1 - _TC"
|
||||||
|
child_2 = "_Test Warehouse Group-C2 - _TC"
|
||||||
|
|
||||||
|
fg_item = "Test PP Group FG"
|
||||||
|
rm_item = "Test PP Group RM"
|
||||||
|
create_item(rm_item, valuation_rate=100)
|
||||||
|
create_item(fg_item, valuation_rate=100)
|
||||||
|
if not frappe.db.get_value("BOM", {"item": fg_item, "is_active": 1}):
|
||||||
|
create_nested_bom({fg_item: {rm_item: {}}}, prefix="")
|
||||||
|
|
||||||
|
make_stock_entry(item_code=rm_item, qty=3, rate=100, target=child_1)
|
||||||
|
make_stock_entry(item_code=rm_item, qty=4, rate=100, target=child_2)
|
||||||
|
|
||||||
|
return frappe._dict(
|
||||||
|
group_warehouse=group_warehouse,
|
||||||
|
children={child_1, child_2},
|
||||||
|
for_wh=child_1, # a leaf inside the group, used as For Warehouse
|
||||||
|
fg_item=fg_item,
|
||||||
|
rm_item=rm_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_group_raw_material_warehouse_aggregates_child_stock(self):
|
||||||
|
"Combined child stock (3 + 4) is used as projected qty; material targets For Warehouse."
|
||||||
|
data = self._setup_group_rm_warehouse()
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code=data.fg_item,
|
||||||
|
planned_qty=10,
|
||||||
|
for_warehouse=data.for_wh,
|
||||||
|
raw_material_group_warehouse=data.group_warehouse,
|
||||||
|
do_not_save=1,
|
||||||
|
skip_getting_mr_items=1,
|
||||||
|
)
|
||||||
|
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||||
|
|
||||||
|
rm_rows = [d for d in mr_items if d.get("item_code") == data.rm_item]
|
||||||
|
self.assertEqual(len(rm_rows), 1)
|
||||||
|
# projected qty reflects the sum across both child warehouses, not a single child
|
||||||
|
self.assertEqual(flt(rm_rows[0].get("projected_qty")), 7.0)
|
||||||
|
# the group is only an availability scope; the row targets For Warehouse
|
||||||
|
self.assertEqual(rm_rows[0].get("warehouse"), data.for_wh)
|
||||||
|
|
||||||
|
def test_group_raw_material_warehouse_transfers_from_child_warehouses(self):
|
||||||
|
"Material is transferred only from actual child warehouses, never the group node."
|
||||||
|
data = self._setup_group_rm_warehouse()
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code=data.fg_item,
|
||||||
|
planned_qty=10,
|
||||||
|
ignore_existing_ordered_qty=1,
|
||||||
|
for_warehouse=data.for_wh,
|
||||||
|
raw_material_group_warehouse=data.group_warehouse,
|
||||||
|
do_not_save=1,
|
||||||
|
skip_getting_mr_items=1,
|
||||||
|
)
|
||||||
|
mr_items = get_items_for_material_requests(
|
||||||
|
plan.as_dict(), warehouses=[{"warehouse": data.group_warehouse}]
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer_rows = [d for d in mr_items if d.get("material_request_type") == "Material Transfer"]
|
||||||
|
self.assertTrue(transfer_rows)
|
||||||
|
for row in transfer_rows:
|
||||||
|
self.assertIn(row.get("from_warehouse"), data.children)
|
||||||
|
for row in mr_items:
|
||||||
|
# a group warehouse must never be a Material Request target
|
||||||
|
self.assertNotEqual(row.get("warehouse"), data.group_warehouse)
|
||||||
|
|
||||||
|
def test_for_warehouse_must_be_child_of_group(self):
|
||||||
|
"A For Warehouse outside the chosen group warehouse is rejected on save."
|
||||||
|
data = self._setup_group_rm_warehouse()
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code=data.fg_item,
|
||||||
|
planned_qty=10,
|
||||||
|
for_warehouse="_Test Warehouse - _TC", # outside the group
|
||||||
|
raw_material_group_warehouse=data.group_warehouse,
|
||||||
|
do_not_save=1,
|
||||||
|
skip_getting_mr_items=1,
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, plan.save)
|
||||||
|
|
||||||
|
def test_for_warehouse_required_with_group_when_getting_raw_materials(self):
|
||||||
|
"A group warehouse without a For Warehouse is rejected when raw materials are fetched."
|
||||||
|
data = self._setup_group_rm_warehouse()
|
||||||
|
|
||||||
|
plan = create_production_plan(
|
||||||
|
item_code=data.fg_item,
|
||||||
|
planned_qty=10,
|
||||||
|
raw_material_group_warehouse=data.group_warehouse,
|
||||||
|
skip_getting_mr_items=1,
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, get_items_for_material_requests, plan.as_dict())
|
||||||
|
|
||||||
def test_skip_available_qty_for_sub_assembly_items(self):
|
def test_skip_available_qty_for_sub_assembly_items(self):
|
||||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
|
|
||||||
@@ -3122,6 +3220,7 @@ def create_production_plan(**args):
|
|||||||
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
||||||
"reserve_stock": args.reserve_stock or 0,
|
"reserve_stock": args.reserve_stock or 0,
|
||||||
"for_warehouse": args.for_warehouse or None,
|
"for_warehouse": args.for_warehouse or None,
|
||||||
|
"raw_material_group_warehouse": args.raw_material_group_warehouse or None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user