Merge pull request #35381 from rohitwaghchaure/fixed-skip-available-qty-for-sub-assembly
feat: provision to skip available sub assembly items in the production plan
This commit is contained in:
commit
4cb9cbee06
@ -35,8 +35,12 @@
|
||||
"section_break_25",
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"get_sub_assembly_items",
|
||||
"combine_sub_items",
|
||||
"section_break_ucc4",
|
||||
"skip_available_sub_assembly_item",
|
||||
"column_break_igxl",
|
||||
"get_sub_assembly_items",
|
||||
"section_break_g4ip",
|
||||
"sub_assembly_items",
|
||||
"download_materials_request_plan_section_section",
|
||||
"download_materials_required",
|
||||
@ -351,12 +355,12 @@
|
||||
{
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"hide_border": 1,
|
||||
"label": "Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan Sub Assembly Item"
|
||||
},
|
||||
@ -392,13 +396,33 @@
|
||||
"fieldname": "download_materials_request_plan_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Download Materials Request Plan Section"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
|
||||
"fieldname": "skip_available_sub_assembly_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Available Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ucc4",
|
||||
"fieldtype": "Column Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_g4ip",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_igxl",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-31 10:30:48.118932",
|
||||
"modified": "2023-05-22 23:36:31.770517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -718,7 +718,9 @@ class ProductionPlan(Document):
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
bom_data = []
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
|
||||
@ -894,7 +896,9 @@ def download_raw_materials(doc, warehouses=None):
|
||||
build_csv_response(item_list, doc.name)
|
||||
|
||||
|
||||
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
|
||||
def get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None
|
||||
):
|
||||
bei = frappe.qb.DocType("BOM Explosion Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
@ -1271,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
include_safety_stock = doc.get("include_safety_stock")
|
||||
|
||||
so_item_details = frappe._dict()
|
||||
|
||||
sub_assembly_items = {}
|
||||
if doc.get("skip_available_sub_assembly_item"):
|
||||
for d in doc.get("sub_assembly_items"):
|
||||
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
|
||||
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
@ -1296,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
|
||||
|
||||
if bom_no:
|
||||
if data.get("include_exploded_items") and include_subcontracted_items:
|
||||
if (
|
||||
data.get("include_exploded_items")
|
||||
and doc.get("sub_assembly_items")
|
||||
and doc.get("skip_available_sub_assembly_item")
|
||||
):
|
||||
item_details = get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
|
||||
elif data.get("include_exploded_items") and include_subcontracted_items:
|
||||
# fetch exploded items from BOM
|
||||
item_details = get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc
|
||||
)
|
||||
else:
|
||||
item_details = get_subitems(
|
||||
@ -1456,12 +1480,22 @@ def get_item_data(item_code):
|
||||
}
|
||||
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
|
||||
data = get_bom_children(parent=bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
if warehouse:
|
||||
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
if bin_dict and bin_dict[0].projected_qty > 0:
|
||||
if bin_dict[0].projected_qty > stock_qty:
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - bin_dict[0].projected_qty
|
||||
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@ -1481,7 +1515,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
)
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1)
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
@ -1519,3 +1553,68 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
)
|
||||
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||
):
|
||||
|
||||
bei = frappe.qb.DocType("BOM Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
||||
|
||||
items = (
|
||||
frappe.qb.from_(bei)
|
||||
.join(bom)
|
||||
.on(bom.name == bei.parent)
|
||||
.join(item)
|
||||
.on(item.name == bei.item_code)
|
||||
.left_join(item_default)
|
||||
.on((item_default.parent == item.name) & (item_default.company == company))
|
||||
.left_join(item_uom)
|
||||
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
|
||||
.select(
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
bei.stock_uom,
|
||||
bei.bom_no,
|
||||
item.min_order_qty,
|
||||
bei.source_warehouse,
|
||||
item.default_material_request_type,
|
||||
item.min_order_qty,
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
& (bom.name == bom_no)
|
||||
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
|
||||
)
|
||||
.groupby(bei.item_code, bei.stock_uom)
|
||||
).run(as_dict=True)
|
||||
|
||||
for item in items:
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.bom_no and key in sub_assembly_items:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
item.bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
item_details.setdefault(item.get("item_code"), item)
|
||||
|
||||
return item_details
|
||||
|
@ -926,6 +926,50 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_skip_available_qty_for_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
bom_tree = {
|
||||
"Fininshed Goods1 For SUB Test": {
|
||||
"SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}},
|
||||
"SubAssembly2 For SUB Test": {},
|
||||
}
|
||||
}
|
||||
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="SubAssembly1 For SUB Test",
|
||||
qty=5,
|
||||
rate=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
self.assertTrue(plan.skip_available_sub_assembly_item)
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
for row in plan.sub_assembly_items:
|
||||
if row.production_item == "SubAssembly1 For SUB Test":
|
||||
self.assertEqual(row.qty, 5)
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
for row in mr_items:
|
||||
row = frappe._dict(row)
|
||||
if row.item_code == "ChildPart1 For SUB Test":
|
||||
self.assertEqual(row.quantity, 5)
|
||||
|
||||
if row.item_code == "SubAssembly2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 10)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
@ -945,6 +989,7 @@ def create_production_plan(**args):
|
||||
"include_subcontracted_items": args.include_subcontracted_items or 0,
|
||||
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
|
||||
"get_items_from": "Sales Order",
|
||||
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||
}
|
||||
)
|
||||
|
||||
@ -958,6 +1003,7 @@ def create_production_plan(**args):
|
||||
"planned_qty": args.planned_qty or 1,
|
||||
"planned_start_date": args.planned_start_date or now_datetime(),
|
||||
"stock_uom": args.stock_uom or "Nos",
|
||||
"warehouse": args.warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,11 @@
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"column_break_22",
|
||||
"description"
|
||||
"description",
|
||||
"section_break_4rxf",
|
||||
"actual_qty",
|
||||
"column_break_xfhm",
|
||||
"projected_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -183,12 +187,34 @@
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Schedule Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4rxf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Actual Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xfhm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "projected_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Projected Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-28 13:50:15.116082",
|
||||
"modified": "2023-05-22 17:52:34.708879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
Loading…
x
Reference in New Issue
Block a user