From 0b71c943c1c3880d5faa791cc78d799b8cc0872d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 23 Jul 2026 19:17:21 +0530 Subject: [PATCH] fix(selling): don't email cancelled proformas or copy their PDF Address review findings: - send_proforma_email rejects non-issued proformas, and the tab suppresses the action for cancelled rows, so a voided document can't be sent to a customer - mark proforma_pdf as no_copy and disable amendment (a proforma is created only from a Sales Order), so a copied proforma can't carry the original's PDF and number --- erpnext/public/js/sales_order_proforma.js | 8 +++++++- .../doctype/proforma_invoice/proforma_invoice.json | 3 +-- .../selling/doctype/proforma_invoice/proforma_invoice.py | 2 ++ .../doctype/proforma_invoice/test_proforma_invoice.py | 8 ++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/sales_order_proforma.js b/erpnext/public/js/sales_order_proforma.js index 5989d32fe0d..5a72e350a41 100644 --- a/erpnext/public/js/sales_order_proforma.js +++ b/erpnext/public/js/sales_order_proforma.js @@ -301,7 +301,13 @@ Object.assign(erpnext.proforma, { { icon: "mail", label: __("Send Email"), - action: (row, refresh) => this.send_email(frm, row.name, refresh), + action: (row, refresh) => { + if (row.status === "Cancelled") { + frappe.msgprint(__("A cancelled Proforma Invoice cannot be emailed.")); + return; + } + this.send_email(frm, row.name, refresh); + }, }, ], }, diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json index 0a45df89088..9fe2b9616af 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json @@ -187,6 +187,7 @@ "fieldname": "proforma_pdf", "fieldtype": "Attach", "label": "Proforma PDF", + "no_copy": 1, "read_only": 1 }, { @@ -250,7 +251,6 @@ "owner": "Administrator", "permissions": [ { - "amend": 1, "cancel": 1, "create": 1, "delete": 1, @@ -264,7 +264,6 @@ "write": 1 }, { - "amend": 1, "cancel": 1, "create": 1, "delete": 1, diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py index cd7f25e5658..2fbf068d882 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py @@ -212,6 +212,8 @@ def _proforma_line(so_item, based_on: str, row: dict) -> dict | None: @frappe.whitelist() def send_proforma_email(proforma_name: str, recipients: str) -> None: proforma = frappe.get_doc("Proforma Invoice", proforma_name) + if proforma.docstatus != 1: + frappe.throw(_("Only an issued Proforma Invoice can be emailed.")) if not proforma.proforma_pdf: frappe.throw(_("This Proforma Invoice has no PDF to send.")) diff --git a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py index b72c0adcc57..2d9f7843e78 100644 --- a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py @@ -9,6 +9,7 @@ from frappe.utils import flt from erpnext.selling.doctype.proforma_invoice.proforma_invoice import ( get_sales_order_items, make_proforma_invoice, + send_proforma_email, ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.tests.utils import ERPNextTestSuite @@ -144,6 +145,13 @@ class TestProformaInvoice(ERPNextTestSuite): [(sales_order.items[0].name, 4)], ) + def test_cannot_email_cancelled_proforma(self): + sales_order = make_sales_order(qty=10) + proforma = self.create_proforma(sales_order, [(sales_order.items[0].name, 4)]) + proforma.cancel() + + self.assertRaises(frappe.ValidationError, send_proforma_email, proforma.name, "customer@example.com") + def test_requires_submitted_sales_order(self): """The server rejects a proforma against a draft Sales Order (the button is JS-gated only).""" sales_order = make_sales_order(qty=10, do_not_submit=True)