diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 84e1e4d2d9..3a15f913d4 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -34,4 +34,6 @@ class MandatoryError(ValidationError): pass class InvalidSignatureError(ValidationError): pass class RateLimitExceededError(ValidationError): pass class CannotChangeConstantError(ValidationError): pass -class LinkValidationError(ValidationError): pass \ No newline at end of file +class LinkValidationError(ValidationError): pass +class DocstatusTransitionError(ValidationError): pass +class TimestampMismatchError(ValidationError): pass \ No newline at end of file diff --git a/frappe/model/document.py b/frappe/model/document.py index 69e0f77cad..df2ff10eee 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint -from frappe.utils import cint, flt +from frappe.utils import cint, flt, cstr, now from frappe.model import default_fields from frappe.model.db_schema import type_map from frappe.model.naming import set_new_name @@ -98,11 +98,7 @@ class BaseDocument(object): for fieldname in self.valid_columns: d[fieldname] = self.get(fieldname) return d - - def get_for_save(self): - d = self.get_valid_dict() - - + @property def valid_columns(self): return self.get_valid_columns() @@ -250,7 +246,8 @@ class Document(BaseDocument): # check links # check permissions - self.set_defaults() + self._set_defaults() + self._set_docstatus_user_and_timestamp() self._validate() # run validate, on update etc. @@ -271,6 +268,7 @@ class Document(BaseDocument): self.insert() return + self._set_docstatus_user_and_timestamp() self._validate() # parent @@ -285,14 +283,33 @@ class Document(BaseDocument): d.db_update() def update_single(self, d): - frappe.db.sql("""delete from tabSingles where doctype=%s""", d.get("doctype")) + frappe.db.sql("""delete from tabSingles where doctype=%s""", self.doctype) for field, value in d.iteritems(): if field not in ("doctype"): frappe.db.sql("""insert into tabSingles(doctype, field, value) - values (%s, %s, %s)""", (d.get("doctype", field, value))) - + values (%s, %s, %s)""", (self.doctype, field, value)) + + def _set_docstatus_user_and_timestamp(self): + self._original_modified = self.modified + self.modified = now() + self.modified_by = frappe.session.user + if not self.creation: + self.creation = self.modified + if not self.owner: + self.owner = self.modified_by + if self.docstatus==None: + self.docstatus=0 + + for d in self.get_all_children(): + d.docstatus = self.docstatus + d.modified = self.modified + d.modified_by = self.modified_by + if not d.owner: + d.owner = self.owner + if not d.creation: + d.creation = self.creation - def set_defaults(self): + def _set_defaults(self): if frappe.flags.in_import: return @@ -308,60 +325,66 @@ class Document(BaseDocument): d.set_missing_values(new_doc) def _validate(self): - #self.check_if_latest() + self.check_if_latest() self.validate_mandatory() self.validate_links() # check restrictions - def check_if_latest(self, method="save"): + def check_if_latest(self): conflict = False if not self.get('__islocal'): if self.meta.issingle: modified = frappe.db.get_value(self.doctype, self.name, "modified") - if cstr(modified) and cstr(modified) != cstr(self.modified): + if cstr(modified) and cstr(modified) != cstr(self._original_modified): conflict = True else: - tmp = frappe.db.sql("""select modified, docstatus from `tab%s` - where name=%s for update""" - % (self.doctype, '%s'), self.name, as_dict=True) + tmp = frappe.db.get_value(self.doctype, self.name, + ["modified", "docstatus"], as_dict=True) if not tmp: frappe.msgprint("""This record does not exist. Please refresh.""", raise_exception=1) - modified = cstr(tmp[0].modified) - if modified and modified != cstr(self.modified): + modified = cstr(tmp.modified) + + if modified and modified != cstr(self._original_modified): conflict = True - self.check_docstatus_transition(tmp[0].docstatus, method) + self.check_docstatus_transition(tmp.docstatus) if conflict: frappe.msgprint(_("Error: Document has been modified after you have opened it") \ + (" (%s, %s). " % (modified, self.modified)) \ - + _("Please refresh to get the latest document."), raise_exception=TimestampMismatchError) - - def check_docstatus_transition(self, db_docstatus): - valid = { - "save": [0,0], - "submit": [0,1], - "cancel": [1,2], - "update_after_submit": [1,1] - } - - labels = { - 0: _("Draft"), - 1: _("Submitted"), - 2: _("Cancelled") - } - - if not hasattr(self, "to_docstatus"): - self.to_docstatus = 0 - - if [db_docstatus, self.to_docstatus] != valid[method]: - frappe.msgprint(_("Cannot change from") + ": " + labels[db_docstatus] + " > " + \ - labels[self.to_docstatus], raise_exception=DocstatusTransitionError) + + _("Please refresh to get the latest document."), + raise_exception=frappe.TimestampMismatchError) + def check_docstatus_transition(self, docstatus): + if not self.docstatus: + self.docstatus = 0 + if docstatus==0: + if self.docstatus==0: + self._action = "save" + elif self.docstatus==1: + self._action = "submit" + else: + raise frappe.DocstatusTransitionError + elif docstatus==1: + if self.docstatus==1: + self._action = "update_after_submit" + self.validate_update_after_submit() + elif self.docstatus==2: + self._action = "cancel" + else: + raise frappe.DocstatusTransitionError + + elif docstatus==2: + raise frappe.ValidationError + + def validate_update_after_submit(self): + # check only allowed values are updated + pass + def validate_mandatory(self): if self.get("ignore_mandatory"): return diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index ed5a678e29..5496290e06 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -1,7 +1,7 @@ # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -import frappe, unittest +import frappe, unittest, time from frappe.model.document import Document @@ -74,7 +74,21 @@ class TestDocument(unittest.TestCase): d.set("first_name", "Test Mandatory") d.insert() self.assertEquals(frappe.db.get_value("User", d.name), d.name) - + + def test_confict_validation(self): + d1 = self.test_insert() + d2 = Document(d1.doctype, d1.name) + time.sleep(1) + d1.save() + self.assertRaises(frappe.TimestampMismatchError, d2.save) + + def test_confict_validation_single(self): + d1 = Document("Website Settings", "Website Settings") + d2 = Document("Website Settings", "Website Settings") + time.sleep(1) + d1.save() + self.assertRaises(frappe.TimestampMismatchError, d2.save) + def test_link_validation(self): d = Document({ "doctype": "User",