diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5c0f78ac986..29f7d5810b3 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -480,6 +480,13 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.subcontracting_order_item = source_doc.subcontracting_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.subcontracting_receipt_item = source_doc.name + if return_against_rejected_qty: + target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.rejected_qty = 0.0 + target_doc.rejected_warehouse = "" + target_doc.warehouse = source_doc.rejected_warehouse + target_doc.received_qty = target_doc.qty + target_doc.return_qty_from_rejected_warehouse = 1 else: target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index fee1cac2542..4e502793068 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -82,10 +82,53 @@ frappe.ui.form.on("Subcontracting Receipt", { frm.add_custom_button( __("Subcontract Return"), () => { - frappe.model.open_mapped_doc({ - method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return", - frm: frm, + const make_standard_return = () => { + frappe.model.open_mapped_doc({ + method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return", + frm: frm, + }); + }; + + let has_rejected_items = frm.doc.items.filter((item) => { + if (item.rejected_qty > 0) { + return true; + } }); + + if (has_rejected_items && has_rejected_items.length > 0) { + frappe.prompt( + [ + { + label: __("Return Qty from Rejected Warehouse"), + fieldtype: "Check", + fieldname: "return_for_rejected_warehouse", + default: 1, + }, + ], + function (values) { + if (values.return_for_rejected_warehouse) { + frappe.call({ + method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return_against_rejected_warehouse", + args: { + source_name: frm.doc.name, + }, + callback: function (r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + } else { + make_standard_return(); + } + }, + __("Return Qty"), + __("Make Return Entry") + ); + } else { + make_standard_return(); + } }, __("Create") ); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 99a6abc8b91..d2ceb90ad52 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -774,6 +774,13 @@ class SubcontractingReceipt(SubcontractingController): make_purchase_receipt(self, save=True, notify=True) +@frappe.whitelist() +def make_subcontract_return_against_rejected_warehouse(source_name): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True) + + @frappe.whitelist() def make_subcontract_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index d5e30bd9368..b9d062af5b2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1196,6 +1196,136 @@ class TestSubcontractingReceipt(FrappeTestCase): scr.cancel() self.assertTrue(scr.docstatus == 2) + def test_subcontract_return_from_rejected_warehouse(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import ( + make_subcontract_return_against_rejected_warehouse, + ) + + # Create subcontracted item + fg_item = make_item( + "_Test Subcontract Item Return from Rejected Warehouse", + properties={ + "is_stock_item": 1, + "is_sub_contracted_item": 1, + }, + ).name + + # Create service item + service_item = make_item( + "_Test Service Item Return from Rejected Warehouse", properties={"is_stock_item": 0} + ).name + + # Create BOM for the subcontracted item with required raw materials + rm_item1 = make_item( + "_Test RM Item 1 Return from Rejected Warehouse", properties={"is_stock_item": 1} + ).name + + rm_item2 = make_item( + "_Test RM Item 2 Return from Rejected Warehouse", properties={"is_stock_item": 1} + ).name + + make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]) + + # Create warehouses + rejected_warehouse = create_warehouse("_Test Subcontract Rejected Warehouse Return Qty Warehouse") + + # Create service items for subcontracting order + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": service_item, + "qty": 10, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 10, + }, + ] + + # Create Subcontracting Order + sco = get_subcontracting_order(service_items=service_items) + + # Stock raw materials + make_stock_entry(item_code=rm_item1, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) + make_stock_entry(item_code=rm_item2, qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) + + # Transfer raw materials + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Step 1: Create Subcontracting Receipt with rejected quantity + sr = make_subcontracting_receipt(sco.name) + sr.items[0].qty = 8 # Accepted quantity + sr.items[0].rejected_qty = 2 + sr.items[0].rejected_warehouse = rejected_warehouse + sr.save() + sr.submit() + + # Verify initial state + sr.reload() + self.assertEqual(sr.items[0].qty, 8) + self.assertEqual(sr.items[0].rejected_qty, 2) + self.assertEqual(sr.items[0].rejected_warehouse, rejected_warehouse) + + # Step 2: Create Subcontract Return from Rejected Warehouse + sr_return = make_subcontract_return_against_rejected_warehouse(sr.name) + + # Verify the return document properties + self.assertEqual(sr_return.doctype, "Subcontracting Receipt") + self.assertEqual(sr_return.is_return, 1) + self.assertEqual(sr_return.return_against, sr.name) + + # Verify item details in return document + self.assertEqual(len(sr_return.items), 1) + self.assertEqual(sr_return.items[0].item_code, fg_item) + self.assertEqual(sr_return.items[0].warehouse, rejected_warehouse) + self.assertEqual(sr_return.items[0].qty, -2.0) # Negative for return + self.assertEqual(sr_return.items[0].rejected_qty, 0.0) + self.assertEqual(sr_return.items[0].rejected_warehouse, "") + + # Check specific fields that should be set for subcontracting returns + self.assertEqual(sr_return.items[0].subcontracting_order, sco.name) + self.assertEqual(sr_return.items[0].subcontracting_order_item, sr.items[0].subcontracting_order_item) + self.assertEqual(sr_return.items[0].return_qty_from_rejected_warehouse, 1) + + # For returns from rejected warehouse, supplied_items might be empty initially + # They might get populated when the document is saved/submitted + # Or they might not be needed since we're returning finished goods + + # Save and submit the return + sr_return.save() + sr_return.submit() + + # Verify final state + sr_return.reload() + self.assertEqual(sr_return.docstatus, 1) + self.assertEqual(sr_return.status, "Return") + + # Verify stock ledger entries for the return + sle = frappe.get_all( + "Stock Ledger Entry", + filters={ + "voucher_type": "Subcontracting Receipt", + "voucher_no": sr_return.name, + "warehouse": rejected_warehouse, + }, + fields=["item_code", "actual_qty", "warehouse"], + ) + + self.assertEqual(len(sle), 1) + self.assertEqual(sle[0].item_code, fg_item) + self.assertEqual(sle[0].actual_qty, -2.0) # Outward entry from rejected warehouse + self.assertEqual(sle[0].warehouse, rejected_warehouse) + + # Verify that the original document's rejected quantity is not affected + sr.reload() + self.assertEqual(sr.items[0].rejected_qty, 2) # Should remain the same + @change_settings("Buying Settings", {"auto_create_purchase_receipt": 1}) def test_auto_create_purchase_receipt(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order