feat: Disassembly Order (backport #42655) (#42957)

* feat: Disassembly Order (#42655)

(cherry picked from commit 663a08e4cd)

# Conflicts:
#	erpnext/stock/doctype/stock_entry_type/stock_entry_type.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
mergify[bot]
2024-08-27 23:07:37 +05:30
committed by GitHub
parent 3a5f19853a
commit 8d8dd0cd2b
11 changed files with 212 additions and 20 deletions

View File

@@ -2053,6 +2053,55 @@ class TestWorkOrder(FrappeTestCase):
"BOM",
)
def test_disassemby_order(self):
fg_item = "Test Disassembly Item"
source_warehouse = "Stores - _TC"
raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"]
make_item(fg_item, {"is_stock_item": 1})
for item in raw_materials:
make_item(item, {"is_stock_item": 1})
test_stock_entry.make_stock_entry(
item_code=item,
target=source_warehouse,
qty=1,
basic_rate=100,
)
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
wo = make_wo_order_test_record(
item=fg_item,
qty=1,
source_warehouse=source_warehouse,
skip_transfer=1,
)
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
for row in stock_entry.items:
if row.item_code in raw_materials:
row.s_warehouse = source_warehouse
stock_entry.submit()
wo.reload()
self.assertEqual(wo.status, "Completed")
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1))
stock_entry.save()
self.assertEqual(stock_entry.purpose, "Disassemble")
for row in stock_entry.items:
if row.item_code == fg_item:
self.assertTrue(row.s_warehouse)
self.assertFalse(row.t_warehouse)
else:
self.assertFalse(row.s_warehouse)
self.assertTrue(row.t_warehouse)
stock_entry.submit()
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
@@ -2370,6 +2419,7 @@ def make_wo_order_test_record(**args):
wo_order.batch_size = args.batch_size or 0
if args.source_warehouse:
wo_order.source_warehouse = args.source_warehouse
for item in wo_order.get("required_items"):
item.source_warehouse = args.source_warehouse

View File

@@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", {
}
}
if (frm.doc.status == "Completed") {
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
frm.add_custom_button(
__("BOM"),
() => {
frm.trigger("make_bom");
},
__("Create")
);
}
}
if (
frm.doc.status == "Completed" &&
frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture"
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
) {
frm.add_custom_button(__("Create BOM"), () => {
frm.trigger("make_bom");
});
frm.add_custom_button(
__("Disassembly Order"),
() => {
frm.trigger("make_disassembly_order");
},
__("Create")
);
}
frm.trigger("add_custom_button_to_return_components");
@@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", {
});
},
make_disassembly_order(frm) {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.then((data) => {
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
},
show_progress_for_items: function (frm) {
var bars = [];
var message = "";
@@ -745,6 +779,10 @@ erpnext.work_order = {
get_max_transferable_qty: (frm, purpose) => {
let max = 0;
if (purpose === "Disassemble") {
return flt(frm.doc.produced_qty);
}
if (frm.doc.skip_transfer) {
max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
} else {
@@ -759,15 +797,38 @@ erpnext.work_order = {
show_prompt_for_qty_input: function (frm, purpose) {
let max = this.get_max_transferable_qty(frm, purpose);
let fields = [
{
fieldtype: "Float",
label: __("Qty for {0}", [__(purpose)]),
fieldname: "qty",
description: __("Max: {0}", [max]),
default: max,
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frappe.prompt(
{
fieldtype: "Float",
label: __("Qty for {0}", [__(purpose)]),
fieldname: "qty",
description: __("Max: {0}", [max]),
default: max,
},
fields,
(data) => {
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;

View File

@@ -1359,7 +1359,7 @@ def set_work_order_ops(name):
@frappe.whitelist()
def make_stock_entry(work_order_id, purpose, qty=None):
def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
work_order = frappe.get_doc("Work Order", work_order_id)
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
wip_warehouse = work_order.wip_warehouse
@@ -1389,9 +1389,16 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.to_warehouse = work_order.fg_warehouse
stock_entry.project = work_order.project
if purpose == "Disassemble":
stock_entry.from_warehouse = work_order.fg_warehouse
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()