Merge pull request #25971 from rutwikhdev/discard-transactions

feat: Discard transactions
This commit is contained in:
Ankush Menat 2024-04-29 16:08:37 +05:30 committed by GitHub
commit ff31290d33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 140 additions and 9 deletions

View file

@ -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",

View file

@ -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)",

View file

@ -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)",

View file

@ -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

View file

@ -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."""

View file

@ -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 () {

View file

@ -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() {

View file

@ -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"