diff --git a/.mergify.yml b/.mergify.yml
index 0bd9641d5b..63fe1a0086 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -48,3 +48,7 @@ pull_request_rules:
actions:
merge:
method: squash
+ commit_message_template: |
+ {{ title }} (#{{ number }})
+
+ {{ body }}
diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js
index 9d6eeaff64..fbff451305 100644
--- a/cypress/integration/workspace.js
+++ b/cypress/integration/workspace.js
@@ -23,7 +23,7 @@ context('Workspace 2.0', () => {
// check if sidebar item is added in pubic section
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
- cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
+ cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
@@ -67,7 +67,7 @@ context('Workspace 2.0', () => {
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
cy.get(".ce-block:last").should('have.class', 'col-xs-12');
- cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
+ cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
});
it('Delete Private Page', () => {
@@ -80,7 +80,7 @@ context('Workspace 2.0', () => {
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
cy.wait(300);
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
- cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
+ cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.get('.codex-editor__redactor .ce-block');
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 3558603454..8a8b70afe3 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -358,7 +358,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
response JSON and shown in a pop-up / modal.
:param msg: Message.
- :param title: [optional] Message title.
+ :param title: [optional] Message title. Default: "Message".
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
@@ -395,8 +395,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
if flags.print_messages and out.message:
print(f"Message: {strip_html_tags(out.message)}")
- if title:
- out.title = title
+ out.title = title or _("Message", context="Default title of the message dialog")
if not indicator and raise_exception:
indicator = 'red'
diff --git a/frappe/app.py b/frappe/app.py
index 609a8535d7..975a2e2002 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -294,7 +294,6 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
_sites_path = sites_path
from werkzeug.serving import run_simple
- patch_werkzeug_reloader()
if profile or os.environ.get('USE_PROFILER'):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
@@ -325,23 +324,3 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
use_debugger=not in_test_env,
use_evalex=not in_test_env,
threaded=not no_threading)
-
-def patch_werkzeug_reloader():
- """
- This function monkey patches Werkzeug reloader to ignore reloading files in
- the __pycache__ directory.
-
- To be deprecated when upgrading to Werkzeug 2.
- """
-
- from werkzeug._reloader import WatchdogReloaderLoop
-
- trigger_reload = WatchdogReloaderLoop.trigger_reload
-
- def custom_trigger_reload(self, filename):
- if os.path.basename(os.path.dirname(filename)) == "__pycache__":
- return
-
- return trigger_reload(self, filename)
-
- WatchdogReloaderLoop.trigger_reload = custom_trigger_reload
diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py
index cd9af498aa..33672a7dea 100644
--- a/frappe/core/doctype/comment/test_comment.py
+++ b/frappe/core/doctype/comment/test_comment.py
@@ -70,6 +70,19 @@ class TestComment(unittest.TestCase):
reference_name = test_blog.name
))), 0)
+ # test for filtering html and css injection elements
+ frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
+
+ frappe.form_dict.comment = 'Comment'
+ frappe.form_dict.comment_by = 'hacker'
+
+ add_comment()
+
+ self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict(
+ reference_doctype = test_blog.doctype,
+ reference_name = test_blog.name
+ ))[0]['content'], 'Comment')
+
test_blog.delete()
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index 1ab07d92e4..f89f0d8765 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -2,6 +2,7 @@
# License: MIT. See LICENSE
from collections import Counter
+from typing import List
import frappe
from frappe import _
from frappe.model.document import Document
@@ -367,15 +368,8 @@ def get_permission_query_conditions_for_communication(user):
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
-def get_contacts(email_strings, auto_create_contact=False):
- email_addrs = []
-
- for email_string in email_strings:
- if email_string:
- result = getaddresses([email_string])
- for email in result:
- email_addrs.append(email[1])
-
+def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
+ email_addrs = get_emails(email_strings)
contacts = []
for email in email_addrs:
email = get_email_without_link(email)
@@ -404,6 +398,17 @@ def get_contacts(email_strings, auto_create_contact=False):
return contacts
+def get_emails(email_strings: List[str]) -> List[str]:
+ email_addrs = []
+
+ for email_string in email_strings:
+ if email_string:
+ result = getaddresses([email_string])
+ for email in result:
+ email_addrs.append(email[1])
+
+ return email_addrs
+
def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_all("Dynamic Link", filters={
"parenttype": "Contact",
@@ -449,8 +454,12 @@ def get_email_without_link(email):
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email
- email_id = email.split("@")[0].split("+")[0]
- email_host = email.split("@")[1]
+ try:
+ _email = email.split("@")
+ email_id = _email[0].split("+")[0]
+ email_host = _email[1]
+ except IndexError:
+ return email
return "{0}@{1}".format(email_id, email_host)
diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py
index f26e70771b..d933c2f494 100644
--- a/frappe/core/doctype/communication/test_communication.py
+++ b/frappe/core/doctype/communication/test_communication.py
@@ -5,6 +5,7 @@ from urllib.parse import quote
import frappe
from frappe.email.doctype.email_queue.email_queue import EmailQueue
+from frappe.core.doctype.communication.communication import get_emails
test_records = frappe.get_test_records('Communication')
@@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase):
self.assertIn(("Note", note.name), doc_links)
+ def parse_emails(self):
+ emails = get_emails(
+ [
+ 'comm_recipient+DocType+DocName@example.com',
+ '"First, LastName" ',
+ 'test@user.com'
+ ]
+ )
+
+ self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com")
+ self.assertEqual(emails[1], "first.lastname@email.com")
+ self.assertEqual(emails[2], "test@user.com")
+
class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or [])
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index 26ddce7d35..6eb8cf347f 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-03 11:56:19.812863",
+ "modified": "2022-01-27 21:22:20.529072",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index d259367a16..6d0409521e 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -781,29 +781,31 @@ def validate_series(dt, autoname=None, name=None):
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
- if frappe.flags.in_patch: return
- if frappe.flags.in_fixtures: return
- if not meta.links: return
+ if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
+ return
- for index, link in enumerate(meta.links):
+ fieldnames = tuple(field.fieldname for field in meta.fields)
+ for index, link in enumerate(meta.links, 1):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
- message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
- if link.is_child_table and not meta.get_field(link.table_fieldname):
- message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
+ if not link.is_child_table:
+ continue
+
+ if not link.parent_doctype:
+ message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index)
+ frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
+
+ if not link.table_fieldname:
+ message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
+ frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
+
+ if link.table_fieldname not in fieldnames:
+ message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
- if link.is_child_table:
- if not link.parent_doctype:
- message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
- frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
-
- if not link.table_fieldname:
- message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
- frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
-
def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False)
validate_links_table_fieldnames(meta)
@@ -1076,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))
@@ -1321,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)
@@ -1332,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/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index ba83dfca19..d8e748a518 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -406,7 +406,7 @@ class TestFile(unittest.TestCase):
test_file.reload()
test_file.file_url = frappe.utils.get_url('unknown.jpg')
test_file.make_thumbnail(suffix="xs")
- self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"})
+ self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
self.assertEquals(test_file.thumbnail_url, None)
def test_file_unzip(self):
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 36e3b09254..5a22304f32 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -4,7 +4,9 @@
import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
+from frappe.desk.reportview import delete_report, save_report as _save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
+from frappe.core.doctype.user_permission.test_user_permission import create_user
test_records = frappe.get_test_records('Report')
test_dependencies = ['User']
@@ -30,6 +32,60 @@ class TestReport(unittest.TestCase):
self.assertEqual(columns[1].get('label'), 'Module')
self.assertTrue('User' in [d.get('name') for d in data])
+ def test_save_or_delete_report(self):
+ '''Test for validations when editing / deleting report of type Report Builder'''
+
+ try:
+ report = frappe.get_doc({
+ 'doctype': 'Report',
+ 'ref_doctype': 'User',
+ 'report_name': 'Test Delete Report',
+ 'report_type': 'Report Builder',
+ 'is_standard': 'No',
+ }).insert()
+
+ # Check for PermissionError
+ create_user("test_report_owner@example.com", "Website Manager")
+ frappe.set_user("test_report_owner@example.com")
+ self.assertRaises(frappe.PermissionError, delete_report, report.name)
+
+ # Check for Report Type
+ frappe.set_user("Administrator")
+ report.db_set("report_type", "Custom Report")
+ self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Only reports of type Report Builder can be deleted",
+ delete_report,
+ report.name
+ )
+
+ # Check if creating and deleting works with proper validations
+ frappe.set_user("test@example.com")
+ report_name = _save_report(
+ 'Dummy Report',
+ 'User',
+ json.dumps([{
+ 'fieldname': 'email',
+ 'fieldtype': 'Data',
+ 'label': 'Email',
+ 'insert_after_index': 0,
+ 'link_field': 'name',
+ 'doctype': 'User',
+ 'options': 'Email',
+ 'width': 100,
+ 'id':'email',
+ 'name': 'Email'
+ }])
+ )
+
+ doc = frappe.get_doc("Report", report_name)
+ delete_report(doc.name)
+
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.rollback()
+
+
def test_custom_report(self):
reset_customization('User')
custom_report_name = save_report(
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 2d3da791ff..4676e9daa8 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -359,6 +359,7 @@ class TestUser(unittest.TestCase):
json.loads(frappe.message_log[0]).get("message"),
"Password reset instructions have been sent to your email"
)
+
sendmail.assert_called_once()
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 235f11aad8..e51dfda14b 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -1,458 +1,468 @@
{
- "actions": [],
- "allow_import": 1,
- "creation": "2013-01-10 16:34:01",
- "description": "Adds a custom field to a DocType",
- "doctype": "DocType",
- "document_type": "Setup",
- "engine": "InnoDB",
- "field_order": [
- "dt",
- "module",
- "label",
- "label_help",
- "fieldname",
- "insert_after",
- "length",
- "column_break_6",
- "fieldtype",
- "precision",
- "hide_seconds",
- "hide_days",
- "options",
- "fetch_from",
- "fetch_if_empty",
- "options_help",
- "section_break_11",
- "collapsible",
- "collapsible_depends_on",
- "default",
- "depends_on",
- "mandatory_depends_on",
- "read_only_depends_on",
- "properties",
- "non_negative",
- "reqd",
- "unique",
- "read_only",
- "ignore_user_permissions",
- "hidden",
- "print_hide",
- "print_hide_if_no_value",
- "print_width",
- "no_copy",
- "allow_on_submit",
- "in_list_view",
- "in_standard_filter",
- "in_global_search",
- "in_preview",
- "bold",
- "report_hide",
- "search_index",
- "allow_in_quick_entry",
- "ignore_xss_filter",
- "translatable",
- "hide_border",
- "description",
- "permlevel",
- "width",
- "columns"
- ],
- "fields": [{
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
- },
- {
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
- },
- {
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
- },
- {
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
- "reqd": 1
- },
- {
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
- },
- {
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
- },
- {
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
- },
- {
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
- },
- {
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
- },
- {
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
- },
- {
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
- },
- {
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
- },
- {
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
- },
- {
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
- },
- {
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
- },
- {
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
- },
- {
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
- },
- {
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
- },
- {
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
- },
- {
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
- },
- {
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
- },
- {
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
- },
- {
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
- },
- {
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
- },
- {
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
- },
- {
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
- },
- {
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
- },
- {
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
- },
- {
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
- },
- {
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
- },
- {
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
- },
- {
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
- },
- {
- "default": "0",
- "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_seconds",
- "fieldtype": "Check",
- "label": "Hide Seconds"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Duration'",
- "fieldname": "hide_days",
- "fieldtype": "Check",
- "label": "Hide Days"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.fieldtype=='Section Break'",
- "fieldname": "hide_border",
- "fieldtype": "Check",
- "label": "Hide Border"
- },
- {
- "default": "0",
- "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
- "fieldname": "non_negative",
- "fieldtype": "Check",
- "label": "Non Negative"
- },
- {
- "fieldname": "module",
- "fieldtype": "Link",
- "label": "Module (for export)",
- "options": "Module Def"
- }
- ],
- "icon": "fa fa-glass",
- "idx": 1,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-09-04 12:45:23.810120",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Custom Field",
- "owner": "Administrator",
- "permissions": [{
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "dt,label,fieldtype,options",
- "sort_field": "modified",
- "sort_order": "ASC",
- "track_changes": 1
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:01",
+ "description": "Adds a custom field to a DocType",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "dt",
+ "module",
+ "label",
+ "label_help",
+ "fieldname",
+ "insert_after",
+ "length",
+ "column_break_6",
+ "fieldtype",
+ "precision",
+ "hide_seconds",
+ "hide_days",
+ "options",
+ "fetch_from",
+ "fetch_if_empty",
+ "options_help",
+ "section_break_11",
+ "collapsible",
+ "collapsible_depends_on",
+ "default",
+ "depends_on",
+ "mandatory_depends_on",
+ "read_only_depends_on",
+ "properties",
+ "non_negative",
+ "reqd",
+ "unique",
+ "is_virtual",
+ "read_only",
+ "ignore_user_permissions",
+ "hidden",
+ "print_hide",
+ "print_hide_if_no_value",
+ "print_width",
+ "no_copy",
+ "allow_on_submit",
+ "in_list_view",
+ "in_standard_filter",
+ "in_global_search",
+ "in_preview",
+ "bold",
+ "report_hide",
+ "search_index",
+ "allow_in_quick_entry",
+ "ignore_xss_filter",
+ "translatable",
+ "hide_border",
+ "description",
+ "permlevel",
+ "width",
+ "columns"
+ ],
+ "fields": [
+ {
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ },
+ {
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
+ },
+ {
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
+ },
+ {
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
+ },
+ {
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
+ },
+ {
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
+ },
+ {
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
+ },
+ {
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
+ },
+ {
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
+ },
+ {
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
+ },
+ {
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_virtual",
+ "fieldtype": "Check",
+ "label": "Is Virtual"
+ },
+ {
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
+ },
+ {
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
+ },
+ {
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
+ },
+ {
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
+ },
+ {
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
+ },
+ {
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
+ },
+ {
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
+ },
+ {
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
+ },
+ {
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_seconds",
+ "fieldtype": "Check",
+ "label": "Hide Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Duration'",
+ "fieldname": "hide_days",
+ "fieldtype": "Check",
+ "label": "Hide Days"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for export)",
+ "options": "Module Def"
+ }
+ ],
+ "icon": "fa fa-glass",
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2022-01-27 21:47:01.065556",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "dt,label,fieldtype,options",
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": [],
+ "track_changes": 1
}
\ No newline at end of file
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 a545cd9fe1..4351e76609 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-03 14:50:32.035768",
+ "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/database.py b/frappe/database/database.py
index 9fa1ff161c..c833bdeed3 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -177,6 +177,8 @@ class Database(object):
raise frappe.QueryTimeoutError(e)
elif frappe.conf.db_type == 'postgres':
+ # TODO: added temporarily
+ print(e)
raise
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
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/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 6a7c736fac..d6390d7613 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -15,12 +15,12 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
- const name = await get_first_document(frm.doc.reference_doctype);
let route_changed = null;
-
+
if (issingle) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
} else if (frm.doc.first_document) {
+ const name = await get_first_document(frm.doc.reference_doctype);
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
@@ -53,73 +53,69 @@ frappe.ui.form.on('Form Tour', {
};
});
- frm.set_query("field", "steps", function() {
- return {
- query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
- filters: {
- doctype: frm.doc.reference_doctype,
- hidden: 0
- }
- };
- });
-
- frm.set_query("parent_field", "steps", function() {
- return {
- query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
- filters: {
- doctype: frm.doc.reference_doctype,
- fieldtype: "Table",
- hidden: 0,
- }
- };
- });
-
frm.trigger('reference_doctype');
},
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
- frappe.db.get_list('DocField', {
- filters: {
- parent: frm.doc.reference_doctype,
- parenttype: 'DocType',
- fieldtype: 'Table'
- },
- fields: ['options']
- }).then(res => {
- if (Array.isArray(res)) {
- frm.child_doctypes = res.map(r => r.options);
- }
+ frm.set_fields_as_options(
+ "fieldname",
+ frm.doc.reference_doctype,
+ df => !df.hidden
+ ).then(options => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "fieldname",
+ "options",
+ [""].concat(options)
+ );
+ });
+
+ frm.set_fields_as_options(
+ 'parent_fieldname',
+ frm.doc.reference_doctype,
+ (df) => df.fieldtype == "Table" && !df.hidden,
+ ).then(options => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "parent_fieldname",
+ "options",
+ [""].concat(options)
+ );
});
}
});
frappe.ui.form.on('Form Tour Step', {
- parent_field(frm, cdt, cdn) {
+ form_render(frm, cdt, cdn) {
+ if (locals[cdt][cdn].is_table_field) {
+ frm.trigger('parent_fieldname', cdt, cdn);
+ }
+ },
+ parent_fieldname(frm, cdt, cdn) {
const child_row = locals[cdt][cdn];
- frappe.model.set_value(cdt, cdn, 'field', '');
- const field_control = get_child_field("steps", cdn, "field");
- field_control.get_query = function() {
- return {
- query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list",
- filters: {
- doctype: child_row.child_doctype,
- hidden: 0
- }
- };
- };
+
+ const parent_fieldname_df = frappe
+ .get_meta(frm.doc.reference_doctype)
+ .fields.find(df => df.fieldname == child_row.parent_fieldname);
+
+ frm.set_fields_as_options(
+ 'fieldname',
+ parent_fieldname_df.options,
+ (df) => !df.hidden,
+ ).then(options => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "fieldname",
+ "options",
+ [""].concat(options)
+ );
+ if (child_row.fieldname) {
+ frappe.model.set_value(cdt, cdn, 'fieldname', child_row.fieldname);
+ }
+ });
}
});
-function get_child_field(child_table, child_name, fieldname) {
- // gets the field from grid row form
- const grid = cur_frm.fields_dict[child_table].grid;
- const grid_row = grid.grid_rows_by_docname[child_name];
- return grid_row.grid_form.fields_dict[fieldname];
-}
-
async function check_if_single(doctype) {
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
return message.issingle || 0;
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 82d47224dd..6248b43e62 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -5,58 +5,23 @@ import frappe
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
+
class FormTour(Document):
- def before_insert(self):
- if not self.is_standard:
- return
+ def before_save(self):
+ meta = frappe.get_meta(self.reference_doctype)
+ for step in self.steps:
+ if step.is_table_field and step.parent_fieldname:
+ parent_field_df = meta.get_field(step.parent_fieldname)
+ step.child_doctype = parent_field_df.options
- # while syncing, set proper docfield reference
- for d in self.steps:
- if not frappe.db.exists('DocField', d.field):
- d.field = frappe.db.get_value('DocField', {
- 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype
- }, "name")
-
- if d.is_table_field and not frappe.db.exists('DocField', d.parent_field):
- d.parent_field = frappe.db.get_value('DocField', {
- 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table'
- }, "name")
+ field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
+ step.label = field_df.label
+ step.fieldtype = field_df.fieldtype
+ else:
+ field_df = meta.get_field(step.fieldname)
+ step.label = field_df.label
+ step.fieldtype = field_df.fieldtype
def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
- export_to_files([['Form Tour', self.name]], self.module)
-
- def before_export(self, doc):
- for d in doc.steps:
- d.field = ""
- d.parent_field = ""
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def get_docfield_list(doctype, txt, searchfield, start, page_len, filters):
- or_filters = [
- ['fieldname', 'like', '%' + txt + '%'],
- ['label', 'like', '%' + txt + '%'],
- ['fieldtype', 'like', '%' + txt + '%']
- ]
-
- parent_doctype = filters.get('doctype')
- fieldtype = filters.get('fieldtype')
- if not fieldtype:
- excluded_fieldtypes = ['Column Break']
- excluded_fieldtypes += filters.get('excluded_fieldtypes', [])
- fieldtype_filter = ['not in', excluded_fieldtypes]
- else:
- fieldtype_filter = fieldtype
-
- docfields = frappe.get_all(
- doctype,
- fields=["name as value", "label", "fieldtype"],
- filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter},
- or_filters=or_filters,
- limit_start=start,
- limit_page_length=page_len,
- order_by="idx",
- as_list=1,
- )
- return docfields
+ export_to_files([["Form Tour", self.name]], self.module)
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
index 3b6c91a208..7eb6eab223 100644
--- a/frappe/desk/doctype/form_tour_step/form_tour_step.json
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -6,19 +6,17 @@
"field_order": [
"is_table_field",
"section_break_2",
- "parent_field",
- "field",
+ "parent_fieldname",
+ "fieldname",
"title",
"description",
"column_break_2",
"position",
"label",
+ "fieldtype",
"has_next_condition",
"next_step_condition",
"section_break_13",
- "fieldname",
- "parent_fieldname",
- "fieldtype",
"child_doctype"
],
"fields": [
@@ -38,23 +36,13 @@
"reqd": 1
},
{
- "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))",
- "fieldname": "field",
- "fieldtype": "Link",
- "label": "Field",
- "options": "DocField",
+ "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))",
+ "fieldname": "fieldname",
+ "fieldtype": "Select",
+ "label": "Fieldname",
"reqd": 1
},
{
- "fetch_from": "field.fieldname",
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Fieldname",
- "read_only": 1
- },
- {
- "fetch_from": "field.label",
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
@@ -88,10 +76,8 @@
},
{
"default": "0",
- "fetch_from": "field.fieldtype",
"fieldname": "fieldtype",
"fieldtype": "Data",
- "hidden": 1,
"label": "Fieldtype",
"read_only": 1
},
@@ -105,14 +91,6 @@
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
- {
- "depends_on": "is_table_field",
- "fieldname": "parent_field",
- "fieldtype": "Link",
- "label": "Parent Field",
- "mandatory_depends_on": "is_table_field",
- "options": "DocField"
- },
{
"fieldname": "section_break_13",
"fieldtype": "Section Break",
@@ -120,7 +98,6 @@
"label": "Hidden Fields"
},
{
- "fetch_from": "parent_field.options",
"fieldname": "child_doctype",
"fieldtype": "Data",
"hidden": 1,
@@ -128,18 +105,17 @@
"read_only": 1
},
{
- "fetch_from": "parent_field.fieldname",
+ "depends_on": "is_table_field",
"fieldname": "parent_fieldname",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Parent Fieldname",
- "read_only": 1
+ "fieldtype": "Select",
+ "label": "Parent Field",
+ "mandatory_depends_on": "is_table_field"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-06-06 20:52:21.076972",
+ "modified": "2022-01-27 15:18:36.481801",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",
@@ -147,5 +123,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 211029dfcf..fa8b81f5fd 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -20,13 +20,13 @@
"hide_custom",
"public",
"content",
- "section_break_2",
+ "tab_break_2",
"charts",
- "section_break_15",
+ "tab_break_15",
"shortcuts",
- "section_break_18",
+ "tab_break_18",
"links",
- "roles_section",
+ "roles_tab",
"roles"
],
"fields": [
@@ -40,8 +40,8 @@
{
"collapsible": 1,
"collapsible_depends_on": "charts",
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
+ "fieldname": "tab_break_2",
+ "fieldtype": "Tab Break",
"label": "Dashboards"
},
{
@@ -78,15 +78,15 @@
{
"collapsible": 1,
"collapsible_depends_on": "shortcuts",
- "fieldname": "section_break_15",
- "fieldtype": "Section Break",
+ "fieldname": "tab_break_15",
+ "fieldtype": "Tab Break",
"label": "Shortcuts"
},
{
"collapsible": 1,
"collapsible_depends_on": "links",
- "fieldname": "section_break_18",
- "fieldtype": "Section Break",
+ "fieldname": "tab_break_18",
+ "fieldtype": "Tab Break",
"label": "Link Cards"
},
{
@@ -152,14 +152,14 @@
"options": "Has Role"
},
{
- "fieldname": "roles_section",
- "fieldtype": "Section Break",
+ "fieldname": "roles_tab",
+ "fieldtype": "Tab Break",
"label": "Roles"
}
],
"in_create": 1,
"links": [],
- "modified": "2021-12-15 19:33:00.805265",
+ "modified": "2022-01-27 12:06:13.111743",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index b42d8c58b7..0c32e886f4 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -6,7 +6,6 @@ from frappe.utils import strip, cint
from frappe.translate import (set_default_language, get_dict, send_translations)
from frappe.geo.country_info import get_country_info
from frappe.utils.password import update_password
-from werkzeug.useragents import UserAgent
from . import install_fixtures
def get_setup_stages(args):
@@ -315,17 +314,10 @@ def prettify_args(args):
return pretty_args
def email_setup_wizard_exception(traceback, args):
- if not frappe.local.conf.setup_wizard_exception_email:
+ if not frappe.conf.setup_wizard_exception_email:
return
pretty_args = prettify_args(args)
-
- if frappe.local.request:
- user_agent = UserAgent(frappe.local.request.headers.get('User-Agent', ''))
-
- else:
- user_agent = frappe._dict()
-
message = """
#### Traceback
@@ -349,18 +341,15 @@ def email_setup_wizard_exception(traceback, args):
#### Basic Information
- **Site:** {site}
-- **User:** {user}
-- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language}
-- **Browser Languages**: `{accept_languages}`""".format(
+- **User:** {user}""".format(
site=frappe.local.site,
traceback=traceback,
args="\n".join(pretty_args),
user=frappe.session.user,
- user_agent=user_agent,
- headers=frappe.local.request.headers,
- accept_languages=", ".join(frappe.local.request.accept_languages.values()))
+ headers=frappe.request.headers,
+ )
- frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email,
+ frappe.sendmail(recipients=frappe.conf.setup_wizard_exception_email,
sender=frappe.session.user,
subject="Setup failed: {}".format(frappe.local.site),
message=message,
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index c45fc9bfdd..b0e1f901aa 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -262,22 +262,66 @@ def compress(data, args=None):
}
@frappe.whitelist()
-def save_report():
- """save report"""
+def save_report(name, doctype, report_settings):
+ """Save reports of type Report Builder from Report View"""
- data = frappe.local.form_dict
- if frappe.db.exists('Report', data['name']):
- d = frappe.get_doc('Report', data['name'])
+ if frappe.db.exists('Report', name):
+ report = frappe.get_doc('Report', name)
+ if report.is_standard == "Yes":
+ frappe.throw(_("Standard Reports cannot be edited"))
+
+ if report.report_type != "Report Builder":
+ frappe.throw(_("Only reports of type Report Builder can be edited"))
+
+ if (
+ report.owner != frappe.session.user
+ and not frappe.has_permission("Report", "write")
+ ):
+ frappe.throw(
+ _("Insufficient Permissions for editing Report"),
+ frappe.PermissionError
+ )
else:
- d = frappe.new_doc('Report')
- d.report_name = data['name']
- d.ref_doctype = data['doctype']
+ report = frappe.new_doc('Report')
+ report.report_name = name
+ report.ref_doctype = doctype
- d.report_type = "Report Builder"
- d.json = data['json']
- frappe.get_doc(d).save()
- frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
- return d.name
+ report.report_type = "Report Builder"
+ report.json = report_settings
+ report.save(ignore_permissions=True)
+ frappe.msgprint(
+ _("Report {0} saved").format(frappe.bold(report.name)),
+ indicator="green",
+ alert=True,
+ )
+ return report.name
+
+@frappe.whitelist()
+def delete_report(name):
+ """Delete reports of type Report Builder from Report View"""
+
+ report = frappe.get_doc("Report", name)
+ if report.is_standard == "Yes":
+ frappe.throw(_("Standard Reports cannot be deleted"))
+
+ if report.report_type != "Report Builder":
+ frappe.throw(_("Only reports of type Report Builder can be deleted"))
+
+ if (
+ report.owner != frappe.session.user
+ and not frappe.has_permission("Report", "delete")
+ ):
+ frappe.throw(
+ _("Insufficient Permissions for deleting Report"),
+ frappe.PermissionError
+ )
+
+ report.delete(ignore_permissions=True)
+ frappe.msgprint(
+ _("Report {0} deleted").format(frappe.bold(report.name)),
+ indicator="green",
+ alert=True,
+ )
@frappe.whitelist()
@frappe.read_only()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 307d95e84b..ea4264212b 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] = [
@@ -375,12 +389,24 @@ class BaseDocument(object):
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
return fieldname[0] if fieldname else None
- def db_insert(self):
- """INSERT the document (with valid columns) in the database."""
+ def db_insert(self, ignore_if_duplicate=False):
+ """INSERT the document (with valid columns) in the database.
+
+ args:
+ ignore_if_duplicate: ignore primary key collision
+ at database level (postgres)
+ in python (mariadb)
+ """
if not self.name:
# name will be set by document class in most cases
set_new_name(self)
+ conflict_handler = ""
+ # On postgres we can't implcitly ignore PK collision
+ # So instruct pg to ignore `name` field conflicts
+ if ignore_if_duplicate and frappe.db.db_type == "postgres":
+ conflict_handler = "on conflict (name) do nothing"
+
if not self.creation:
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
@@ -391,10 +417,11 @@ class BaseDocument(object):
columns = list(d)
try:
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
- VALUES ({values})""".format(
- doctype = self.doctype,
- columns = ", ".join("`"+c+"`" for c in columns),
- values = ", ".join(["%s"] * len(columns))
+ VALUES ({values}) {conflict_handler}""".format(
+ doctype=self.doctype,
+ columns=", ".join("`"+c+"`" for c in columns),
+ values=", ".join(["%s"] * len(columns)),
+ conflict_handler=conflict_handler
), list(d.values()))
except Exception as e:
if frappe.db.is_primary_key_violation(e):
@@ -407,8 +434,11 @@ class BaseDocument(object):
self.db_insert()
return
- frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
- raise frappe.DuplicateEntryError(self.doctype, self.name, e)
+ if not ignore_if_duplicate:
+ frappe.msgprint(_("{0} {1} already exists")
+ .format(self.doctype, frappe.bold(self.name)),
+ title=_("Duplicate Name"), indicator="red")
+ raise frappe.DuplicateEntryError(self.doctype, self.name, e)
elif frappe.db.is_unique_key_violation(e):
# unique constraint
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 66a0cef7dd..cb36c18b47 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -249,11 +249,7 @@ class Document(BaseDocument):
if getattr(self.meta, "issingle", 0):
self.update_single(self.get_valid_dict())
else:
- try:
- self.db_insert()
- except frappe.DuplicateEntryError as e:
- if not ignore_if_duplicate:
- raise e
+ self.db_insert(ignore_if_duplicate=ignore_if_duplicate)
# children
for d in self.get_all_children():
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/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index df4dbf09e7..6e3dd3eb0b 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -549,14 +549,14 @@ frappe.ui.form.Dashboard = class FormDashboard {
render_graph(args) {
this.chart_area.show();
this.chart_area.body.empty();
- $.extend({
+ $.extend(args, {
type: 'line',
colors: ['green'],
truncateLegends: 1,
axisOptions: {
shortenYAxisNumbers: 1
}
- }, args);
+ });
this.show();
this.chart = new frappe.Chart('.form-graph', args);
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 7fefb59ac6..2bb888e17c 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -151,7 +151,7 @@ frappe.ui.form.FormTour = class FormTour {
const curr_step = step_info;
const next_step = this.tour.steps[curr_step.idx];
- const is_next_field_in_curr_table = next_step.parent_field == curr_step.field;
+ const is_next_field_in_curr_table = next_step.parent_fieldname == curr_step.fieldname;
if (!is_next_field_in_curr_table) return;
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/ui/messages.js b/frappe/public/js/frappe/ui/messages.js
index ac0c01c406..f0d03f0743 100644
--- a/frappe/public/js/frappe/ui/messages.js
+++ b/frappe/public/js/frappe/ui/messages.js
@@ -233,7 +233,7 @@ frappe.msgprint = function(msg, title, is_minimizable) {
if(data.title || !msg_exists) {
// set title only if it is explicitly given
// and no existing title exists
- frappe.msg_dialog.set_title(data.title || __('Message'));
+ frappe.msg_dialog.set_title(data.title || __('Message', null, 'Default title of the message dialog'));
}
// show / hide indicator
diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js
index 6fa8303574..bf1cee2cbf 100644
--- a/frappe/public/js/frappe/ui/notifications/notifications.js
+++ b/frappe/public/js/frappe/ui/notifications/notifications.js
@@ -283,12 +283,13 @@ class NotificationsView extends BaseNotificationsView {
e.stopImmediatePropagation();
this.mark_as_read(field.name, item_html);
});
-
- item_html.on('click', () => {
- this.mark_as_read(field.name, item_html);
- });
}
+ item_html.on('click', () => {
+ !field.read && this.mark_as_read(field.name, item_html);
+ this.notifications_icon.trigger('click');
+ });
+
return item_html;
}
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 = /