From 6b671af1ec29ae1e95595b7e8dd2c8495eedaa6a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 12:29:23 +0530 Subject: [PATCH] Merge branch 'develop' into autocomplete-control --- frappe/core/doctype/docfield/docfield.json | 9 ++- frappe/core/doctype/doctype/doctype.py | 13 ++-- .../doctype/custom_field/custom_field.json | 11 +++- .../doctype/custom_field/custom_field.py | 4 +- .../doctype/customize_form/customize_form.py | 9 ++- .../customize_form_field.json | 9 ++- .../property_setter/property_setter.py | 55 ++++------------ frappe/database/schema.py | 5 +- frappe/model/base_document.py | 28 ++++++-- frappe/model/meta.py | 11 +++- .../js/frappe/form/controls/base_control.js | 5 +- frappe/public/js/frappe/form/quick_entry.js | 2 +- .../public/js/frappe/form/script_manager.js | 15 ++++- frappe/public/js/frappe/list/list_view.js | 3 +- frappe/public/js/frappe/utils/common.js | 13 ++-- .../js/frappe/views/reports/report_view.js | 3 +- frappe/public/js/frappe/views/treeview.js | 2 +- frappe/tests/test_document.py | 65 +++++++++++++++++-- frappe/website/doctype/web_form/web_form.js | 2 +- 19 files changed, 178 insertions(+), 86 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 69c8c3505f..3267429298 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -17,6 +17,7 @@ "hide_days", "hide_seconds", "reqd", + "is_virtual", "search_index", "column_break_18", "options", @@ -534,13 +535,19 @@ "fieldname": "show_dashboard", "fieldtype": "Check", "label": "Show Dashboard" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Virtual" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-04 11:56:19.812863", + "modified": "2022-02-14 11:56:19.812863", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index aa731a686b..6d0409521e 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1078,6 +1078,9 @@ def validate_fields(meta): field.fetch_from = field.fetch_from.strip('\n').strip() def validate_data_field_type(docfield): + if docfield.get("is_virtual"): + return + if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"): if docfield.options and (docfield.options not in data_field_options): df_str = frappe.bold(_(docfield.label)) @@ -1323,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): else: raise -def check_fieldname_conflicts(doctype, fieldname): +def check_fieldname_conflicts(docfield): """Checks if fieldname conflicts with methods or properties""" - - doc = frappe.get_doc({"doctype": doctype}) + doc = frappe.get_doc({"doctype": docfield.dt}) available_objects = [x for x in dir(doc) if isinstance(x, str)] property_list = [ x for x in available_objects if isinstance(getattr(type(doc), x, None), property) @@ -1334,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname): method_list = [ x for x in available_objects if x not in property_list and callable(getattr(doc, x)) ] + msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname) - if fieldname in method_list + property_list: - frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) + if docfield.fieldname in method_list + property_list: + frappe.msgprint(msg, raise_exception=not docfield.is_virtual) def clear_linked_doctype_cache(): frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled') diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index d8b12d13b7..f09829a688 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,7 +1,7 @@ { "actions": [], "allow_import": 1, - "creation": "2022-01-25 14:14:01.217507", + "creation": "2013-01-10 16:34:01", "description": "Adds a custom field to a DocType", "doctype": "DocType", "document_type": "Setup", @@ -34,6 +34,7 @@ "non_negative", "reqd", "unique", + "is_virtual", "read_only", "ignore_user_permissions", "hidden", @@ -240,6 +241,12 @@ "fieldtype": "Check", "label": "Unique" }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, { "default": "0", "fieldname": "read_only", @@ -424,7 +431,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-01-29 15:42:21.885999", + "modified": "2022-02-14 15:42:21.885999", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 8f7b21dd24..cb1ea2c54d 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -54,7 +54,7 @@ class CustomField(Document): old_fieldtype = self.db_get('fieldtype') is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype) - if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype): + if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype): frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype)) if not self.fieldname: @@ -65,7 +65,7 @@ class CustomField(Document): if not self.flags.ignore_validate: from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts - check_fieldname_conflicts(self.dt, self.fieldname) + check_fieldname_conflicts(self) def on_update(self): if not frappe.flags.in_setup_wizard: diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 1593ed49a5..2ccfa87544 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -418,6 +418,9 @@ class CustomizeForm(Document): return property_value def validate_fieldtype_change(self, df, old_value, new_value): + if df.is_virtual: + return + allowed = self.allow_fieldtype_change(old_value, new_value) if allowed: old_value_length = cint(frappe.db.type_map.get(old_value)[1]) @@ -430,7 +433,8 @@ class CustomizeForm(Document): self.validate_fieldtype_length() else: self.flags.update_db = True - if not allowed: + + else: frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) def validate_fieldtype_length(self): @@ -558,7 +562,8 @@ docfield_properties = { 'allow_in_quick_entry': 'Check', 'hide_border': 'Check', 'hide_days': 'Check', - 'hide_seconds': 'Check' + 'hide_seconds': 'Check', + 'is_virtual': 'Check', } doctype_link_properties = { diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 93d1de0e63..1d721c57ec 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -14,6 +14,7 @@ "non_negative", "reqd", "unique", + "is_virtual", "in_list_view", "in_standard_filter", "in_global_search", @@ -115,6 +116,12 @@ "fieldtype": "Check", "label": "Unique" }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, { "default": "0", "fieldname": "in_list_view", @@ -436,7 +443,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-29 15:43:05.540546", + "modified": "2022-01-27 21:45:22.349776", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 0a65aa6f5d..a86cf5efd6 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import frappe @@ -18,53 +18,19 @@ class PropertySetter(Document): def validate(self): self.validate_fieldtype_change() + if self.is_new(): delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) - - # clear cache frappe.clear_cache(doctype = self.doc_type) def validate_fieldtype_change(self): - if self.field_name in not_allowed_fieldtype_change and \ - self.property == 'fieldtype': - frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name)) - - def get_property_list(self, dt): - return frappe.db.get_all('DocField', - fields=['fieldname', 'label', 'fieldtype'], - filters={ - 'parent': dt, - 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], - 'fieldname': ['!=', ''] - }, - order_by='label asc', - as_dict=1 - ) - - def get_setup_data(self): - return { - 'doctypes': frappe.get_all("DocType", pluck="name"), - 'dt_properties': self.get_property_list('DocType'), - 'df_properties': self.get_property_list('DocField') - } - - def get_field_ids(self): - return frappe.db.get_values( - "DocField", - filters={"parent": self.doc_type}, - fieldname=["name", "fieldtype", "label", "fieldname"], - as_dict=True, - ) - - def get_defaults(self): - if not self.field_name: - return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0] - else: - return frappe.db.get_values( - "DocField", - filters={"fieldname": self.field_name, "parent": self.doc_type}, - fieldname="*", - )[0] + if ( + self.property == 'fieldtype' + and self.field_name in not_allowed_fieldtype_change + ): + frappe.throw( + _("Field type cannot be changed for {0}").format(self.field_name) + ) def on_update(self): if frappe.flags.in_patch: @@ -74,6 +40,7 @@ class PropertySetter(Document): from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype validate_fields_for_doctype(self.doc_type) + def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False, validate_fields_for_doctype=True): # WARNING: Ignores Permissions @@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.insert() return property_setter + def delete_property_setter(doc_type, property, field_name=None, row_name=None): """delete other property setters on this, if this is new""" filters = dict(doc_type=doc_type, property=property) @@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None): filters["row_name"] = row_name frappe.db.delete('Property Setter', filters) - diff --git a/frappe/database/schema.py b/frappe/database/schema.py index dd54385c83..7cab8d42b2 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -67,7 +67,7 @@ class DBTable: """ get columns from docfields and custom fields """ - fields = self.meta.get_fieldnames_with_value(True) + fields = self.meta.get_fieldnames_with_value(with_field_meta=True) # optional fields like _comments if not self.meta.get('istable'): @@ -85,6 +85,9 @@ class DBTable: }) for field in fields: + if field.get("is_virtual"): + continue + self.columns[field.get('fieldname')] = DbColumn( self, field.get('fieldname'), diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 307d95e84b..8380161050 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -1,16 +1,14 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import datetime import frappe -import datetime from frappe import _ -from frappe.model import default_fields, table_fields, child_table_fields +from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module -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 import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html from frappe.utils.html_utils import unescape_html from frappe.model.docstatus import DocStatus @@ -254,7 +252,22 @@ class BaseDocument(object): continue df = self.meta.get_field(fieldname) - if df: + + if df and df.get("is_virtual"): + from frappe.utils.safe_exec import get_safe_globals + + if d[fieldname] is None: + if df.get("options"): + d[fieldname] = frappe.safe_eval( + code=df.get("options"), + eval_globals=get_safe_globals(), + eval_locals={"doc": self}, + ) + else: + _val = getattr(self, fieldname, None) + if _val and not callable(_val): + d[fieldname] = _val + elif df: if df.fieldtype=="Check": d[fieldname] = 1 if cint(d[fieldname]) else 0 @@ -328,6 +341,7 @@ class BaseDocument(object): def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False): doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc["doctype"] = self.doctype + for df in self.meta.get_table_fields(): children = self.get(df.fieldname) or [] doc[df.fieldname] = [ diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 372392f689..77d4de466f 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -444,9 +444,16 @@ class Meta(Document): self.permissions = [Document(d) for d in custom_perms] def get_fieldnames_with_value(self, with_field_meta=False): - return [df if with_field_meta else df.fieldname \ - for df in self.fields if df.fieldtype not in no_value_fields] + def is_value_field(docfield): + return not ( + docfield.get("is_virtual") + or docfield.fieldtype in no_value_fields + ) + if with_field_meta: + return [df for df in self.fields if is_value_field(df)] + + return [df.fieldname for df in self.fields if is_value_field(df)] def get_fields_to_check_permissions(self, user_permission_doctypes): fields = self.get("fields", { diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index ce871c50cb..4ee52d16b8 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl { if (this.df.get_status) { return this.df.get_status(this); } + if (this.df.is_virtual) { + return "Read"; + } if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { // like in case of a dialog box @@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl { if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console return "None"; - } else if (cint(this.df.read_only)) { + } else if (cint(this.df.read_only || this.df.is_virtual)) { // eslint-disable-next-line if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console return "Read"; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index e412b1dec8..86523d7088 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm { // prepare a list of mandatory, bold and allow in quick entry fields this.mandatory = fields.filter(df => { - return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only); + return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual); }); } diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index 6169fa75b8..29f1c86d17 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager { } function setup_add_fetch(df) { - if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image', - 'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1) - && df.fetch_from && df.fetch_from.indexOf(".")!=-1) { + let is_read_only_field = ( + ['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image', + 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) + || df.read_only == 1 + || df.is_virtual == 1 + ) + + if ( + is_read_only_field + && df.fetch_from + && df.fetch_from.indexOf(".") != -1 + ) { var parts = df.fetch_from.split("."); me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 3cde04313f..64960e0b09 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { frappe.model.is_value_type(field_doc) && field_doc.fieldtype !== "Read Only" && !field_doc.hidden && - !field_doc.read_only + !field_doc.read_only && + !field_doc.is_virtual ); }; diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index b324cecd39..1f3558b367 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) { '/': '/' }; const REGEX_SCRIPT = /)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14 + const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty. + // Rule 3 - TODO: Check event handlers? + // script and alert should be checked first or else it will be escaped + if (options.strategies.includes('js')) { + sanitised = sanitised.replace(REGEX_SCRIPT, ""); + sanitised = sanitised.replace(REGEX_ALERT, ""); + } + // Rule 1 if (options.strategies.includes('html')) { for (let char in HTML_ESCAPE_MAP) { @@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) { } } - // Rule 3 - TODO: Check event handlers? - if (options.strategies.includes('js')) { - sanitised = sanitised.replace(REGEX_SCRIPT, ""); - } - return sanitised; } diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 1291e63543..25c0c512ff 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -648,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { // not a cancelled doc && data.docstatus !== 2 && !df.read_only + && !df.is_virtual && !df.hidden // not a standard field i.e., owner, modified_by, etc. && !frappe.model.std_fields_list.includes(df.fieldname)) @@ -1029,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { title += ` (${__(doctype)})`; } - const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only; + const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual; const align = (() => { const is_numeric = frappe.model.is_numeric_field(docfield); diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 7179e4ab56..d5c6fb5e80 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView { this.ignore_fields = this.opts.ignore_fields || []; var mandatory_fields = $.map(me.opts.meta.fields, function(d) { - return (d.reqd || d.bold && !d.read_only) ? d : null }); + return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null }); var opts_field_names = this.fields.map(function(d) { return d.fieldname diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 34a1dd070c..a0c44c5c72 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -1,11 +1,20 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import os import unittest +from contextlib import contextmanager +from datetime import timedelta +from unittest.mock import patch import frappe -from frappe.utils import cint -from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series +from frappe.desk.doctype.note.note import Note +from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last +from frappe.utils import cint, now_datetime + + +class CustomTestNote(Note): + @property + def age(self): + return now_datetime() - self.creation class TestDocument(unittest.TestCase): @@ -256,4 +265,50 @@ class TestDocument(unittest.TestCase): def test_limit_for_get(self): doc = frappe.get_doc("DocType", "DocType") # assuming DocType has more that 3 Data fields - self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) \ No newline at end of file + self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) + + def test_virtual_fields(self): + """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked + """ + frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"}) + + def patch_note(): + return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}}) + + @contextmanager + def customize_note(with_options=False): + options = "frappe.utils.now_datetime() - doc.creation" if with_options else "" + custom_field = frappe.get_doc({ + "doctype": "Custom Field", + "dt": "Note", + "fieldname": "age", + "fieldtype": "Data", + "read_only": True, + "is_virtual": True, + "options": options, + }) + + try: + yield custom_field.insert(ignore_if_duplicate=True) + finally: + custom_field.delete() + + with patch_note(): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, CustomTestNote) + self.assertIsInstance(doc.age, timedelta) + self.assertIsNone(doc.as_dict().get("age")) + self.assertIsNone(doc.get_valid_dict().get("age")) + + with customize_note(), patch_note(): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, CustomTestNote) + self.assertIsInstance(doc.age, timedelta) + self.assertIsInstance(doc.as_dict().get("age"), timedelta) + self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) + + with customize_note(with_options=True): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, Note) + self.assertIsInstance(doc.as_dict().get("age"), timedelta) + self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index d69d21c64d..1f27b350be 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", { options: field.options, reqd: field.reqd, default: field.default, - read_only: field.read_only, + read_only: field.read_only || field.is_virtual, depends_on: field.depends_on, mandatory_depends_on: field.mandatory_depends_on, read_only_depends_on: field.read_only_depends_on,