feat: Disassembly Order (#42655)

This commit is contained in:
rohitwaghchaure
2024-08-27 22:25:20 +05:30
committed by GitHub
parent 758d1606a9
commit 663a08e4cd
11 changed files with 212 additions and 20 deletions

View File

@@ -2054,6 +2054,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)
@@ -2371,6 +2420,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

@@ -177,13 +177,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");
@@ -345,6 +362,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 = "";
@@ -756,6 +790,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 {
@@ -770,15 +808,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

@@ -1398,7 +1398,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
@@ -1428,9 +1428,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()

View File

@@ -376,4 +376,5 @@ erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
erpnext.patches.v15_0.do_not_use_batchwise_valuation
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type

View File

@@ -0,0 +1,13 @@
import frappe
def execute():
if not frappe.db.exists("Stock Entry Type", "Disassemble"):
frappe.get_doc(
{
"doctype": "Stock Entry Type",
"name": "Disassemble",
"purpose": "Disassemble",
"is_standard": 1,
}
).insert(ignore_permissions=True)

View File

@@ -11,6 +11,7 @@ def execute():
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]:
if frappe.db.exists("Stock Entry Type", stock_entry_type):
frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1)

View File

@@ -103,6 +103,7 @@ def install(country=None):
"purpose": "Repack",
"is_standard": 1,
},
{"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1},
{
"doctype": "Stock Entry Type",
"name": _("Send to Subcontractor"),

View File

@@ -127,7 +127,7 @@
"label": "Purpose",
"oldfieldname": "purpose",
"oldfieldtype": "Select",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
"read_only": 1,
"search_index": 1
},
@@ -143,7 +143,7 @@
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
@@ -242,7 +242,7 @@
},
{
"default": "0",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
"fieldname": "from_bom",
"fieldtype": "Check",
"label": "From BOM",
@@ -697,7 +697,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-13 19:02:42.386955",
"modified": "2024-08-13 19:05:42.386955",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@@ -132,6 +132,7 @@ class StockEntry(StockController):
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]
remarks: DF.Text | None
sales_invoice_no: DF.Link | None
@@ -354,6 +355,7 @@ class StockEntry(StockController):
"Repack",
"Send to Subcontractor",
"Material Consumption for Manufacture",
"Disassemble",
]
if self.purpose not in valid_purposes:
@@ -631,6 +633,7 @@ class StockEntry(StockController):
"Manufacture",
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
"Disassemble",
):
# check if work order is entered
@@ -1726,11 +1729,63 @@ class StockEntry(StockController):
},
)
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
items = self.get_items_from_manufacture_entry()
s_warehouse = ""
if self.work_order:
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
for row in items:
child_row = self.append("items", {})
for field, value in row.items():
if value is not None:
child_row.set(field, value)
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.is_finished_item = 0 if row.is_finished_item else 1
def get_items_from_manufacture_entry(self):
return frappe.get_all(
"Stock Entry",
fields=[
"`tabStock Entry Detail`.`item_code`",
"`tabStock Entry Detail`.`item_name`",
"`tabStock Entry Detail`.`description`",
"`tabStock Entry Detail`.`qty`",
"`tabStock Entry Detail`.`transfer_qty`",
"`tabStock Entry Detail`.`stock_uom`",
"`tabStock Entry Detail`.`uom`",
"`tabStock Entry Detail`.`basic_rate`",
"`tabStock Entry Detail`.`conversion_factor`",
"`tabStock Entry Detail`.`is_finished_item`",
"`tabStock Entry Detail`.`batch_no`",
"`tabStock Entry Detail`.`serial_no`",
"`tabStock Entry Detail`.`use_serial_batch_fields`",
],
filters=[
["Stock Entry", "purpose", "=", "Manufacture"],
["Stock Entry", "work_order", "=", self.work_order],
["Stock Entry", "docstatus", "=", 1],
["Stock Entry Detail", "docstatus", "=", 1],
],
order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc",
)
@frappe.whitelist()
def get_items(self):
self.set("items", [])
self.validate_work_order()
if self.purpose == "Disassemble":
return self.get_items_for_disassembly()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))

View File

@@ -17,7 +17,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Purpose",
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
"reqd": 1,
"set_only_once": 1
},
@@ -37,10 +37,11 @@
}
],
"links": [],
"modified": "2024-08-22 16:00:22.696958",
"modified": "2024-08-23 16:00:22.696958",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Type",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{

View File

@@ -30,6 +30,7 @@ class StockEntryType(Document):
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]
# end: auto-generated types
@@ -48,6 +49,7 @@ class StockEntryType(Document):
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]:
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")