fix(buying): allow purchase returns against a closed purchase order (#58139)

This commit is contained in:
Pandiyan P
2026-08-14 09:00:55 +05:30
committed by GitHub
parent ea52ab36d1
commit 61b549960e
4 changed files with 50 additions and 9 deletions

View File

@@ -286,9 +286,7 @@ class PurchaseInvoice(BuyingController):
self.check_conversion_rate()
self.validate_credit_to_acc()
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
@@ -1752,9 +1750,7 @@ class PurchaseInvoice(BuyingController):
super().on_cancel()
PurchaseTaxWithholding(self).on_cancel()
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return

View File

@@ -981,6 +981,14 @@ class BuyingController(SubcontractingController):
item.serial_and_batch_bundle, warehouse, type_of_transaction=type_of_transaction
)
def check_purchase_order_on_hold_or_close(self, ref_fieldname, exclude_if_field=None):
if self.get("is_return"):
return
self.check_for_on_hold_or_closed_status(
"Purchase Order", ref_fieldname, exclude_if_field=exclude_if_field
)
def update_ordered_and_reserved_qty(self):
po_map = {}
for d in self.get("items"):
@@ -994,7 +1002,7 @@ class BuyingController(SubcontractingController):
if po and po_item_rows:
po_obj = frappe.get_lazy_doc("Purchase Order", po)
if po_obj.status in ["Closed", "Cancelled"]:
if po_obj.status == "Cancelled" or (po_obj.status == "Closed" and not self.get("is_return")):
frappe.throw(
_("{doctype} {name} is cancelled or closed.").format(
doctype=frappe.bold(_("Purchase Order")),

View File

@@ -264,7 +264,7 @@ class PurchaseReceipt(BuyingController):
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
self.check_purchase_order_on_hold_or_close("purchase_order")
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
@@ -437,7 +437,7 @@ class PurchaseReceipt(BuyingController):
def on_cancel(self):
super().on_cancel()
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
self.check_purchase_order_on_hold_or_close("purchase_order")
self.update_prevdoc_status()
self.update_billing_status()

View File

@@ -703,6 +703,43 @@ class TestPurchaseReceipt(ERPNextTestSuite):
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
def test_purchase_return_against_closed_purchase_order(self):
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_receipt as make_pr_from_po,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.controllers.sales_and_purchase_return import make_return_doc
po = create_purchase_order(qty=2, rate=100)
receipts = []
for _ in range(2):
pr = make_pr_from_po(po.name)
pr.items[0].qty = pr.items[0].received_qty = 1
pr.submit()
receipts.append(pr)
first_return = make_return_doc("Purchase Receipt", receipts[0].name)
first_return.submit()
po.reload()
po.update_status("Closed")
# a return against a closed Purchase Order should still go through,
# the same way a Delivery Note return does against a closed Sales Order
second_return = make_return_doc("Purchase Receipt", receipts[1].name)
second_return.submit()
self.assertEqual(second_return.docstatus, 1)
self.assertEqual(frappe.db.get_value("Purchase Order", po.name, "status"), "Closed")
# cancelling the return runs the same check on the closed order
second_return.cancel()
# a regular receipt against the closed order must still be blocked
blocked_pr = make_pr_from_po(po.name)
self.assertRaisesRegex(frappe.InvalidStatusError, "Closed", blocked_pr.save)
def test_pr_billing_status(self):
"""Flow:
1. PO -> PR1 -> PI