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:
parent
4af37fe239
commit
89922bae90
10 changed files with 132 additions and 43 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
25
frappe/model/docstatus.py
Normal 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)
|
||||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
18
frappe/tests/test_base_document.py
Normal file
18
frappe/tests/test_base_document.py
Normal 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)
|
||||
26
frappe/tests/test_docstatus.py
Normal file
26
frappe/tests/test_docstatus.py
Normal 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())
|
||||
Loading…
Add table
Reference in a new issue