From e4fb5ed3c4a08b87d2a689ee79721a4c94c57d19 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 23 Jul 2026 18:59:08 +0530 Subject: [PATCH] fix(selling): guard proforma against unsubmitted SO and missing PDF Address review findings: - make_proforma_invoice: reject a non-submitted Sales Order (the whitelisted endpoint was previously only JS-gated on docstatus) - send_proforma_email: throw a clear error when the attached PDF File is missing instead of passing a null fid to sendmail --- .../doctype/proforma_invoice/proforma_invoice.py | 4 ++++ .../doctype/proforma_invoice/test_proforma_invoice.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py index ac9d1bc046e..cd7f25e5658 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py @@ -152,6 +152,8 @@ def make_proforma_invoice( validate_feature_enabled() selected = frappe.parse_json(items) sales_order_doc = frappe.get_doc("Sales Order", sales_order) + if sales_order_doc.docstatus != 1: + frappe.throw(_("A Proforma Invoice can only be created against a submitted Sales Order.")) so_items = {item.name: item for item in sales_order_doc.items} proforma = frappe.new_doc("Proforma Invoice") @@ -214,6 +216,8 @@ def send_proforma_email(proforma_name: str, recipients: str) -> None: frappe.throw(_("This Proforma Invoice has no PDF to send.")) file_name = frappe.db.get_value("File", {"file_url": proforma.proforma_pdf}, "name") + if not file_name: + frappe.throw(_("The attached PDF file could not be found.")) frappe.sendmail( recipients=[email.strip() for email in recipients.split(",") if email.strip()], subject=_("Proforma Invoice {0}").format(proforma.name), diff --git a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py index a5e1789bb85..b72c0adcc57 100644 --- a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py @@ -143,3 +143,14 @@ class TestProformaInvoice(ERPNextTestSuite): sales_order, [(sales_order.items[0].name, 4)], ) + + 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) + + self.assertRaises( + frappe.ValidationError, + self.create_proforma, + sales_order, + [(sales_order.items[0].name, 4)], + )