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/control_link.js b/cypress/integration/control_link.js
index bfa70ad338..7a7e94d2f5 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -95,6 +95,51 @@ context('Control Link', () => {
});
});
+ it('show title field in link', () => {
+ get_dialog_with_link().as('dialog');
+
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "ToDo",
+ "property": "show_title_field_in_link",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "1"
+ }, true);
+
+ cy.window().its('frappe').then(frappe => {
+ if (!frappe.boot) {
+ frappe.boot = {
+ link_title_doctypes: ['ToDo']
+ };
+ } else {
+ frappe.boot.link_title_doctypes = ['ToDo'];
+ }
+ });
+
+ cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
+
+ cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
+ cy.wait('@search_link');
+ cy.get('@input').type('todo for link');
+ cy.wait('@search_link');
+ cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
+ cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
+ cy.get('.frappe-control[data-fieldname=link] input').blur();
+ cy.get('@dialog').then(dialog => {
+ cy.get('@todos').then(todos => {
+ let field = dialog.get_field('link');
+ let value = field.get_value();
+ let label = field.get_label_value();
+
+ expect(value).to.eq(todos[0]);
+ expect(label).to.eq('this is a test todo for link');
+
+ cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
+ });
+ });
+ });
+
it('should update dependant fields (via fetch_from)', () => {
cy.get('@todos').then(todos => {
cy.visit(`/app/todo/${todos[0]}`);
diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js
index 0253e8fd43..5a0b13a3a0 100644
--- a/cypress/integration/report_view.js
+++ b/cypress/integration/report_view.js
@@ -11,30 +11,60 @@ context('Report View', () => {
'title': 'Doc 1',
'description': 'Random Text',
'enabled': 0,
- // submit document
- 'docstatus': 1
- }, true).as('doc');
+ 'docstatus': 1 // submit document
+ }, true);
+ return cy.window().its('frappe').then(frappe => {
+ return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
+ });
});
+
it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`);
+
// check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
+
// select the cell
cell.dblclick();
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
+
cy.wait('@value-update');
- cy.get('@doc').then(doc => {
- cy.call('frappe.client.get_value', {
- doctype: doc.doctype,
- filters: {
- name: doc.name,
- },
- fieldname: 'enabled'
- }).then(r => {
- expect(r.message.enabled).to.equals(1);
- });
+
+ cy.call('frappe.client.get_value', {
+ doctype: doctype_name,
+ filters: {
+ title: 'Doc 1',
+ },
+ fieldname: 'enabled'
+ }).then(r => {
+ expect(r.message.enabled).to.equals(1);
});
});
+
+ it('test load more with count selection buttons', () => {
+ cy.visit('/app/contact/view/report');
+
+ cy.clear_filters();
+ cy.get('.list-paging-area .list-count').should('contain.text', '20 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '40 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '60 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '100 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '200 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '300 of');
+
+ cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();
+
+ cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
+ cy.get('.list-paging-area .btn-more').click();
+ cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
+ });
});
\ No newline at end of file
diff --git a/frappe/api.py b/frappe/api.py
index e7f7bf5a04..226853c47b 100644
--- a/frappe/api.py
+++ b/frappe/api.py
@@ -159,7 +159,10 @@ def get_request_form_data():
else:
data = frappe.local.form_dict.data
- return frappe.parse_json(data)
+ try:
+ return frappe.parse_json(data)
+ except ValueError:
+ return frappe.local.form_dict
def validate_auth():
@@ -208,7 +211,6 @@ def validate_oauth(authorization_header):
pass
-
def validate_auth_via_api_keys(authorization_header):
"""
Authenticate request using API keys and set session user
diff --git a/frappe/boot.py b/frappe/boot.py
index 7f62d96cae..524913059c 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -89,6 +89,7 @@ def get_bootinfo():
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
+ bootinfo.link_title_doctypes = get_link_title_doctypes()
return bootinfo
@@ -324,6 +325,15 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
+def get_link_title_doctypes():
+ dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
+ custom_dts = frappe.get_all(
+ "Property Setter",
+ {"property": "show_title_field_in_link", "value": "1"},
+ ["doc_type as name"],
+ )
+ return [d.name for d in dts + custom_dts if d]
+
def set_time_zone(bootinfo):
bootinfo.time_zone = {
"system": get_time_zone(),
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.json b/frappe/core/doctype/doctype/doctype.json
index 03e3b65ea1..2bba4127bb 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -46,6 +46,7 @@
"allow_auto_repeat",
"view_settings",
"title_field",
+ "show_title_field_in_link",
"search_fields",
"default_print_format",
"sort_field",
@@ -582,6 +583,12 @@
"fieldname": "document_states_section",
"fieldtype": "Section Break",
"label": "Document States"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_title_field_in_link",
+ "fieldtype": "Check",
+ "label": "Show Title in Link Fields"
}
],
"icon": "fa fa-bolt",
@@ -663,7 +670,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-12-09 14:53:10.717788",
+ "modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
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 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.json b/frappe/custom/doctype/customize_form/customize_form.json
index bdf95ad351..1ee9d4a02a 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -27,6 +27,7 @@
"autoname",
"view_settings_section",
"title_field",
+ "show_title_field_in_link",
"image_field",
"default_print_format",
"column_break_29",
@@ -296,6 +297,12 @@
"fieldtype": "Table",
"label": "States",
"options": "DocType State"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_title_field_in_link",
+ "fieldtype": "Check",
+ "label": "Show Title in Link Fields"
}
],
"hide_toolbar": 1,
@@ -304,7 +311,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-12-14 16:45:04.308690",
+ "modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 1593ed49a5..92a540447f 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):
@@ -512,7 +516,8 @@ doctype_properties = {
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data',
- 'autoname': 'Data'
+ 'autoname': 'Data',
+ 'show_title_field_in_link': 'Check'
}
docfield_properties = {
@@ -558,7 +563,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/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 7c9309ee9f..f2a1206c7c 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -224,6 +224,7 @@ CREATE TABLE `tabDocType` (
`email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
+ `show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 1662b7b93e..1e79bf67d8 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -229,6 +229,7 @@ CREATE TABLE "tabDocType" (
"email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
+ "show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
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/dashboard/dashboard_list.js b/frappe/desk/doctype/dashboard/dashboard_list.js
new file mode 100644
index 0000000000..d60a324048
--- /dev/null
+++ b/frappe/desk/doctype/dashboard/dashboard_list.js
@@ -0,0 +1,16 @@
+frappe.listview_settings['Dashboard'] = {
+ button: {
+ show(doc) {
+ return doc.name;
+ },
+ get_label() {
+ return frappe.utils.icon("dashboard-list", "sm");
+ },
+ get_description(doc) {
+ return __('View {0}', [`${doc.name}`]);
+ },
+ action(doc) {
+ frappe.set_route('dashboard-view', doc.name);
+ }
+ },
+};
\ No newline at end of file
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 58d5b30103..4d35ebf5e8 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -49,7 +49,7 @@ def getdoc(doctype, name, user=None):
raise
doc.add_seen()
-
+ set_link_titles(doc)
frappe.response.docs.append(doc)
@frappe.whitelist()
@@ -367,6 +367,60 @@ def get_additional_timeline_content(doctype, docname):
return contents
+def set_link_titles(doc):
+ link_titles = {}
+ link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc))
+ link_titles.update(get_title_values_for_table_and_multiselect_fields(doc))
+
+ send_link_titles(link_titles)
+
+def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
+ link_titles = {}
+
+ if not link_fields:
+ meta = frappe.get_meta(doc.doctype)
+ link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()
+
+ for field in link_fields:
+ if not doc.get(field.fieldname):
+ continue
+
+ doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)
+
+ meta = frappe.get_meta(doctype)
+ if not meta or not (meta.title_field and meta.show_title_field_in_link):
+ continue
+
+ link_title = frappe.db.get_value(
+ doctype, doc.get(field.fieldname), meta.title_field, cache=True
+ )
+ link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})
+
+ return link_titles
+
+def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None):
+ link_titles = {}
+
+ if not table_fields:
+ meta = frappe.get_meta(doc.doctype)
+ table_fields = meta.get_table_fields()
+
+ for field in table_fields:
+ if not doc.get(field.fieldname):
+ continue
+
+ for value in doc.get(field.fieldname):
+ link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value))
+
+ return link_titles
+
+def send_link_titles(link_titles):
+ """Append link titles dict in `frappe.local.response`."""
+ if "_link_titles" not in frappe.local.response:
+ frappe.local.response["_link_titles"] = {}
+
+ frappe.local.response["_link_titles"].update(link_titles)
+
def update_user_info(docinfo):
for d in docinfo.communications:
frappe.utils.add_user_info(d.sender, docinfo.user_info)
@@ -387,3 +441,4 @@ def get_user_info_for_viewers(users):
frappe.utils.add_user_info(user, user_info)
return user_info
+
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 95397070ae..3b76953ed1 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -49,8 +49,10 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field
@frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
- search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
- frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
+ search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters,
+ reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
+
+ frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype)
del frappe.response["values"]
# this is called by the search box
@@ -138,6 +140,12 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
fields = list(set(fields + json.loads(filter_fields)))
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]
+ title_field_query = get_title_field_query(meta)
+
+ # Insert title field query after name
+ if title_field_query:
+ formatted_fields.insert(1, title_field_query)
+
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
@@ -205,11 +213,38 @@ def get_std_fields_list(meta, key):
return sflist
-def build_for_autosuggest(res):
+def get_title_field_query(meta):
+ title_field = meta.title_field if meta.title_field else None
+ show_title_field_in_link = meta.show_title_field_in_link if meta.show_title_field_in_link else None
+ field = None
+
+ if title_field and show_title_field_in_link:
+ field = "`tab{0}`.{1} as `label`".format(meta.name, title_field)
+
+ return field
+
+def build_for_autosuggest(res, doctype):
results = []
- for r in res:
- out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
- results.append(out)
+ meta = frappe.get_meta(doctype)
+ if not (meta.title_field and meta.show_title_field_in_link):
+ for r in res:
+ r = list(r)
+ results.append({
+ "value": r[0],
+ "description": ", ".join(unique(cstr(d) for d in r[1:] if d))
+ })
+
+ else:
+ title_field_exists = meta.title_field and meta.show_title_field_in_link
+ _from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists
+ for r in res:
+ r = list(r)
+ results.append({
+ "value": r[0],
+ "label": r[1] if title_field_exists else None,
+ "description": ", ".join(unique(cstr(d) for d in r[_from:] if d))
+ })
+
return results
def scrub_custom_query(query, key, txt):
@@ -272,3 +307,12 @@ def get_user_groups():
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
'is_group': True
})
+
+@frappe.whitelist()
+def get_link_title(doctype, docname):
+ meta = frappe.get_meta(doctype)
+
+ if meta.title_field and meta.show_title_field_in_link:
+ return frappe.db.get_value(doctype, docname, meta.title_field)
+
+ return docname
\ No newline at end of file
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 307d95e84b..83d3d70eea 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
@@ -75,9 +73,12 @@ def get_controller(doctype):
return site_controllers[doctype]
class BaseDocument(object):
- ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
+ ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
def __init__(self, d):
+ if d.get("doctype"):
+ self.doctype = d["doctype"]
+
self.update(d)
self.dont_update_if_missing = []
@@ -146,10 +147,14 @@ class BaseDocument(object):
else:
value = self.__dict__.get(key, default)
- if value is None and key not in self.ignore_in_getter \
- and key in (d.fieldname for d in self.meta.get_table_fields()):
- self.set(key, [])
- value = self.__dict__.get(key)
+ if value is None and key in (
+ d.fieldname for d in self.meta.get_table_fields()
+ ):
+ value = []
+ self.set(key, value)
+
+ if limit and isinstance(value, (list, tuple)) and len(value) > limit:
+ value = value[:limit]
return value
else:
@@ -159,6 +164,9 @@ class BaseDocument(object):
return self.get(key, filters=filters, limit=1)[0]
def set(self, key, value, as_value=False):
+ if key in self.ignore_in_setter:
+ return
+
if isinstance(value, list) and not as_value:
self.__dict__[key] = []
self.extend(key, value)
@@ -244,7 +252,7 @@ class BaseDocument(object):
return value
- def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
+ def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False):
d = frappe._dict()
for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname)
@@ -254,7 +262,26 @@ class BaseDocument(object):
continue
df = self.meta.get_field(fieldname)
- if df:
+
+ if df and df.get("is_virtual"):
+ if ignore_virtual:
+ del d[fieldname]
+ continue
+
+ 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 +355,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,26 +403,43 @@ 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
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
- d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
+ d = self.get_valid_dict(
+ convert_dates_to_str=True,
+ ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE,
+ ignore_virtual=True,
+ )
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 +452,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
@@ -736,7 +784,7 @@ class BaseDocument(object):
type_map = frappe.db.type_map
- for fieldname, value in self.get_valid_dict().items():
+ for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
df = self.meta.get_field(fieldname)
if not df or df.fieldtype == 'Check':
@@ -814,7 +862,7 @@ class BaseDocument(object):
if frappe.flags.in_install:
return
- for fieldname, value in self.get_valid_dict().items():
+ for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
if not value or not isinstance(value, str):
continue
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/patches.txt b/frappe/patches.txt
index db9610a767..0d2a6162c2 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -184,6 +184,7 @@ frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.set_first_day_of_the_week
+execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema
diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg
index f2977e3016..bf4e02a7af 100644
--- a/frappe/public/icons/timeless/symbol-defs.svg
+++ b/frappe/public/icons/timeless/symbol-defs.svg
@@ -814,6 +814,13 @@
+
+
+
+
+
+
+
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/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 9f02485a9e..2295cad41a 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -29,7 +29,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
setTimeout(function() {
if(me.$input.val() && me.get_options()) {
let doctype = me.get_options();
- let name = me.$input.val();
+ let name = me.get_input_value();
me.$link.toggle(true);
me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name));
}
@@ -69,6 +69,59 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.$input_area.find(".link-btn").remove();
}
}
+ set_formatted_input(value) {
+ super.set_formatted_input();
+ if (!value) return;
+
+ if (!this.title_value_map) {
+ this.title_value_map = {};
+ }
+ this.set_link_title(value);
+ }
+ set_link_title(value) {
+ let doctype = this.get_options();
+
+ if (!doctype) return;
+
+ if (in_list(frappe.boot.link_title_doctypes, doctype)) {
+ let link_title = frappe.utils.get_link_title(doctype, value);
+ if (!link_title) {
+ link_title = frappe.utils
+ .fetch_link_title(doctype, value)
+ .then(link_title => {
+ this.set_input_value(link_title);
+ this.title_value_map[link_title] = value;
+ });
+ } else {
+ this.set_input_value(link_title);
+ this.title_value_map[link_title] = value;
+ }
+ } else {
+ this.set_input_value(value);
+ }
+ }
+ parse_validate_and_set_in_model(value, e, label) {
+ if (this.parse) value = this.parse(value, label);
+ if (label) {
+ this.label = label;
+ frappe.utils.add_link_title(this.df.options, value, label);
+ }
+
+ return this.validate_and_set_in_model(value, e);
+ }
+ get_input_value() {
+ if (this.$input) {
+ const input_value = this.$input.val();
+ return this.title_value_map?.[input_value] || input_value;
+ }
+ return null;
+ }
+ get_label_value() {
+ return this.$input ? this.$input.val() : "";
+ }
+ set_input_value(value) {
+ this.$input && this.$input.val(value);
+ }
open_advanced_search() {
var doctype = this.get_options();
if(!doctype) return;
@@ -98,7 +151,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
// partially entered name field
- frappe.route_options.name_field = this.get_value();
+ frappe.route_options.name_field = this.get_label_value();
// reference to calling link
frappe._from_link = this;
@@ -120,6 +173,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
maxItems: 99,
autoFirst: true,
list: [],
+ replace: function (suggestion) {
+ // Override Awesomeplete replace function as it is used to set the input value
+ // https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403
+ this.input.value = suggestion.label || suggestion.value;
+ },
data: function (item) {
return {
label: item.label || item.value,
@@ -236,9 +294,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.selected = false;
return;
}
- var value = me.get_input_value();
- if(value!==me.last_value) {
- me.parse_validate_and_set_in_model(value);
+ let value = me.get_input_value();
+ let label = me.get_label_value();
+
+ if (value !== me.last_value || me.label !== label) {
+ me.parse_validate_and_set_in_model(value, null, label);
}
});
@@ -258,14 +318,15 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
// prevent selection on tab
var TABKEY = 9;
- if(e.keyCode === TABKEY) {
+ if (e.keyCode === TABKEY) {
e.preventDefault();
me.awesomplete.close();
return false;
}
- if(item.action) {
+ if (item.action) {
item.value = "";
+ item.label = "";
item.action.apply(me);
}
@@ -277,12 +338,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
frappe.boot.user.last_selected_values[me.df.options] = item.value;
}
- me.parse_validate_and_set_in_model(item.value);
+ me.parse_validate_and_set_in_model(item.value, null, item.label);
});
this.$input.on("awesomplete-selectcomplete", function(e) {
- var o = e.originalEvent;
- if(o.text.value.indexOf("__link_option") !== -1) {
+ let o = e.originalEvent;
+ if (o.text.value.indexOf("__link_option") !== -1) {
me.$input.val("");
}
});
diff --git a/frappe/public/js/frappe/form/controls/multiselect_pills.js b/frappe/public/js/frappe/form/controls/multiselect_pills.js
index b4a1ecf30d..bf93ac0dd8 100644
--- a/frappe/public/js/frappe/form/controls/multiselect_pills.js
+++ b/frappe/public/js/frappe/form/controls/multiselect_pills.js
@@ -83,15 +83,21 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends f
}
get_pill_html(value) {
+ const label = this.get_label(value);
const encoded_value = encodeURIComponent(value);
return `
`;
}
+ get_label(value) {
+ const item = this._data?.find(d => d.value === value);
+ return item ? item.label || item.value : null;
+ }
+
get_awesomplete_settings() {
const settings = super.get_awesomplete_settings();
diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js
index 15dfd9649e..477679bc92 100644
--- a/frappe/public/js/frappe/form/controls/table_multiselect.js
+++ b/frappe/public/js/frappe/form/controls/table_multiselect.js
@@ -49,7 +49,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
setup_buttons() {
this.$input_area.find('.link-btn').remove();
}
- parse(value) {
+ parse(value, label) {
const link_field = this.get_link_field();
if (value) {
@@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
[link_field.fieldname]: value
});
}
+ frappe.utils.add_link_title(link_field.options, value, label);
}
this._rows_list = this.rows.map(row => row[link_field.fieldname]);
return this.rows;
@@ -126,10 +127,12 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
this.$input_area.prepend(html);
}
get_pill_html(value) {
+ const link_field = this.get_link_field();
const encoded_value = encodeURIComponent(value);
+ const pill_name = frappe.utils.get_link_title(link_field.options, value) || value;
return `
`;
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index fd3fcb1bc7..c39c4046b4 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -110,12 +110,14 @@ frappe.form.formatters = {
Link: function(value, docfield, options, doc) {
var doctype = docfield._options || docfield.options;
var original_value = value;
+ let link_title = frappe.utils.get_link_title(doctype, value);
+
if(value && value.match && value.match(/^['"].*['"]$/)) {
value.replace(/^.(.*).$/, "$1");
}
if(options && (options.for_print || options.only_value)) {
- return value;
+ return link_title || value;
}
if(frappe.form.link_formatters[doctype]) {
@@ -139,13 +141,14 @@ frappe.form.formatters = {
return `
- ${__(options && options.label || value)}`;
+ data-name="${original_value}"
+ data-value="${original_value}">
+ ${__(options && options.label || link_title || value)}`;
} else {
- return value;
+ return link_title || value;
}
} else {
- return value;
+ return link_title || value;
}
},
Date: function(value) {
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/save.js b/frappe/public/js/frappe/form/save.js
index 934c90f017..da642b7ca5 100644
--- a/frappe/public/js/frappe/form/save.js
+++ b/frappe/public/js/frappe/form/save.js
@@ -249,30 +249,39 @@ frappe.ui.form.update_calling_link = (newdoc) => {
};
if (is_valid_doctype()) {
- // set value
- if (doc && doc.parentfield) {
- //update values for child table
- $.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
- if (field.doc && field.doc.name === frappe._from_link.docname) {
- frappe._from_link.set_value(newdoc.name);
- }
- });
- } else {
- frappe._from_link.set_value(newdoc.name);
- }
-
- // refresh field
- frappe._from_link.refresh();
-
- // if from form, switch
- if (frappe._from_link.frm) {
- frappe.set_route("Form",
- frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
- .then(() => {
- frappe.utils.scroll_to(frappe._from_link_scrollY);
+ frappe.model.with_doctype(newdoc.doctype, () => {
+ let meta = frappe.get_meta(newdoc.doctype);
+ // set value
+ if (doc && doc.parentfield) {
+ //update values for child table
+ $.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
+ if (field.doc && field.doc.name === frappe._from_link.docname) {
+ if (meta.title_field && meta.show_title_field_in_link) {
+ frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
+ }
+ frappe._from_link.set_value(newdoc.name);
+ }
});
- }
+ } else {
+ if (meta.title_field && meta.show_title_field_in_link) {
+ frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
+ }
+ frappe._from_link.set_value(newdoc.name);
+ }
- frappe._from_link = null;
+ // refresh field
+ frappe._from_link.refresh();
+
+ // if from form, switch
+ if (frappe._from_link.frm) {
+ frappe.set_route("Form",
+ frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
+ .then(() => {
+ frappe.utils.scroll_to(frappe._from_link_scrollY);
+ });
+ }
+
+ frappe._from_link = null;
+ });
}
}
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/base_list.js b/frappe/public/js/frappe/list/base_list.js
index 8fa512bfc0..ed27bdb29e 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -394,7 +394,6 @@ frappe.views.BaseList = class BaseList {
this.page_length = $this.data().value;
} else if ($this.is(".btn-more")) {
this.start = this.start + this.page_length;
- this.page_length = 20;
}
this.refresh();
});
@@ -475,7 +474,6 @@ frappe.views.BaseList = class BaseList {
this.render();
this.after_render();
this.freeze(false);
- this.reset_defaults();
if (this.settings.refresh) {
this.settings.refresh(this);
}
@@ -502,11 +500,6 @@ frappe.views.BaseList = class BaseList {
this.data = this.data.uniqBy((d) => d.name);
}
- reset_defaults() {
- this.page_length = this.page_length + this.start;
- this.start = 0;
- }
-
freeze() {
// show a freeze message while data is loading
}
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/request.js b/frappe/public/js/frappe/request.js
index 35f946df65..5766619d59 100644
--- a/frappe/public/js/frappe/request.js
+++ b/frappe/public/js/frappe/request.js
@@ -260,6 +260,14 @@ frappe.request.call = function(opts) {
$.extend(frappe._messages, data.__messages);
}
+ // sync link titles
+ if (data._link_titles) {
+ if (!frappe._link_titles) {
+ frappe._link_titles = {};
+ }
+ $.extend(frappe._link_titles, data._link_titles);
+ }
+
// callbacks
var status_code_handler = statusCode[xhr.statusCode().status];
if (status_code_handler) {
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index f5726d3a29..91922b0bc1 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -314,6 +314,10 @@ frappe.ui.Filter = class {
return this.utils.get_selected_value(this.field, this.get_condition());
}
+ get_selected_label() {
+ return this.utils.get_selected_label(this.field);
+ }
+
get_condition() {
return this.filter_edit_area.find('.condition').val();
}
@@ -361,7 +365,7 @@ frappe.ui.Filter = class {
get_filter_button_text() {
let value = this.utils.get_formatted_value(
this.field,
- this.get_selected_value()
+ this.get_selected_label() || this.get_selected_value()
);
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
value
@@ -449,6 +453,12 @@ frappe.ui.filter_utils = {
return val;
},
+ get_selected_label(field) {
+ if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) {
+ return field.get_label_value();
+ }
+ },
+
get_default_condition(df) {
if (df.fieldtype == 'Data') {
return 'like';
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 = /