mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 22:51:49 +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.
(cherry picked from commit cfe18e8427)
# Conflicts:
# erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
# erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
This commit is contained in:
@@ -404,29 +404,10 @@ class PurchaseReceipt(BuyingController):
|
|||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
self.reserve_stock_for_sales_order()
|
self.reserve_stock_for_sales_order()
|
||||||
|
|
||||||
def check_next_docstatus(self):
|
|
||||||
submit_rv = frappe.db.sql(
|
|
||||||
"""select t1.name
|
|
||||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
|
||||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
|
||||||
(self.name),
|
|
||||||
)
|
|
||||||
if submit_rv:
|
|
||||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0]))
|
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
|
|
||||||
self.check_on_hold_or_closed_status()
|
self.check_on_hold_or_closed_status()
|
||||||
# Check if Purchase Invoice has been submitted against current Purchase Order
|
|
||||||
submitted = frappe.db.sql(
|
|
||||||
"""select t1.name
|
|
||||||
from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2
|
|
||||||
where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""",
|
|
||||||
self.name,
|
|
||||||
)
|
|
||||||
if submitted:
|
|
||||||
frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0]))
|
|
||||||
|
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.update_billing_status()
|
self.update_billing_status()
|
||||||
|
|||||||
@@ -5446,6 +5446,32 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
srbnb_credit = sum(flt(row.credit) for row in gl_entries if row.account == srbnb_account)
|
||||||
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
|
self.assertAlmostEqual(srbnb_credit, pi_base_net_amount, places=2)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
Reference in New Issue
Block a user