diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index a88bc99f0a..4e5d0d606b 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -57,7 +57,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nBefore Print\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Discard\nAfter Discard\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nBefore Print\nOn Payment Authorization\nOn Payment Paid\nOn Payment Failed" }, { "depends_on": "eval:doc.script_type==='API'", @@ -151,7 +151,7 @@ "link_fieldname": "server_script" } ], - "modified": "2024-04-08 16:18:52.901097", + "modified": "2024-04-15 20:12:41.971315", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index f5b4e518bf..d3af5d1fc6 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -42,6 +42,8 @@ class ServerScript(Document): "After Submit", "Before Cancel", "After Cancel", + "Before Discard", + "After Discard", "Before Delete", "After Delete", "Before Save (Submitted Document)", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index ebc5fe9e9d..418f3aea83 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -15,6 +15,8 @@ EVENT_MAP = { "on_submit": "After Submit", "before_cancel": "Before Cancel", "on_cancel": "After Cancel", + "before_discard": "Before Discard", + "on_discard": "After Discard", "on_trash": "Before Delete", "after_delete": "After Delete", "before_update_after_submit": "Before Save (Submitted Document)", diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 795a7deeb9..49a060ad46 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -59,6 +59,17 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat frappe.msgprint(frappe._("Cancelled"), indicator="red", alert=True) +@frappe.whitelist() +def discard(doctype: str, name: str | int): + """discard a draft document""" + doc = frappe.get_doc(doctype, name) + capture_doc(doc, "Discard") + + doc.discard() + send_updated_docs(doc) + frappe.msgprint(frappe._("Discarded"), indicator="red", alert=True) + + def send_updated_docs(doc): from .load import get_docinfo diff --git a/frappe/model/document.py b/frappe/model/document.py index 17fcaf50da..f9c9ae7ae1 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -809,11 +809,12 @@ class Document(BaseDocument): self.load_doc_before_save(raise_exception=True) - self._action = "save" - previous = self._doc_before_save + if not hasattr(self, "_action"): + self._action = "save" + previous = self._doc_before_save # previous is None for new document insert - if not previous: + if not previous and self._action != "discard": self.check_docstatus_transition(0) return @@ -825,7 +826,7 @@ class Document(BaseDocument): raise_exception=frappe.TimestampMismatchError, ) - if not self.meta.issingle: + if not self.meta.issingle and self._action != "discard": self.check_docstatus_transition(previous.docstatus) def check_docstatus_transition(self, to_docstatus): @@ -1058,6 +1059,25 @@ class Document(BaseDocument): """Cancel the document. Sets `docstatus` = 2, then saves.""" return self._cancel() + @frappe.whitelist() + def discard(self): + """Discard the draft document. Sets `docstatus` = 2 with db_set.""" + self._action = "discard" + + self.check_if_locked() + self.set_user_and_timestamp() + self.check_if_latest() + + if not self.docstatus == DocStatus.draft(): + raise frappe.ValidationError(_("Only draft documents can be discarded"), self.docstatus) + + self.check_permission("write") + + self.run_method("before_discard") + self.db_set("docstatus", DocStatus.cancelled()) + delattr(self, "_action") + self.run_method("on_discard") + @frappe.whitelist() def rename(self, name: str, merge=False, force=False, validate_rename=True): """Rename the document to `name`. This transforms the current object.""" diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 854d60d907..252010d36f 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -833,6 +833,17 @@ frappe.ui.form.Form = class FrappeForm { } } + discard(btn, callback, on_error) { + const me = this; + return new Promise((resolve) => { + frappe.confirm(__("Discard {0}", [this.docname]), function () { + me.script_manager.trigger("before_discard").then(function () { + return me._discard(btn, callback, on_error, false); // ? + }); + }); + }); + } + savesubmit(btn, callback, on_error) { var me = this; return new Promise((resolve) => { @@ -1015,6 +1026,52 @@ frappe.ui.form.Form = class FrappeForm { } } + _discard(btn, on_error, skip_confirm) { + const me = this; + const discard_doc = () => { + frappe.validated = true; + me.script_manager.trigger("before_discard").then(() => { + if (!frappe.validated) { + return me.handle_save_fail(btn, on_error); + } + + var after_discard = function (r) { + if (r.exc) { + me.handle_save_fail(btn, on_error); + } else { + frappe.utils.play_sound("cancel"); + me.refresh(); + me.script_manager.trigger("after_discard"); + } + me.reload_doc(); + }; + //frappe.ui.form.discard(me, after_discard, btn); + frappe.call({ + freeze: true, + method: "frappe.desk.form.save.discard", + args: { + doctype: me.doc.doctype, + name: me.doc.name, + }, + btn: btn, + callback: function (r) { + after_discard(r); + }, + }); + }); + }; + + if (skip_confirm) { + discard_doc(); + } else { + frappe.confirm( + __("Permanently Discard {0}?", [this.docname]), + discard_doc, + me.handle_save_fail(btn, on_error) + ); + } + } + savetrash() { this.validate_form_action("Delete"); frappe.model.delete_doc(this.doctype, this.docname, function () { diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 0fc3b114fb..1183d04d03 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -310,6 +310,16 @@ frappe.ui.form.Toolbar = class Toolbar { const allow_print_for_draft = cint(print_settings.allow_print_for_draft); const allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); + if (is_submittable && docstatus == 0 && !this.has_workflow()) { + this.page.add_menu_item( + __("Discard"), + function () { + me.frm._discard(); + }, + true + ); + } + if ( !is_submittable || docstatus == 1 || @@ -584,9 +594,7 @@ frappe.ui.form.Toolbar = class Toolbar { } has_workflow() { if (this._has_workflow === undefined) - this._has_workflow = frappe.get_list("Workflow", { - document_type: this.frm.doctype, - }).length; + this._has_workflow = frappe.model.has_workflow(this.frm.doctype); return this._has_workflow; } get_docstatus() { diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index dd76970903..5f7c0a5efb 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -98,6 +98,37 @@ class TestDocument(FrappeTestCase): self.assertEqual(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed") + def test_discard_transitions(self): + d = self.test_insert() + self.assertEqual(d.docstatus, 0) + + # invalid: Submit > Discard, Cancel > Discard + d.submit() + self.assertRaises(frappe.ValidationError, d.discard) + d.reload() + + d.cancel() + self.assertRaises(frappe.ValidationError, d.discard) + + # valid: Draft > Discard + d2 = self.test_insert() + d2.discard() + self.assertEqual(d2.docstatus, 2) + + def test_save_on_discard_throws(self): + from frappe.desk.doctype.event.event import Event + + d3 = self.test_insert() + + def test_on_discard(d3): + d3.subject = d3.subject + "update" + d3.save() + + d3.on_discard = (test_on_discard)(d3) + d3.on_discard = test_on_discard.__get__(d3, Event) + + self.assertRaises(frappe.ValidationError, d3.discard) + def test_value_changed(self): d = self.test_insert() d.subject = "subject changed again"