Skip to content

Commit

Permalink
feat: stock reservation for Work Order
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitwaghchaure committed Jan 2, 2025
1 parent bdece96 commit 1e606ec
Show file tree
Hide file tree
Showing 20 changed files with 1,311 additions and 70 deletions.
182 changes: 182 additions & 0 deletions erpnext/manufacturing/doctype/work_order/test_work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,187 @@ def test_components_as_per_bom_for_manufacture_entry(self):

frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)

def test_stock_reservation_for_serialized_raw_material(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
make_stock_entry as make_stock_entry_test_record,
)

production_item = "Test Stock Reservation FG 1"
rm_item = "Test Stock Reservation RM 1"
source_warehouse = "Stores - _TC"

make_item(production_item, {"is_stock_item": 1})
make_item(rm_item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TST-SER-RES-.###"})

bom = make_bom(
item=production_item,
source_warehouse=source_warehouse,
raw_materials=[rm_item],
operating_cost_per_bom_quantity=100,
do_not_submit=True,
)

for row in bom.exploded_items:
make_stock_entry_test_record(
item_code=row.item_code,
target=source_warehouse,
qty=10,
basic_rate=100,
)

wo = make_wo_order_test_record(
item=production_item,
qty=10,
reserve_stock=1,
source_warehouse=source_warehouse,
)

self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))

wo1 = make_wo_order_test_record(
item=production_item,
qty=10,
reserve_stock=1,
source_warehouse=source_warehouse,
)

self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name}))

transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10))
transfer_entry.save()

self.assertRaises(frappe.ValidationError, transfer_entry.submit)

def test_stock_reservation_for_batched_raw_material(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
make_stock_entry as make_stock_entry_test_record,
)

production_item = "Test Stock Reservation FG 2"
rm_item = "Test Stock Reservation RM 2"
source_warehouse = "Stores - _TC"

make_item(production_item, {"is_stock_item": 1})
make_item(
rm_item,
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "TST-BATCH-RES-.###",
"create_new_batch": 1,
},
)

bom = make_bom(
item=production_item,
source_warehouse=source_warehouse,
raw_materials=[rm_item],
operating_cost_per_bom_quantity=100,
do_not_submit=True,
)

for row in bom.exploded_items:
make_stock_entry_test_record(
item_code=row.item_code,
target=source_warehouse,
qty=10,
basic_rate=100,
)

wo = make_wo_order_test_record(
item=production_item,
qty=10,
reserve_stock=1,
source_warehouse=source_warehouse,
)

self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))

wo1 = make_wo_order_test_record(
item=production_item,
qty=10,
reserve_stock=1,
source_warehouse=source_warehouse,
)

self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name}))

transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10))
transfer_entry.save()

self.assertRaises(frappe.ValidationError, transfer_entry.submit)

def test_auto_stock_reservation_for_batched_raw_material(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import (
make_stock_entry as make_stock_entry_test_record,
)

frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1)

production_item = "Test Stock Reservation FG 3"
rm_item = "Test Stock Reservation RM 3"
source_warehouse = "Stores - _TC"

make_item(production_item, {"is_stock_item": 1})
make_item(
rm_item,
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "TST-BATCH-RES-.###",
"create_new_batch": 1,
},
)

bom = make_bom(
item=production_item,
source_warehouse=source_warehouse,
raw_materials=[rm_item],
operating_cost_per_bom_quantity=100,
do_not_submit=True,
)

itemwise_batches = frappe._dict()
for row in bom.exploded_items:
se = make_stock_entry_test_record(
item_code=row.item_code,
target=source_warehouse,
qty=10,
basic_rate=100,
)

itemwise_batches[row.item_code] = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)

wo = make_wo_order_test_record(
item=production_item,
qty=10,
reserve_stock=1,
source_warehouse=source_warehouse,
)

self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}))

for row in frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}):
reservation_entry = frappe.get_doc("Stock Reservation Entry", row.name)
self.assertTrue(reservation_entry.has_batch_no)
self.assertTrue(reservation_entry.sb_entries)

for row in bom.exploded_items:
make_stock_entry_test_record(
item_code=row.item_code,
target=source_warehouse,
qty=10,
basic_rate=100,
)

transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
transfer_entry.save()
transfer_entry.submit()

for row in transfer_entry.items:
batch_no = get_batch_from_bundle(row.serial_and_batch_bundle)
self.assertEqual(batch_no, itemwise_batches[row.item_code])


def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
Expand Down Expand Up @@ -2792,6 +2973,7 @@ def make_wo_order_test_record(**args):
"BOM", {"item": wo_order.production_item, "is_active": 1, "is_default": 1}
)
wo_order.qty = args.qty or 10
wo_order.reserve_stock = args.reserve_stock or 0
wo_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC"
wo_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC"
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
Expand Down
54 changes: 54 additions & 0 deletions erpnext/manufacturing/doctype/work_order/work_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,20 @@ frappe.ui.form.on("Work Order", {
frm.trigger("add_custom_button_to_return_components");
},

has_unreserved_stock(frm) {
let has_unreserved_stock = frm.doc.required_items.some(
(item) => flt(item.required_qty) > flt(item.stock_reserved_qty)
);

return has_unreserved_stock;
},

has_reserved_stock(frm) {
let has_reserved_stock = frm.doc.required_items.some((item) => flt(item.stock_reserved_qty) > 0);

return has_reserved_stock;
},

add_custom_button_to_return_components: function (frm) {
if (frm.doc.docstatus === 1 && ["Closed", "Completed"].includes(frm.doc.status)) {
let non_consumed_items = frm.doc.required_items.filter((d) => {
Expand Down Expand Up @@ -545,6 +559,12 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order.calculate_cost(frm.doc);
erpnext.work_order.calculate_total_cost(frm);
},

on_submit() {
frappe.route_hooks.after_submit = (frm) => {
frm.reload_doc();
};
},
});

frappe.ui.form.on("Work Order Item", {
Expand Down Expand Up @@ -657,6 +677,8 @@ erpnext.work_order = {
);
}

erpnext.work_order.setup_stock_reservation(frm);

if (!frm.doc.track_semi_finished_goods) {
const show_start_btn =
frm.doc.skip_transfer || frm.doc.transfer_material_against == "Job Card" ? 0 : 1;
Expand Down Expand Up @@ -750,6 +772,38 @@ erpnext.work_order = {
}
}
},

setup_stock_reservation(frm) {
if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) {
if (
frm.events.has_unreserved_stock(frm) &&
(frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty)
) {
frm.add_custom_button(
__("Reserve"),
() => erpnext.stock_reservation.make_entries(frm, "required_items"),
__("Stock Reservation")
);
}

if (frm.events.has_reserved_stock(frm)) {
if (frm.doc.skip_transfer || frm.doc.material_transferred_for_manufacturing < frm.doc.qty) {
frm.add_custom_button(
__("Unreserve"),
() => erpnext.stock_reservation.unreserve_stock(frm),
__("Stock Reservation")
);
}

frm.add_custom_button(
__("Reserved Stock"),
() => erpnext.stock_reservation.show_reserved_stock(frm, "required_items"),
__("Stock Reservation")
);
}
}
},

calculate_cost: function (doc) {
if (doc.operations) {
var op = doc.operations;
Expand Down
14 changes: 11 additions & 3 deletions erpnext/manufacturing/doctype/work_order/work_order.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"process_loss_qty",
"project",
"track_semi_finished_goods",
"reserve_stock",
"warehouses",
"source_warehouse",
"wip_warehouse",
Expand Down Expand Up @@ -102,7 +103,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nClosed\nCancelled",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nStock Reserved\nStock Partially Reserved\nCompleted\nStopped\nClosed\nCancelled",
"read_only": 1,
"reqd": 1,
"search_index": 1
Expand Down Expand Up @@ -214,7 +215,6 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "sales_order",
"fieldtype": "Link",
"in_global_search": 1,
Expand Down Expand Up @@ -322,6 +322,8 @@
"label": "Expected Delivery Date"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:!doc.operations",
"fieldname": "operations_section",
"fieldtype": "Section Break",
"label": "Operations",
Expand Down Expand Up @@ -584,14 +586,20 @@
"fieldtype": "Check",
"label": "Track Semi Finished Goods",
"read_only": 1
},
{
"default": "0",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": " Reserve Stock"
}
],
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:13:00.129434",
"modified": "2024-09-23 16:56:00.483027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
Expand Down
Loading

0 comments on commit 1e606ec

Please sign in to comment.