feat: utility methods for docstatus (#15515)

* feat: utility methods for docstatus

* refactor: use utility method for doctsatus

* refactor: enum for docstatus

* refactor: docstatus as property

* fix: set docstatus

* feat: docstatus extends int class

* test: docstatus of BaseDocument

* refactor: occurrences of docstatus

* fix: typo

* refactor: move docstatus to a separate file

* test: docstatus

* fix: sider
This commit is contained in:
Raffael Meyer 2022-02-04 08:41:25 +01:00 committed by GitHub
parent 4af37fe239
commit 89922bae90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 43 deletions

View file

@ -42,13 +42,13 @@ def submit_cancel_or_update_docs(doctype, docnames, action='submit', data=None):
doc = frappe.get_doc(doctype, d)
try:
message = ''
if action == 'submit' and doc.docstatus==0:
if action == 'submit' and doc.docstatus.is_draft():
doc.submit()
message = _('Submiting {0}').format(doctype)
elif action == 'cancel' and doc.docstatus==1:
elif action == 'cancel' and doc.docstatus.is_submitted():
doc.cancel()
message = _('Cancelling {0}').format(doctype)
elif action == 'update' and doc.docstatus < 2:
elif action == 'update' and not doc.docstatus.is_cancelled():
doc.update(data)
doc.save()
message = _('Updating {0}').format(doctype)

View file

@ -137,7 +137,7 @@ def get_context(context):
if self.set_property_after_alert:
allow_update = True
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
if doc.docstatus.is_submitted() and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
allow_update = False
try:
if allow_update and not doc.flags.in_notification_update:

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
import datetime
from frappe import _
@ -11,6 +12,7 @@ from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils.html_utils import unescape_html
from frappe.model.docstatus import DocStatus
max_positive_value = {
'smallint': 2 ** 15,
@ -20,6 +22,7 @@ max_positive_value = {
DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link')
def get_controller(doctype):
"""Returns the **class** object of the given DocType.
For `custom` type, returns `frappe.model.document.Document`.
@ -224,7 +227,7 @@ class BaseDocument(object):
value.parentfield = key
if value.docstatus is None:
value.docstatus = 0
value.docstatus = DocStatus.draft()
if not getattr(value, "idx", None):
value.idx = len(self.get(key) or []) + 1
@ -282,8 +285,11 @@ class BaseDocument(object):
if key not in self.__dict__:
self.__dict__[key] = None
if key in ("idx", "docstatus") and self.__dict__[key] is None:
self.__dict__[key] = 0
if self.__dict__[key] is None:
if key == "docstatus":
self.docstatus = DocStatus.draft()
elif key == "idx":
self.__dict__[key] = 0
for key in self.get_valid_columns():
if key not in self.__dict__:
@ -304,6 +310,14 @@ class BaseDocument(object):
def is_new(self):
return self.get("__islocal")
@property
def docstatus(self):
return DocStatus(self.get("docstatus"))
@docstatus.setter
def docstatus(self, value):
self.__dict__["docstatus"] = DocStatus(cint(value))
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
doc["doctype"] = self.doctype
@ -492,7 +506,7 @@ class BaseDocument(object):
self.set(df.fieldname, flt(self.get(df.fieldname)))
if self.docstatus is not None:
self.docstatus = cint(self.docstatus)
self.docstatus = DocStatus(cint(self.docstatus))
def _get_missing_mandatory_fields(self):
"""Get mandatory fields that do not have any values"""
@ -581,7 +595,7 @@ class BaseDocument(object):
setattr(self, df.fieldname, values.name)
for _df in fields_to_fetch:
if self.is_new() or self.docstatus != 1 or _df.allow_on_submit:
if self.is_new() or not self.docstatus.is_submitted() or _df.allow_on_submit:
self.set_fetch_from_value(doctype, _df, values)
notify_link_count(doctype, docname)
@ -591,7 +605,7 @@ class BaseDocument(object):
elif (df.fieldname != "amended_from"
and (is_submittable or self.meta.is_submittable) and frappe.get_meta(doctype).is_submittable
and cint(frappe.db.get_value(doctype, docname, "docstatus"))==2):
and cint(frappe.db.get_value(doctype, docname, "docstatus")) == DocStatus.cancelled()):
cancelled_links.append((df.fieldname, docname, get_msg(df, docname)))
@ -805,8 +819,8 @@ class BaseDocument(object):
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
or self.docstatus.is_cancelled()
or (self.docstatus.is_submitted() and not df.get("allow_on_submit"))):
continue
else:

View file

@ -212,7 +212,7 @@ def check_permission_and_not_submitted(doc):
.format(doc.doctype, doc.name), raise_exception=frappe.PermissionError)
# check if submitted
if doc.docstatus == 1:
if doc.docstatus.is_submitted():
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
raise_exception=True)

25
frappe/model/docstatus.py Normal file
View file

@ -0,0 +1,25 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
class DocStatus(int):
def is_draft(self):
return self == self.draft()
def is_submitted(self):
return self == self.submitted()
def is_cancelled(self):
return self == self.cancelled()
@classmethod
def draft(cls):
return cls(0)
@classmethod
def submitted(cls):
return cls(1)
@classmethod
def cancelled(cls):
return cls(2)

View file

@ -1,13 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
import hashlib
import json
import time
from werkzeug.exceptions import NotFound
import frappe
from frappe import _, msgprint, is_whitelisted
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
from frappe.model.base_document import BaseDocument, get_controller
from frappe.model.naming import set_new_name, gen_new_name_for_cancelled_doc
from werkzeug.exceptions import NotFound, Forbidden
import hashlib, json
from frappe.model.docstatus import DocStatus
from frappe.model import optional_fields, table_fields
from frappe.model.workflow import validate_workflow
from frappe.model.workflow import set_workflow_state_on_action
@ -17,6 +20,7 @@ from frappe.desk.form.document_follow import follow_document
from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event
from frappe.utils.data import get_absolute_url
# once_only validation
# methods
@ -307,7 +311,7 @@ class Document(BaseDocument):
self.check_permission("write", "save")
if self.docstatus == 2:
if self.docstatus.is_cancelled():
self._rename_doc_on_cancel()
self.set_user_and_timestamp()
@ -490,7 +494,7 @@ class Document(BaseDocument):
def set_docstatus(self):
if self.docstatus is None:
self.docstatus=0
self.docstatus = DocStatus.draft()
for d in self.get_all_children():
d.docstatus = self.docstatus
@ -740,7 +744,7 @@ class Document(BaseDocument):
else:
self.check_docstatus_transition(0)
def check_docstatus_transition(self, docstatus):
def check_docstatus_transition(self, to_docstatus):
"""Ensures valid `docstatus` transition.
Valid transitions are (number in brackets is `docstatus`):
@ -751,31 +755,32 @@ class Document(BaseDocument):
"""
if not self.docstatus:
self.docstatus = 0
if docstatus==0:
if self.docstatus==0:
self.docstatus = DocStatus.draft()
if to_docstatus == DocStatus.draft():
if self.docstatus.is_draft():
self._action = "save"
elif self.docstatus==1:
elif self.docstatus.is_submitted():
self._action = "submit"
self.check_permission("submit")
elif self.docstatus==2:
elif self.docstatus.is_cancelled():
raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 0 (Draft) to 2 (Cancelled)"))
else:
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
elif docstatus==1:
if self.docstatus==1:
elif to_docstatus == DocStatus.submitted():
if self.docstatus.is_submitted():
self._action = "update_after_submit"
self.check_permission("submit")
elif self.docstatus==2:
elif self.docstatus.is_cancelled():
self._action = "cancel"
self.check_permission("cancel")
elif self.docstatus==0:
elif self.docstatus.is_draft():
raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 1 (Submitted) to 0 (Draft)"))
else:
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
elif docstatus==2:
elif to_docstatus == DocStatus.cancelled():
raise frappe.ValidationError(_("Cannot edit cancelled document"))
def set_parent_in_children(self):
@ -929,14 +934,14 @@ class Document(BaseDocument):
@whitelist.__func__
def _submit(self):
"""Submit the document. Sets `docstatus` = 1, then saves."""
self.docstatus = 1
self.docstatus = DocStatus.submitted()
return self.save()
@whitelist.__func__
def _cancel(self):
"""Cancel the document. Sets `docstatus` = 2, then saves.
"""
self.docstatus = 2
self.docstatus = DocStatus.cancelled()
return self.save()
@whitelist.__func__
@ -954,7 +959,7 @@ class Document(BaseDocument):
frappe.delete_doc(self.doctype, self.name, ignore_permissions = ignore_permissions, flags=self.flags)
def run_before_save_methods(self):
"""Run standard methods before `INSERT` or `UPDATE`. Standard Methods are:
"""Run standard methods before `INSERT` or `UPDATE`. Standard Methods are:
- `validate`, `before_save` for **Save**.
- `validate`, `before_submit` for **Submit**.

View file

@ -1,10 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe.utils import cint
from frappe import _
import json
from frappe.utils import cint
from frappe.model.docstatus import DocStatus
class WorkflowStateError(frappe.ValidationError): pass
class WorkflowTransitionError(frappe.ValidationError): pass
@ -102,13 +103,13 @@ def apply_workflow(doc, action):
doc.set(next_state.update_field, next_state.update_value)
new_docstatus = cint(next_state.doc_status)
if doc.docstatus == 0 and new_docstatus == 0:
if doc.docstatus.is_draft() and new_docstatus == DocStatus.draft():
doc.save()
elif doc.docstatus == 0 and new_docstatus == 1:
elif doc.docstatus.is_draft() and new_docstatus == DocStatus.submitted():
doc.submit()
elif doc.docstatus == 1 and new_docstatus == 1:
elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.submitted():
doc.save()
elif doc.docstatus == 1 and new_docstatus == 2:
elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.cancelled():
doc.cancel()
else:
frappe.throw(_('Illegal Document Status for {0}').format(next_state.state))
@ -212,10 +213,10 @@ def bulk_workflow_approval(docnames, doctype, action):
frappe.db.commit()
except Exception as e:
if not frappe.message_log:
# Exception is raised manually and not from msgprint or throw
# Exception is raised manually and not from msgprint or throw
message = "{0}".format(e.__class__.__name__)
if e.args:
message += " : {0}".format(e.args[0])
message += " : {0}".format(e.args[0])
message_dict = {"docname": docname, "message": message}
failed_transactions[docname].append(message_dict)

View file

@ -59,9 +59,9 @@ class EnergyPointRule(Document):
# indicates that this was a new doc
return doc.get_doc_before_save() is None
if self.for_doc_event == 'Submit':
return doc.docstatus == 1
return doc.docstatus.is_submitted()
if self.for_doc_event == 'Cancel':
return doc.docstatus == 2
return doc.docstatus.is_cancelled()
if self.for_doc_event == 'Value Change':
field_to_check = self.field_to_check
if not field_to_check: return False
@ -96,7 +96,7 @@ def process_energy_points(doc, state):
old_doc = doc.get_doc_before_save()
# check if doc has been cancelled
if old_doc and old_doc.docstatus == 1 and doc.docstatus == 2:
if old_doc and old_doc.docstatus.is_submitted() and doc.docstatus.is_cancelled():
return revert_points_for_cancelled_doc(doc)
for d in frappe.cache_manager.get_doctype_map('Energy Point Rule', doc.doctype,

View file

@ -0,0 +1,18 @@
import unittest
from frappe.model.base_document import BaseDocument
class TestBaseDocument(unittest.TestCase):
def test_docstatus(self):
doc = BaseDocument({"docstatus": 0})
self.assertTrue(doc.docstatus.is_draft())
self.assertEquals(doc.docstatus, 0)
doc.docstatus = 1
self.assertTrue(doc.docstatus.is_submitted())
self.assertEquals(doc.docstatus, 1)
doc.docstatus = 2
self.assertTrue(doc.docstatus.is_cancelled())
self.assertEquals(doc.docstatus, 2)

View file

@ -0,0 +1,26 @@
import unittest
from frappe.model.docstatus import DocStatus
class TestDocStatus(unittest.TestCase):
def test_draft(self):
self.assertEqual(DocStatus(0), DocStatus.draft())
self.assertTrue(DocStatus.draft().is_draft())
self.assertFalse(DocStatus.draft().is_cancelled())
self.assertFalse(DocStatus.draft().is_submitted())
def test_submitted(self):
self.assertEqual(DocStatus(1), DocStatus.submitted())
self.assertFalse(DocStatus.submitted().is_draft())
self.assertTrue(DocStatus.submitted().is_submitted())
self.assertFalse(DocStatus.submitted().is_cancelled())
def test_cancelled(self):
self.assertEqual(DocStatus(2), DocStatus.cancelled())
self.assertFalse(DocStatus.cancelled().is_draft())
self.assertFalse(DocStatus.cancelled().is_submitted())
self.assertTrue(DocStatus.cancelled().is_cancelled())