fix: validate doc_status for non-submittable doctypes in workflow

This commit is contained in:
Shubh Doshi 2026-02-18 13:41:28 +05:30
parent f12b5c081b
commit 675b23c47c
5 changed files with 118 additions and 12 deletions

View file

@ -4,6 +4,11 @@ import { useStore } from "../store";
let store = useStore();
const is_doc_status_readonly = computed(() => {
if (!store.workflow.selected || !("state" in store.workflow.selected.data)) return false;
return !store.is_submittable();
});
let title = ref("Workflow Details");
let doc = computed(() => {
@ -30,6 +35,13 @@ let properties = computed(() => {
);
store.statefields.splice(2, 0, allow_edit);
const submittable = store.is_submittable();
// Auto-reset doc_status to "Draft" for non-submittable doctypes
if (!submittable && store.workflow.selected.data.doc_status !== "Draft") {
store.workflow.selected.data.doc_status = "Draft";
}
return store.statefields.filter((df) => {
if (df.fieldname == "doc_status") {
df.options = ["Draft", "Submitted", "Cancelled"];
@ -61,6 +73,7 @@ let properties = computed(() => {
v-model="doc[df.fieldname]"
:data-fieldname="df.fieldname"
:data-fieldtype="df.fieldtype"
:read_only="df.fieldname === 'doc_status' ? is_doc_status_readonly : false"
/>
</div>
</div>

View file

@ -135,13 +135,18 @@ export const useStore = defineStore("workflow-builder-store", () => {
frappe.breadcrumbs.$breadcrumbs.append(breadcrumbs);
}
function is_submittable() {
if (!workflow_doc.value?.document_type) return true;
return frappe.get_meta(workflow_doc.value.document_type)?.is_submittable;
}
function get_state_df(data) {
let doc_status_map = {
Draft: 0,
Submitted: 1,
Cancelled: 2,
};
data.doc_status = doc_status_map[data.doc_status];
data.doc_status = is_submittable() ? doc_status_map[data.doc_status] : 0;
return data;
}
@ -234,5 +239,6 @@ export const useStore = defineStore("workflow-builder-store", () => {
reset_changes,
save_changes,
setup_undo_redo,
is_submittable,
};
});

View file

@ -108,14 +108,15 @@ class TestWorkflow(IntegrationTestCase):
self.assertEqual(workflow_actions[0].status, "Completed")
def test_if_workflow_set_on_action(self):
self.workflow, doc = create_new_submittable_doctype_with_workflow()
self.workflow._update_state_docstatus = True
self.workflow.states[1].doc_status = 1
self.workflow.save()
todo = create_new_todo()
self.assertEqual(todo.docstatus, 0)
todo.submit()
self.assertEqual(todo.docstatus, 1)
self.assertEqual(todo.workflow_state, "Approved")
self.assertEqual(doc.docstatus, 0)
doc.submit()
self.assertEqual(doc.docstatus, 1)
self.assertEqual(doc.workflow_state, "Approved")
self.workflow.states[1].doc_status = 0
self.workflow.save()
@ -350,6 +351,57 @@ def create_new_todo():
return frappe.get_doc(doctype="ToDo", description="workflow " + random_string(10)).insert()
def create_new_submittable_doctype_with_workflow():
submittable_dt = frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"name": "Test Submittable Doc",
"custom": 1,
"is_submittable": 1,
"fields": [
{"label": "Field", "fieldname": "test_field", "fieldtype": "Data"},
{
"label": "Workflow State",
"fieldname": "workflow_state",
"fieldtype": "Link",
"options": "Workflow State",
},
],
"permissions": [{"role": "System Manager", "read": 1, "write": 1, "submit": 1, "cancel": 1}],
}
).insert(ignore_if_duplicate=True)
workflow = None
if not frappe.db.exists("Workflow", "Submittable Workflow"):
workflow = frappe.new_doc("Workflow")
workflow.workflow_name = "Submittable Workflow"
workflow.document_type = submittable_dt.name
workflow.workflow_state_field = "workflow_state"
workflow.is_active = 1
workflow.append("states", dict(state="Pending", allow_edit="All"))
workflow.append(
"states",
dict(state="Approved", allow_edit="System Manager", doc_status=0),
)
workflow.append(
"transitions",
dict(
state="Pending",
action="Approve",
next_state="Approved",
allowed="System Manager",
allow_self_approval=1,
),
)
workflow.insert(ignore_permissions=True)
else:
workflow = frappe.get_doc("Workflow", "Submittable Workflow")
doc = frappe.get_doc({"doctype": submittable_dt.name, "test_field": "test"}).insert()
return workflow, doc
def create_new_note(doc):
note = frappe.new_doc("Note")
note.title = "workflow - " + doc.name

View file

@ -109,9 +109,10 @@ frappe.ui.form.on("Workflow", {
return;
}
frappe.model.with_doctype(doc.document_type, () => {
const fieldnames = frappe
.get_meta(doc.document_type)
.fields.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
const meta = frappe.get_meta(doc.document_type);
const is_submittable = meta.is_submittable;
const fieldnames = meta.fields
.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
.map((field) => field.fieldname);
frm.fields_dict.states.grid.update_docfield_property(
@ -119,6 +120,23 @@ frappe.ui.form.on("Workflow", {
"options",
[""].concat(fieldnames)
);
frm.fields_dict.states.grid.update_docfield_property(
"doc_status",
"read_only",
!is_submittable
);
if (!is_submittable) {
let changed = false;
frm.doc.states.forEach((row) => {
if (parseInt(row.doc_status || 0) !== 0) {
row.doc_status = "0";
changed = true;
}
});
if (changed) frm.refresh_field("states");
}
});
},
create_warning_dialog: function (frm) {

View file

@ -90,23 +90,40 @@ class Workflow(Document):
frappe.throw(frappe._("{0} not a valid State").format(state))
# Check if doctype is submittable
meta = frappe.get_meta(self.document_type)
is_submittable = meta.is_submittable
# Validate that non-submittable doctypes only have doc_status 0
if not is_submittable:
for state in self.states:
if cint(state.doc_status or 0) != 0:
frappe.throw(
frappe._(
"Workflow State '{0}' has Document Status {1}, but DocType '{2}' is not submittable. "
"Only Document Status 0 (Draft) is allowed for non-submittable DocTypes."
).format(state.state, state.doc_status, self.document_type)
)
for t in self.transitions:
state = get_state(t.state)
next_state = get_state(t.next_state)
state_docstatus = cint(state.doc_status or 0)
next_state_docstatus = cint(next_state.doc_status or 0)
if state.doc_status == "2":
if state_docstatus == 2:
frappe.throw(
frappe._("Cannot change state of Cancelled Document. Transition row {0}").format(t.idx)
)
if state.doc_status == "1" and next_state.doc_status == "0":
if state_docstatus == 1 and next_state_docstatus == 0:
frappe.throw(
frappe._(
"Submitted Document cannot be converted back to draft. Transition row {0}"
).format(t.idx)
)
if state.doc_status == "0" and next_state.doc_status == "2":
if state_docstatus == 0 and next_state_docstatus == 2:
frappe.throw(frappe._("Cannot cancel before submitting. See Transition {0}").format(t.idx))
def set_active(self):