Skip to content

Commit

Permalink
feat: reserved production plan sub assembly items
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitwaghchaure committed Nov 6, 2023
1 parent a9372c4 commit f120083
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
44 changes: 40 additions & 4 deletions erpnext/manufacturing/doctype/production_plan/production_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,12 @@ def update_bin_qty(self):
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)}
Expand Down Expand Up @@ -809,7 +815,11 @@ def get_sub_assembly_items(self, manufacturing_type=None):

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)
Expand All @@ -831,7 +841,7 @@ def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_typ
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"
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -1042,13 +1042,14 @@ def test_resered_qty_for_production_plan_for_material_requests(self):
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):
Expand Down Expand Up @@ -1359,6 +1360,93 @@ def test_mr_qty_for_same_rm_with_different_sub_assemblies(self):
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):
"""
Expand All @@ -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,
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -81,7 +73,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty"
"label": "Received Qty",
"read_only": 1
},
{
"fieldname": "bom_no",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
36 changes: 35 additions & 1 deletion erpnext/manufacturing/doctype/work_order/work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ def update_work_order_qty(self):
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):
Expand Down Expand Up @@ -569,16 +570,49 @@ def validate_cancel(self):
)

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
Expand Down
30 changes: 29 additions & 1 deletion erpnext/stock/doctype/bin/bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@ def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False)
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),
Expand All @@ -48,6 +53,29 @@ def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False)
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"""
Expand Down

0 comments on commit f120083

Please sign in to comment.