diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py
new file mode 100644
index 00000000000..8d1f078db1f
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.model.mapper import get_mapped_doc
+from frappe.utils import flt, get_link_to_form
+
+
+@frappe.whitelist()
+def make_subcontract_return_against_rejected_warehouse(source_name: str):
+ 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: str, target_doc: str | dict | Document | None = None):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ return make_return_doc("Subcontracting Receipt", source_name, target_doc)
+
+
+@frappe.whitelist(methods=["POST"])
+def make_purchase_receipt(
+ source_name: Document | str,
+ target_doc: str | dict | Document | None = None,
+ save: bool = False,
+ submit: bool = False,
+ notify: bool = False,
+):
+ if isinstance(source_name, str):
+ source_doc = frappe.get_doc("Subcontracting Receipt", source_name)
+ else:
+ source_doc = source_name
+
+ if source_doc.is_return:
+ return
+
+ po_sr_item_dict = {}
+ po_name = None
+ for item in source_doc.items:
+ if not item.purchase_order:
+ continue
+
+ if not po_name:
+ po_name = item.purchase_order
+
+ po_sr_item_dict[item.purchase_order_item] = {
+ "qty": flt(item.qty),
+ "rejected_qty": flt(item.rejected_qty),
+ "warehouse": item.warehouse,
+ "rejected_warehouse": item.rejected_warehouse,
+ "subcontracting_receipt_item": item.name,
+ }
+
+ if not po_name:
+ frappe.throw(
+ _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format(
+ source_doc.name
+ )
+ )
+
+ def update_item(obj, target, source_parent):
+ sr_item_details = po_sr_item_dict.get(obj.name)
+ ratio = flt(obj.qty) / flt(obj.fg_item_qty)
+
+ target.update(
+ {
+ "qty": ratio * sr_item_details["qty"],
+ "rejected_qty": ratio * sr_item_details["rejected_qty"],
+ "warehouse": sr_item_details["warehouse"],
+ "rejected_warehouse": sr_item_details["rejected_warehouse"],
+ "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"],
+ }
+ )
+
+ def post_process(source, target):
+ target.set_missing_values()
+ target.update(
+ {
+ "posting_date": source_doc.posting_date,
+ "posting_time": source_doc.posting_time,
+ "subcontracting_receipt": source_doc.name,
+ "supplier_warehouse": source_doc.supplier_warehouse,
+ "is_subcontracted": 1,
+ "currency": frappe.get_cached_value("Company", target.company, "default_currency"),
+ }
+ )
+
+ target_doc = get_mapped_doc(
+ "Purchase Order",
+ po_name,
+ {
+ "Purchase Order": {
+ "doctype": "Purchase Receipt",
+ "field_map": {"supplier_warehouse": "supplier_warehouse"},
+ "validation": {
+ "docstatus": ["=", 1],
+ },
+ },
+ "Purchase Order Item": {
+ "doctype": "Purchase Receipt Item",
+ "field_map": {
+ "name": "purchase_order_item",
+ "parent": "purchase_order",
+ "bom": "bom",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: doc.name in po_sr_item_dict,
+ },
+ "Purchase Taxes and Charges": {
+ "doctype": "Purchase Taxes and Charges",
+ "reset_value": True,
+ # for POs created in earlier version with tax_withholding_row
+ "condition": lambda doc: not doc.is_tax_withholding_account,
+ },
+ },
+ postprocess=post_process,
+ )
+
+ if not target_doc.get("items"):
+ add_po_items_to_pr(source_doc, target_doc)
+
+ if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
+ target_doc.save()
+
+ if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
+ frappe.db.savepoint("submit_subcontracting_receipt")
+ try:
+ target_doc.submit()
+ except Exception as e:
+ frappe.db.rollback(save_point="submit_subcontracting_receipt")
+ target_doc.add_comment("Comment", _("Submit Action Failed") + "
" + str(e))
+
+ if notify:
+ frappe.msgprint(
+ _("Purchase Receipt {0} created.").format(
+ get_link_to_form(target_doc.doctype, target_doc.name)
+ ),
+ indicator="green",
+ alert=True,
+ )
+
+ return target_doc
+
+
+def add_po_items_to_pr(scr_doc, target_doc):
+ fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
+
+ for (item_code, po_name), fg_qty in fg_items.items():
+ po_doc = frappe.get_doc("Purchase Order", po_name)
+ for item in po_doc.items:
+ if item.fg_item != item_code:
+ continue
+
+ qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
+ if qty:
+ target_doc.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "item_name": item.item_name,
+ "description": item.description,
+ "qty": qty,
+ "rate": item.rate,
+ "warehouse": item.warehouse,
+ "purchase_order": item.parent,
+ "purchase_order_item": item.name,
+ },
+ )
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index ffd6a160def..3dfa6188b0b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -1592,6 +1592,73 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
+ @ERPNextTestSuite.change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
+ def test_auto_create_purchase_receipt_with_tax_withholding_row(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+
+ fg_item = "Subcontracted Item SA1"
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 5,
+ },
+ ]
+
+ po = create_purchase_order(
+ rm_items=service_items,
+ is_subcontracted=1,
+ supplier_warehouse="_Test Warehouse 1 - _TC",
+ do_not_submit=True,
+ )
+ # withheld against the full PO value, and not recomputed on a partial receipt
+ po.append(
+ "taxes",
+ {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "Actual",
+ "add_deduct_tax": "Deduct",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "TDS on Contract",
+ "doctype": "Purchase Taxes and Charges",
+ "tax_amount": 800,
+ "is_tax_withholding_account": 1,
+ },
+ )
+ po.save()
+ po.submit()
+ self.assertEqual(po.grand_total, 200)
+
+ sco = get_subcontracting_order(po_name=po.name)
+
+ 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),
+ )
+
+ scr = make_subcontracting_receipt(sco.name)
+ scr.items[0].qty = 3
+ scr.save()
+
+ # carrying the withholding row over would deduct 800 from a 600 receipt,
+ # and Purchase Receipt rejects the resulting negative Grand Total
+ scr.submit()
+
+ pr_name = frappe.db.get_value("Purchase Receipt", {"subcontracting_receipt": scr.name})
+ self.assertTrue(pr_name)
+
+ pr = frappe.get_doc("Purchase Receipt", pr_name)
+ self.assertEqual(pr.items[0].qty, 6)
+ self.assertEqual(pr.net_total, 600)
+ self.assertFalse([row for row in pr.taxes if row.is_tax_withholding_account])
+ self.assertEqual(pr.grand_total, 600)
+
@ERPNextTestSuite.change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order