mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 21:51:48 +00:00
fix: let Purchase Receipt cancel defer to Frappe's linked-document check (#57592)
on_cancel pre-blocked cancellation with its own "Purchase Invoice is already submitted" guard, duplicating the check Frappe already runs for any submitted linked document. Drop the guard and the unused check_next_docstatus() method it mirrored so the receipt defers to the framework: the Cancel All Documents flow cancels the invoice first and then the receipt, and a direct cancel is still rejected by Frappe's linked-document check. Add a regression test that a direct cancel of a receipt with a submitted invoice is rejected and rolls back, leaving no stray stock or GL entries.
This commit is contained in:
@@ -423,31 +423,10 @@ class PurchaseReceipt(BuyingController):
|
||||
row.received_qty,
|
||||
)
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"purchase_receipt": self.name, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
as_list=True,
|
||||
limit=1,
|
||||
)
|
||||
if submit_rv:
|
||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submit_rv[0][0]))
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
|
||||
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
|
||||
# Check if Purchase Invoice has been submitted against current Purchase Order
|
||||
submitted = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"purchase_receipt": self.name, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
as_list=True,
|
||||
limit=1,
|
||||
)
|
||||
if submitted:
|
||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
|
||||
|
||||
self.update_prevdoc_status()
|
||||
self.update_billing_status()
|
||||
|
||||
@@ -6131,17 +6131,31 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
# already received against this PO line, excluding pr2 itself, is pr1's 4
|
||||
self.assertEqual(pr2.get_already_received_qty(po.name, po_detail), 4.0)
|
||||
|
||||
def test_check_next_docstatus_blocks_with_submitted_invoice(self):
|
||||
"""check_next_docstatus must flag a submitted Purchase Invoice drawn from the receipt —
|
||||
covers the converted child-table get_all (Purchase Invoice Item, docstatus=1)."""
|
||||
def test_cancel_blocked_by_submitted_invoice_rolls_back(self):
|
||||
"""A submitted Purchase Invoice must block cancelling its Purchase Receipt. Frappe's backlink
|
||||
check rejects the cancel only after on_cancel has run stock, GL, and status work, so the whole
|
||||
transaction has to roll back: the receipt stays submitted with no leaked ledger entries."""
|
||||
pr = make_purchase_receipt()
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as cm:
|
||||
pr.check_next_docstatus()
|
||||
self.assertIn("is already submitted", str(cm.exception))
|
||||
pr.reload()
|
||||
status_before = pr.status
|
||||
sle_before = frappe.db.count("Stock Ledger Entry", {"voucher_no": pr.name})
|
||||
gle_before = frappe.db.count("GL Entry", {"voucher_no": pr.name})
|
||||
|
||||
frappe.db.savepoint("before_blocked_cancel")
|
||||
with self.assertRaises(frappe.LinkExistsError) as cm:
|
||||
pr.cancel()
|
||||
self.assertIn(pi.name, str(cm.exception))
|
||||
frappe.db.rollback(save_point="before_blocked_cancel") # mimic the request-level rollback
|
||||
|
||||
pr.reload()
|
||||
self.assertEqual(pr.docstatus, 1)
|
||||
self.assertEqual(pr.status, status_before)
|
||||
self.assertEqual(frappe.db.count("Stock Ledger Entry", {"voucher_no": pr.name}), sle_before)
|
||||
self.assertEqual(frappe.db.count("GL Entry", {"voucher_no": pr.name}), gle_before)
|
||||
|
||||
|
||||
def create_asset_category_for_pr_test():
|
||||
|
||||
Reference in New Issue
Block a user