Add support for subcontract return from rejected warehouse (backport #50299) (#50330)

* Merge pull request #50299 from saadchaudharry/feature-subcontract-return-rejected

Add support for subcontract return from rejected warehouse

(cherry picked from commit 8854db51dd)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py

* chore: resolve conflicts

---------

Co-authored-by: Saad Chaudhary <47004596+saadchaudharry@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
This commit is contained in:
mergify[bot]
2025-11-04 09:09:41 +00:00
committed by GitHub
parent eed1abd272
commit ac901dee3c
4 changed files with 190 additions and 3 deletions

View File

@@ -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

View File

@@ -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")
);

View File

@@ -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

View File

@@ -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