feat: reserved production plan sub assembly items (#37884)
This commit is contained in:
parent
e5a018f84c
commit
34d3eb88b3
@ -36,6 +36,7 @@
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"combine_sub_items",
|
||||
"sub_assembly_warehouse",
|
||||
"section_break_ucc4",
|
||||
"skip_available_sub_assembly_item",
|
||||
"column_break_igxl",
|
||||
@ -416,13 +417,19 @@
|
||||
{
|
||||
"fieldname": "column_break_igxl",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sub Assembly Warehouse",
|
||||
"options": "Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-29 11:41:03.246059",
|
||||
"modified": "2023-11-03 14:08:11.928027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
@ -490,6 +490,12 @@ class ProductionPlan(Document):
|
||||
bin = frappe.get_doc("Bin", bin_name, for_update=True)
|
||||
bin.update_reserved_qty_for_production_plan()
|
||||
|
||||
for d in self.sub_assembly_items:
|
||||
if d.fg_warehouse and d.type_of_manufacturing == "In House":
|
||||
bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
|
||||
bin = frappe.get_doc("Bin", bin_name, for_update=True)
|
||||
bin.update_reserved_qty_for_for_sub_assembly()
|
||||
|
||||
def delete_draft_work_order(self):
|
||||
for d in frappe.get_all(
|
||||
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
|
||||
@ -809,7 +815,11 @@ class ProductionPlan(Document):
|
||||
|
||||
bom_data = []
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
warehouse = (
|
||||
(self.sub_assembly_warehouse or 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)
|
||||
@ -831,7 +841,7 @@ class ProductionPlan(Document):
|
||||
for data in bom_data:
|
||||
data.qty = data.stock_qty
|
||||
data.production_plan_item = row.name
|
||||
data.fg_warehouse = row.warehouse
|
||||
data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
|
||||
data.schedule_date = row.planned_start_date
|
||||
data.type_of_manufacturing = manufacturing_type or (
|
||||
"Subcontract" if data.is_sub_contracted_item else "In House"
|
||||
@ -1637,8 +1647,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
|
||||
query = query.run()
|
||||
|
||||
if not query:
|
||||
return 0.0
|
||||
if not query or query[0][0] is None:
|
||||
return None
|
||||
|
||||
reserved_qty_for_production_plan = flt(query[0][0])
|
||||
|
||||
@ -1780,3 +1790,29 @@ def sales_order_query(
|
||||
query = query.offset(start)
|
||||
|
||||
return query.run()
|
||||
|
||||
|
||||
def get_reserved_qty_for_sub_assembly(item_code, warehouse):
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Production Plan Sub Assembly Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.production_item == item_code)
|
||||
& (child.fg_warehouse == warehouse)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
)
|
||||
)
|
||||
|
||||
query = query.run()
|
||||
|
||||
if not query or query[0][0] is None:
|
||||
return None
|
||||
|
||||
qty = flt(query[0][0])
|
||||
return qty if qty > 0 else 0.0
|
||||
|
@ -1042,13 +1042,14 @@ class TestProductionPlan(FrappeTestCase):
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty - before_qty, 1)
|
||||
|
||||
pln = frappe.get_doc("Production Plan", pln.name)
|
||||
pln.cancel()
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln.reload()
|
||||
self.assertEqual(pln.docstatus, 2)
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_work_order(self):
|
||||
@ -1359,6 +1360,93 @@ class TestProductionPlan(FrappeTestCase):
|
||||
if row.item_code == "ChildPart2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 2)
|
||||
|
||||
def test_reserve_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
bom_tree = {
|
||||
"Fininshed Goods Bicycle": {
|
||||
"Frame Assembly": {"Frame": {}},
|
||||
"Chain Assembly": {"Chain": {}},
|
||||
}
|
||||
}
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = "_Test Company"
|
||||
|
||||
sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
|
||||
|
||||
for item_code in ["Frame", "Chain"]:
|
||||
make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
|
||||
|
||||
before_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=2,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse=warehouse,
|
||||
sub_assembly_warehouse=sub_assembly_warehouse,
|
||||
)
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
plan.submit()
|
||||
|
||||
after_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(after_qty, before_qty + 2)
|
||||
|
||||
plan.make_work_order()
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
fields=["name", "production_item"],
|
||||
filters={"production_plan": plan.name},
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
for d in work_orders:
|
||||
wo_doc = frappe.get_doc("Work Order", d.name)
|
||||
wo_doc.skip_transfer = 1
|
||||
wo_doc.from_wip_warehouse = 1
|
||||
|
||||
wo_doc.wip_warehouse = (
|
||||
warehouse
|
||||
if d.production_item in ["Frame Assembly", "Chain Assembly"]
|
||||
else sub_assembly_warehouse
|
||||
)
|
||||
|
||||
wo_doc.submit()
|
||||
|
||||
if d.production_item == "Frame Assembly":
|
||||
self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
|
||||
se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
|
||||
se_doc.submit()
|
||||
|
||||
after_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
|
||||
"reserved_qty_for_production_plan",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
@ -1379,6 +1467,7 @@ def create_production_plan(**args):
|
||||
"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,
|
||||
"sub_assembly_warehouse": args.sub_assembly_warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -17,11 +17,10 @@
|
||||
"type_of_manufacturing",
|
||||
"supplier",
|
||||
"work_order_details_section",
|
||||
"work_order",
|
||||
"wo_produced_qty",
|
||||
"purchase_order",
|
||||
"production_plan_item",
|
||||
"column_break_7",
|
||||
"produced_qty",
|
||||
"received_qty",
|
||||
"indent",
|
||||
"section_break_19",
|
||||
@ -52,13 +51,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "work_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work Order",
|
||||
"options": "Work Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
@ -81,7 +73,8 @@
|
||||
{
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty"
|
||||
"label": "Received Qty",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_no",
|
||||
@ -161,12 +154,6 @@
|
||||
"label": "Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Data",
|
||||
"label": "Produced Quantity",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "In House",
|
||||
"fieldname": "type_of_manufacturing",
|
||||
@ -209,12 +196,18 @@
|
||||
"label": "Projected Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "wo_produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Produced Qty",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-22 17:52:34.708879",
|
||||
"modified": "2023-11-03 13:33:42.959387",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
@ -293,6 +293,7 @@ class WorkOrder(Document):
|
||||
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
|
||||
|
||||
if self.production_plan:
|
||||
self.set_produced_qty_for_sub_assembly_item()
|
||||
self.update_production_plan_status()
|
||||
|
||||
def get_transferred_or_manufactured_qty(self, purpose):
|
||||
@ -569,16 +570,49 @@ class WorkOrder(Document):
|
||||
)
|
||||
|
||||
def update_planned_qty(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_reserved_qty_for_sub_assembly,
|
||||
)
|
||||
|
||||
qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
|
||||
|
||||
if self.production_plan_sub_assembly_item and self.production_plan:
|
||||
qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
|
||||
self.production_item, self.fg_warehouse
|
||||
)
|
||||
|
||||
update_bin_qty(
|
||||
self.production_item,
|
||||
self.fg_warehouse,
|
||||
{"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
|
||||
qty_dict,
|
||||
)
|
||||
|
||||
if self.material_request:
|
||||
mr_obj = frappe.get_doc("Material Request", self.material_request)
|
||||
mr_obj.update_requested_qty([self.material_request_item])
|
||||
|
||||
def set_produced_qty_for_sub_assembly_item(self):
|
||||
table = frappe.qb.DocType("Work Order")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.produced_qty))
|
||||
.where(
|
||||
(table.production_plan == self.production_plan)
|
||||
& (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
|
||||
& (table.docstatus == 1)
|
||||
)
|
||||
).run()
|
||||
|
||||
produced_qty = flt(query[0][0]) if query else 0
|
||||
|
||||
frappe.db.set_value(
|
||||
"Production Plan Sub Assembly Item",
|
||||
self.production_plan_sub_assembly_item,
|
||||
"wo_produced_qty",
|
||||
produced_qty,
|
||||
)
|
||||
|
||||
def update_ordered_qty(self):
|
||||
if (
|
||||
self.production_plan
|
||||
|
@ -34,10 +34,15 @@ class Bin(Document):
|
||||
get_reserved_qty_for_production_plan,
|
||||
)
|
||||
|
||||
self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
|
||||
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
|
||||
return
|
||||
|
||||
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
|
||||
|
||||
self.db_set(
|
||||
"reserved_qty_for_production_plan",
|
||||
flt(self.reserved_qty_for_production_plan),
|
||||
@ -48,6 +53,29 @@ class Bin(Document):
|
||||
self.set_projected_qty()
|
||||
self.db_set("projected_qty", self.projected_qty, update_modified=True)
|
||||
|
||||
def update_reserved_qty_for_for_sub_assembly(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_reserved_qty_for_sub_assembly,
|
||||
)
|
||||
|
||||
reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
|
||||
self.item_code, self.warehouse
|
||||
)
|
||||
|
||||
if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
|
||||
return
|
||||
|
||||
self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
|
||||
self.set_projected_qty()
|
||||
|
||||
self.db_set(
|
||||
{
|
||||
"projected_qty": self.projected_qty,
|
||||
"reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
|
||||
},
|
||||
update_modified=True,
|
||||
)
|
||||
|
||||
def update_reserved_qty_for_production(self):
|
||||
"""Update qty reserved for production from Production Item tables
|
||||
in open work orders"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user