diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 10099631a75..08fe7feff56 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -404,29 +404,10 @@ class PurchaseReceipt(BuyingController): self.set_consumed_qty_in_subcontract_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): super().on_cancel() 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_billing_status() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index edde28a04e6..9d51a20f605 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5446,6 +5446,32 @@ class TestPurchaseReceipt(FrappeTestCase): 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) + 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(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier