Merge branch 'develop' into fix_df_propagation
This commit is contained in:
commit
4697cc40bb
56 changed files with 1507 additions and 723 deletions
|
|
@ -48,3 +48,7 @@ pull_request_rules:
|
|||
actions:
|
||||
merge:
|
||||
method: squash
|
||||
commit_message_template: |
|
||||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
|
|
|||
|
|
@ -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]}`);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
) ;
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
16
frappe/desk/doctype/dashboard/dashboard_list.js
Normal file
16
frappe/desk/doctype/dashboard/dashboard_list.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -814,6 +814,13 @@
|
|||
<path d="M16.814 13.3304L17.9274 12.6875" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard-list">
|
||||
<path d="M7.5 2.5H4.5C3.94772 2.5 3.5 2.94772 3.5 3.5V9.5C3.5 10.0523 3.94772 10.5 4.5 10.5H7.5C8.05228 10.5 8.5 10.0523 8.5 9.5V3.5C8.5 2.94772 8.05228 2.5 7.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 13.5H4.5C3.94772 13.5 3.5 13.9477 3.5 14.5V16.5C3.5 17.0523 3.94772 17.5 4.5 17.5H7.5C8.05228 17.5 8.5 17.0523 8.5 16.5V14.5C8.5 13.9477 8.05228 13.5 7.5 13.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 2.5H12.5C11.9477 2.5 11.5 2.94772 11.5 3.5V6.5C11.5 7.05228 11.9477 7.5 12.5 7.5H15.5C16.0523 7.5 16.5 7.05228 16.5 6.5V3.5C16.5 2.94772 16.0523 2.5 15.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.5 10.5H12.5C11.9477 10.5 11.5 10.9477 11.5 11.5V16.5C11.5 17.0523 11.9477 17.5 12.5 17.5H15.5C16.0523 17.5 16.5 17.0523 16.5 16.5V11.5C16.5 10.9477 16.0523 10.5 15.5 10.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-text">
|
||||
<path d="M5 4V6.4H9V16H11.4V6.4H15.4V4H5Z" fill="var(--icon-stroke)" stroke="none"/>
|
||||
</symbol>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
|
||||
<span class="btn-link-to-form">${__(value)}</span>
|
||||
<span class="btn-link-to-form">${__(label || value)}</span>
|
||||
<span class="btn-remove">${frappe.utils.icon('close')}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
|
||||
<span class="btn-link-to-form">${__(value)}</span>
|
||||
<span class="btn-link-to-form">${__(pill_name)}</span>
|
||||
<span class="btn-remove">${frappe.utils.icon('close')}</span>
|
||||
</button>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 `<a
|
||||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
|
||||
data-doctype="${doctype}"
|
||||
data-name="${original_value}">
|
||||
${__(options && options.label || value)}</a>`;
|
||||
data-name="${original_value}"
|
||||
data-value="${original_value}">
|
||||
${__(options && options.label || link_title || value)}</a>`;
|
||||
} else {
|
||||
return value;
|
||||
return link_title || value;
|
||||
}
|
||||
} else {
|
||||
return value;
|
||||
return link_title || value;
|
||||
}
|
||||
},
|
||||
Date: function(value) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
|
|||
'/': '/'
|
||||
};
|
||||
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
|
||||
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
|
||||
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.
|
||||
|
||||
// Rule 3 - TODO: Check event handlers?
|
||||
// script and alert should be checked first or else it will be escaped
|
||||
if (options.strategies.includes('js')) {
|
||||
sanitised = sanitised.replace(REGEX_SCRIPT, "");
|
||||
sanitised = sanitised.replace(REGEX_ALERT, "");
|
||||
}
|
||||
|
||||
// Rule 1
|
||||
if (options.strategies.includes('html')) {
|
||||
for (let char in HTML_ESCAPE_MAP) {
|
||||
|
|
@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
|
|||
}
|
||||
}
|
||||
|
||||
// Rule 3 - TODO: Check event handlers?
|
||||
if (options.strategies.includes('js')) {
|
||||
sanitised = sanitised.replace(REGEX_SCRIPT, "");
|
||||
}
|
||||
|
||||
return sanitised;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1416,5 +1416,42 @@ Object.assign(frappe.utils, {
|
|||
arr.push(i);
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
|
||||
get_link_title(doctype, name) {
|
||||
if (!doctype || !name || !frappe._link_titles) {
|
||||
return;
|
||||
}
|
||||
|
||||
return frappe._link_titles[doctype + "::" + name];
|
||||
},
|
||||
|
||||
add_link_title(doctype, name, value) {
|
||||
if (!doctype || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frappe._link_titles) {
|
||||
// for link titles
|
||||
frappe._link_titles = {};
|
||||
}
|
||||
|
||||
frappe._link_titles[doctype + "::" + name] = value;
|
||||
},
|
||||
|
||||
fetch_link_title(doctype, name) {
|
||||
try {
|
||||
return frappe.xcall("frappe.desk.search.get_link_title", {
|
||||
"doctype": doctype,
|
||||
"docname": name
|
||||
}).then(title => {
|
||||
frappe.utils.add_link_title(doctype, name, title);
|
||||
return title;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error while fetching link title.'); // eslint-disable-line
|
||||
console.log(error); // eslint-disable-line
|
||||
return Promise.resolve(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -648,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
// not a cancelled doc
|
||||
&& data.docstatus !== 2
|
||||
&& !df.read_only
|
||||
&& !df.is_virtual
|
||||
&& !df.hidden
|
||||
// not a standard field i.e., owner, modified_by, etc.
|
||||
&& !frappe.model.std_fields_list.includes(df.fieldname))
|
||||
|
|
@ -1029,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
title += ` (${__(doctype)})`;
|
||||
}
|
||||
|
||||
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
|
||||
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;
|
||||
|
||||
const align = (() => {
|
||||
const is_numeric = frappe.model.is_numeric_field(docfield);
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
|
|||
this.ignore_fields = this.opts.ignore_fields || [];
|
||||
|
||||
var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
|
||||
return (d.reqd || d.bold && !d.read_only) ? d : null });
|
||||
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });
|
||||
|
||||
var opts_field_names = this.fields.map(function(d) {
|
||||
return d.fieldname
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@
|
|||
--shadow-md: 0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04);
|
||||
--shadow-lg: 0px 18px 22px rgba(25, 39, 52, 0.1), 0px 1px 10px rgba(0, 0, 0, 0.06), 0px 0.5px 5px rgba(25, 39, 52, 0.04);
|
||||
|
||||
--drop-shadow: 0px 0.5px 0px rgba(0, 0, 0, 0.05), 0px 0px 0px rgba(0, 0, 0, 0), 0px 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--modal-shadow: var(--shadow-md);
|
||||
--card-shadow: var(--shadow-sm);
|
||||
--btn-shadow: var(--shadow-xs);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,31 @@ $level-margin-right: 8px;
|
|||
}
|
||||
|
||||
.list-paging-area, .footnote-area {
|
||||
border-top: 1px sol var(--border-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
.btn-group {
|
||||
box-shadow: var(--drop-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&> .btn:nth-child(2) {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.btn-paging {
|
||||
box-shadow: none;
|
||||
margin-left: 0px !important;
|
||||
border: 1px solid var(--dark-border-color);
|
||||
|
||||
&.btn-info {
|
||||
background-color: var(--gray-400);
|
||||
border-color: var(--gray-400);
|
||||
color: var(--white);
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.frappe-card {
|
||||
|
|
|
|||
|
|
@ -104,10 +104,10 @@ body[data-route^="Module"] .main-menu {
|
|||
}
|
||||
|
||||
.sidebar-image-section {
|
||||
width: min(100%, 170px);
|
||||
cursor: pointer;
|
||||
|
||||
.sidebar-image {
|
||||
width: min(100%, 170px);
|
||||
height: auto;
|
||||
max-height: 170px;
|
||||
object-fit: cover;
|
||||
|
|
|
|||
|
|
@ -1,77 +1,126 @@
|
|||
import sys
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from random import choice
|
||||
from threading import Thread
|
||||
from typing import Dict, Optional, Tuple
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
from semantic_version import Version
|
||||
from werkzeug.test import TestResponse
|
||||
|
||||
import frappe
|
||||
from frappe.utils import get_site_url
|
||||
from frappe.utils import get_site_url, get_test_client
|
||||
|
||||
try:
|
||||
_site = frappe.local.site
|
||||
except Exception:
|
||||
_site = None
|
||||
|
||||
authorization_token = None
|
||||
|
||||
@contextmanager
|
||||
def suppress_stdout():
|
||||
"""Supress stdout for tests which expectedly make noise
|
||||
but that you don't need in tests"""
|
||||
sys.stdout = None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__
|
||||
|
||||
|
||||
def maintain_state(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
frappe.db.rollback()
|
||||
r = f(*args, **kwargs)
|
||||
frappe.db.commit()
|
||||
return r
|
||||
|
||||
return wrapper
|
||||
def make_request(target: str, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None) -> TestResponse:
|
||||
t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs)
|
||||
t.start()
|
||||
t.join()
|
||||
return t._return
|
||||
|
||||
|
||||
class TestResourceAPI(unittest.TestCase):
|
||||
SITE_URL = get_site_url(frappe.local.site)
|
||||
def patch_request_header(key, *args, **kwargs):
|
||||
if key == "Authorization":
|
||||
return f"token {authorization_token}"
|
||||
|
||||
|
||||
class ThreadWithReturnValue(Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
Thread.__init__(self, group, target, name, args, kwargs)
|
||||
self._return = None
|
||||
|
||||
def run(self):
|
||||
if self._target is not None:
|
||||
with patch("frappe.app.get_site_name", return_value=_site):
|
||||
header_patch = patch("frappe.get_request_header", new=patch_request_header)
|
||||
if authorization_token:
|
||||
header_patch.start()
|
||||
self._return = self._target(*self._args, **self._kwargs)
|
||||
if authorization_token:
|
||||
header_patch.stop()
|
||||
|
||||
def join(self, *args):
|
||||
Thread.join(self, *args)
|
||||
return self._return
|
||||
|
||||
|
||||
class FrappeAPITestCase(unittest.TestCase):
|
||||
SITE = frappe.local.site
|
||||
SITE_URL = get_site_url(SITE)
|
||||
RESOURCE_URL = f"{SITE_URL}/api/resource"
|
||||
TEST_CLIENT = get_test_client()
|
||||
|
||||
@property
|
||||
def sid(self) -> str:
|
||||
if not getattr(self, "_sid", None):
|
||||
r = self.post("/api/method/login", {
|
||||
"usr": "Administrator",
|
||||
"pwd": frappe.conf.admin_password or "admin",
|
||||
})
|
||||
self._sid = r.headers[2][1].split(";")[0].lstrip("sid=")
|
||||
|
||||
return self._sid
|
||||
|
||||
def get(self, path: str, params: Optional[Dict] = None) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.get, args=(path, ), kwargs={"data": params})
|
||||
|
||||
def post(self, path, data) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.post, args=(path, ), kwargs={"data": data})
|
||||
|
||||
def put(self, path, data) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.put, args=(path, ), kwargs={"data": data})
|
||||
|
||||
def delete(self, path) -> TestResponse:
|
||||
return make_request(target=self.TEST_CLIENT.delete, args=(path, ))
|
||||
|
||||
|
||||
class TestResourceAPI(FrappeAPITestCase):
|
||||
DOCTYPE = "ToDo"
|
||||
GENERATED_DOCUMENTS = []
|
||||
|
||||
@classmethod
|
||||
@maintain_state
|
||||
def setUpClass(self):
|
||||
def setUpClass(cls):
|
||||
for _ in range(10):
|
||||
doc = frappe.get_doc(
|
||||
{"doctype": "ToDo", "description": frappe.mock("paragraph")}
|
||||
).insert()
|
||||
self.GENERATED_DOCUMENTS.append(doc.name)
|
||||
cls.GENERATED_DOCUMENTS.append(doc.name)
|
||||
frappe.db.commit()
|
||||
|
||||
@classmethod
|
||||
@maintain_state
|
||||
def tearDownClass(self):
|
||||
for name in self.GENERATED_DOCUMENTS:
|
||||
frappe.delete_doc_if_exists(self.DOCTYPE, name)
|
||||
def tearDownClass(cls):
|
||||
for name in cls.GENERATED_DOCUMENTS:
|
||||
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
|
||||
frappe.db.commit()
|
||||
|
||||
def setUp(self):
|
||||
# commit to ensure consistency in session (postgres CI randomly fails)
|
||||
if frappe.conf.db_type == "postgres":
|
||||
frappe.db.commit()
|
||||
|
||||
@property
|
||||
def sid(self):
|
||||
if not getattr(self, "_sid", None):
|
||||
self._sid = requests.post(
|
||||
f"{self.SITE_URL}/api/method/login",
|
||||
data={
|
||||
"usr": "Administrator",
|
||||
"pwd": frappe.conf.admin_password or "admin",
|
||||
},
|
||||
).cookies.get("sid")
|
||||
|
||||
return self._sid
|
||||
|
||||
def get(self, path, params=""):
|
||||
return requests.get(f"{self.RESOURCE_URL}/{path}?sid={self.sid}{params}")
|
||||
|
||||
def post(self, path, data):
|
||||
return requests.post(
|
||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
|
||||
)
|
||||
|
||||
def put(self, path, data):
|
||||
return requests.put(
|
||||
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
|
||||
)
|
||||
|
||||
def delete(self, path):
|
||||
return requests.delete(f"{self.RESOURCE_URL}/{path}?sid={self.sid}")
|
||||
if self._testMethodName == "test_auth_cycle":
|
||||
from frappe.core.doctype.user.user import generate_keys
|
||||
generate_keys("Administrator")
|
||||
frappe.db.commit()
|
||||
|
||||
def test_unauthorized_call(self):
|
||||
# test 1: fetch documents without auth
|
||||
|
|
@ -80,88 +129,107 @@ class TestResourceAPI(unittest.TestCase):
|
|||
|
||||
def test_get_list(self):
|
||||
# test 2: fetch documents without params
|
||||
response = self.get(self.DOCTYPE)
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json(), dict)
|
||||
self.assertIn("data", response.json())
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn("data", response.json)
|
||||
|
||||
def test_get_list_limit(self):
|
||||
# test 3: fetch data with limit
|
||||
response = self.get(self.DOCTYPE, "&limit=2")
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "limit": 2})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.json()["data"]), 2)
|
||||
self.assertEqual(len(response.json["data"]), 2)
|
||||
|
||||
def test_get_list_dict(self):
|
||||
# test 4: fetch response as (not) dict
|
||||
response = self.get(self.DOCTYPE, "&as_dict=True")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": True})
|
||||
json = frappe._dict(response.json)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json.data, list)
|
||||
self.assertIsInstance(json.data[0], dict)
|
||||
|
||||
response = self.get(self.DOCTYPE, "&as_dict=False")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": False})
|
||||
json = frappe._dict(response.json)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json.data, list)
|
||||
self.assertIsInstance(json.data[0], list)
|
||||
|
||||
def test_get_list_debug(self):
|
||||
# test 5: fetch response with debug
|
||||
response = self.get(self.DOCTYPE, "&debug=true")
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "debug": True})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("exc", response.json())
|
||||
self.assertIsInstance(response.json()["exc"], str)
|
||||
self.assertIsInstance(eval(response.json()["exc"]), list)
|
||||
self.assertIn("exc", response.json)
|
||||
self.assertIsInstance(response.json["exc"], str)
|
||||
self.assertIsInstance(eval(response.json["exc"]), list)
|
||||
|
||||
def test_get_list_fields(self):
|
||||
# test 6: fetch response with fields
|
||||
response = self.get(self.DOCTYPE, r'&fields=["description"]')
|
||||
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "fields": '["description"]'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
json = frappe._dict(response.json())
|
||||
json = frappe._dict(response.json)
|
||||
self.assertIn("description", json.data[0])
|
||||
|
||||
def test_create_document(self):
|
||||
# test 7: POST method on /api/resource to create doc
|
||||
data = {"description": frappe.mock("paragraph")}
|
||||
response = self.post(self.DOCTYPE, data)
|
||||
data = {"description": frappe.mock("paragraph"), "sid": self.sid}
|
||||
response = self.post(f"/api/resource/{self.DOCTYPE}", data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
docname = response.json()["data"]["name"]
|
||||
docname = response.json["data"]["name"]
|
||||
self.assertIsInstance(docname, str)
|
||||
self.GENERATED_DOCUMENTS.append(docname)
|
||||
|
||||
def test_update_document(self):
|
||||
# test 8: PUT method on /api/resource to update doc
|
||||
generated_desc = frappe.mock("paragraph")
|
||||
data = {"description": generated_desc}
|
||||
data = {"description": generated_desc, "sid": self.sid}
|
||||
random_doc = choice(self.GENERATED_DOCUMENTS)
|
||||
desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description")
|
||||
|
||||
response = self.put(f"{self.DOCTYPE}/{random_doc}", data=data)
|
||||
response = self.put(f"/api/resource/{self.DOCTYPE}/{random_doc}", data=data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotEqual(response.json()["data"]["description"], desc_before_update)
|
||||
self.assertEqual(response.json()["data"]["description"], generated_desc)
|
||||
self.assertNotEqual(response.json["data"]["description"], desc_before_update)
|
||||
self.assertEqual(response.json["data"]["description"], generated_desc)
|
||||
|
||||
def test_delete_document(self):
|
||||
# test 9: DELETE method on /api/resource
|
||||
doc_to_delete = choice(self.GENERATED_DOCUMENTS)
|
||||
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}")
|
||||
response = self.delete(f"/api/resource/{self.DOCTYPE}/{doc_to_delete}")
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertDictEqual(response.json(), {"message": "ok"})
|
||||
self.assertDictEqual(response.json, {"message": "ok"})
|
||||
self.GENERATED_DOCUMENTS.remove(doc_to_delete)
|
||||
|
||||
non_existent_doc = frappe.generate_hash(length=12)
|
||||
response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}")
|
||||
with suppress_stdout():
|
||||
response = self.delete(f"/api/resource/{self.DOCTYPE}/{non_existent_doc}")
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertDictEqual(response.json(), {})
|
||||
self.assertDictEqual(response.json, {})
|
||||
|
||||
def test_run_doc_method(self):
|
||||
# test 10: Run whitelisted method on doc via /api/resource
|
||||
# status_code is 403 if no other tests are run before this - it's not logged in
|
||||
self.post("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
|
||||
response = self.get("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
|
||||
self.assertIn(response.status_code, (403, 200))
|
||||
|
||||
if response.status_code == 403:
|
||||
self.assertTrue(set(response.json.keys()) == {'exc_type', 'exception', 'exc', '_server_messages'})
|
||||
self.assertEqual(response.json.get('exc_type'), 'PermissionError')
|
||||
self.assertEqual(response.json.get('exception'), 'frappe.exceptions.PermissionError: Not permitted')
|
||||
self.assertIsInstance(response.json.get('exc'), str)
|
||||
|
||||
elif response.status_code == 200:
|
||||
data = response.json.get("data")
|
||||
self.assertIsInstance(data, list)
|
||||
self.assertIsInstance(data[0], dict)
|
||||
|
||||
|
||||
class TestMethodAPI(unittest.TestCase):
|
||||
METHOD_URL = f"{get_site_url(frappe.local.site)}/api/method"
|
||||
class TestMethodAPI(FrappeAPITestCase):
|
||||
METHOD_PATH = "/api/method"
|
||||
|
||||
def test_version(self):
|
||||
# test 1: test for /api/method/version
|
||||
response = requests.get(f"{self.METHOD_URL}/version")
|
||||
json = frappe._dict(response.json())
|
||||
response = self.get(f"{self.METHOD_PATH}/version")
|
||||
json = frappe._dict(response.json)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(json, dict)
|
||||
|
|
@ -170,7 +238,27 @@ class TestMethodAPI(unittest.TestCase):
|
|||
|
||||
def test_ping(self):
|
||||
# test 2: test for /api/method/ping
|
||||
response = requests.get(f"{self.METHOD_URL}/ping")
|
||||
response = self.get(f"{self.METHOD_PATH}/ping")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json(), dict)
|
||||
self.assertEqual(response.json()['message'], "pong")
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertEqual(response.json["message"], "pong")
|
||||
|
||||
def test_get_user_info(self):
|
||||
# test 3: test for /api/method/frappe.realtime.get_user_info
|
||||
response = self.get(f"{self.METHOD_PATH}/frappe.realtime.get_user_info")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.json, dict)
|
||||
self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))
|
||||
|
||||
def test_auth_cycle(self):
|
||||
# test 4: Pass authorization token in request
|
||||
global authorization_token
|
||||
user = frappe.get_doc("User", "Administrator")
|
||||
api_key, api_secret = user.api_key, user.get_password("api_secret")
|
||||
authorization_token = f"{api_key}:{api_secret}"
|
||||
response = self.get("/api/method/frappe.auth.get_logged_user")
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json["message"], "Administrator")
|
||||
|
||||
authorization_token = None
|
||||
|
|
|
|||
|
|
@ -291,6 +291,16 @@ class TestDB(unittest.TestCase):
|
|||
|
||||
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION
|
||||
|
||||
def test_pk_collision_ignoring(self):
|
||||
# note has `name` generated from title
|
||||
for _ in range(3):
|
||||
frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True)
|
||||
|
||||
with savepoint():
|
||||
self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert)
|
||||
# recover transaction to continue other tests
|
||||
raise Exception
|
||||
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
class TestDDLCommandsMaria(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
|
||||
from frappe.desk.doctype.note.note import Note
|
||||
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
|
||||
from frappe.utils import cint, now_datetime
|
||||
|
||||
|
||||
class CustomTestNote(Note):
|
||||
@property
|
||||
def age(self):
|
||||
return now_datetime() - self.creation
|
||||
|
||||
|
||||
class TestDocument(unittest.TestCase):
|
||||
|
|
@ -255,5 +264,58 @@ class TestDocument(unittest.TestCase):
|
|||
|
||||
def test_limit_for_get(self):
|
||||
doc = frappe.get_doc("DocType", "DocType")
|
||||
# assuming DocType has more that 3 Data fields
|
||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
|
||||
# assuming DocType has more than 3 Data fields
|
||||
self.assertEquals(len(doc.get("fields", limit=3)), 3)
|
||||
|
||||
# limit with filters
|
||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
|
||||
|
||||
def test_virtual_fields(self):
|
||||
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked
|
||||
"""
|
||||
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
|
||||
note = frappe.new_doc("Note")
|
||||
note.content = "some content"
|
||||
note.title = frappe.generate_hash(length=20)
|
||||
note.insert()
|
||||
|
||||
def patch_note():
|
||||
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})
|
||||
|
||||
@contextmanager
|
||||
def customize_note(with_options=False):
|
||||
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
|
||||
custom_field = frappe.get_doc({
|
||||
"doctype": "Custom Field",
|
||||
"dt": "Note",
|
||||
"fieldname": "age",
|
||||
"fieldtype": "Data",
|
||||
"read_only": True,
|
||||
"is_virtual": True,
|
||||
"options": options,
|
||||
})
|
||||
|
||||
try:
|
||||
yield custom_field.insert(ignore_if_duplicate=True)
|
||||
finally:
|
||||
custom_field.delete()
|
||||
|
||||
with patch_note():
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, CustomTestNote)
|
||||
self.assertIsInstance(doc.age, timedelta)
|
||||
self.assertIsNone(doc.as_dict().get("age"))
|
||||
self.assertIsNone(doc.get_valid_dict().get("age"))
|
||||
|
||||
with customize_note(), patch_note():
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, CustomTestNote)
|
||||
self.assertIsInstance(doc.age, timedelta)
|
||||
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
|
||||
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
|
||||
|
||||
with customize_note(with_options=True):
|
||||
doc = frappe.get_last_doc("Note")
|
||||
self.assertIsInstance(doc, Note)
|
||||
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
|
||||
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import requests
|
|||
import base64
|
||||
|
||||
class TestFrappeClient(unittest.TestCase):
|
||||
PASSWORD = "admin"
|
||||
PASSWORD = frappe.conf.admin_password or "admin"
|
||||
|
||||
def test_insert_many(self):
|
||||
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
|
||||
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})
|
||||
|
|
@ -169,7 +170,6 @@ class TestFrappeClient(unittest.TestCase):
|
|||
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
|
||||
self.assertEqual(res.status_code, 403)
|
||||
|
||||
|
||||
# random api key and api secret
|
||||
api_key = "@3djdk3kld"
|
||||
api_secret = "ksk&93nxoe3os"
|
||||
|
|
|
|||
|
|
@ -419,3 +419,96 @@ class TestXlsxUtils(unittest.TestCase):
|
|||
val = handle_html("<p>html data ></p>")
|
||||
self.assertIn("html data >", val)
|
||||
self.assertEqual("abc", handle_html("abc"))
|
||||
|
||||
|
||||
class TestLinkTitle(unittest.TestCase):
|
||||
def test_link_title_doctypes_in_boot_info(self):
|
||||
"""
|
||||
Test that doctypes are added to link_title_map in boot_info
|
||||
"""
|
||||
custom_doctype = frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{
|
||||
"label": "Test Field",
|
||||
"fieldname": "test_title_field",
|
||||
"fieldtype": "Data",
|
||||
}
|
||||
],
|
||||
"show_title_field_in_link": 1,
|
||||
"title_field": "test_title_field",
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Test Custom Doctype for Link Title",
|
||||
}
|
||||
)
|
||||
custom_doctype.insert()
|
||||
|
||||
prop_setter = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "User",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
from frappe.boot import get_link_title_doctypes
|
||||
|
||||
link_title_doctypes = get_link_title_doctypes()
|
||||
self.assertTrue("User" in link_title_doctypes)
|
||||
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes)
|
||||
|
||||
prop_setter.delete()
|
||||
custom_doctype.delete()
|
||||
|
||||
def test_link_titles_on_getdoc(self):
|
||||
"""
|
||||
Test that link titles are added to the doctype on getdoc
|
||||
"""
|
||||
prop_setter = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doc_type": "User",
|
||||
"property": "show_title_field_in_link",
|
||||
"property_type": "Check",
|
||||
"doctype_or_field": "DocType",
|
||||
"value": "1",
|
||||
}
|
||||
).insert()
|
||||
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"user_type": "Website User",
|
||||
"email": "test_user_for_link_title@example.com",
|
||||
"send_welcome_email": 0,
|
||||
"first_name": "Test User for Link Title",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
todo = frappe.get_doc(
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"description": "test-link-title-on-getdoc",
|
||||
"allocated_to": user.name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
from frappe.desk.form.load import getdoc
|
||||
|
||||
getdoc("ToDo", todo.name)
|
||||
link_titles = frappe.local.response["_link_titles"]
|
||||
|
||||
self.assertTrue(f"{user.doctype}::{user.name}" in link_titles)
|
||||
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name)
|
||||
|
||||
todo.delete()
|
||||
user.delete()
|
||||
prop_setter.delete()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ def create_contact_records():
|
|||
insert_contact('Test Form Contact 2', '54321')
|
||||
insert_contact('Test Form Contact 3', '12345')
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_multiple_contact_records():
|
||||
if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}):
|
||||
return
|
||||
for index in range(1001):
|
||||
insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1))
|
||||
|
||||
def insert_contact(first_name, phone_number):
|
||||
doc = frappe.get_doc({
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ More Information,Mehr Informationen,
|
|||
More...,Mehr...,
|
||||
Move,Bewegen,
|
||||
My Account,Mein Konto,
|
||||
My Profile,Mein Profil,
|
||||
My Settings,Meine Einstellungen,
|
||||
New Address,Neue Adresse,
|
||||
New Contact,Neuer Kontakt,
|
||||
Next,Weiter,
|
||||
|
|
@ -406,7 +408,7 @@ Allow Self Approval,Erlaube Selbstgenehmigung,
|
|||
Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen,
|
||||
Allow events in timeline,Ereignisse in der Zeitleiste zulassen,
|
||||
Allow in Quick Entry,In Schnelleingabe zulassen,
|
||||
Allow on Submit,Beim Übertragen zulassen,
|
||||
Allow on Submit,Änderungen zulassen wenn gebucht,
|
||||
Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen,
|
||||
Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben,
|
||||
Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen,
|
||||
|
|
|
|||
|
|
|
@ -438,7 +438,8 @@ def touch_file(path):
|
|||
os.utime(path, None)
|
||||
return path
|
||||
|
||||
def get_test_client():
|
||||
def get_test_client() -> Client:
|
||||
"""Returns an test instance of the Frappe WSGI"""
|
||||
from frappe.app import application
|
||||
return Client(application)
|
||||
|
||||
|
|
|
|||
|
|
@ -88,9 +88,14 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
|
|||
return frappe.utils.markdown(value)
|
||||
|
||||
elif df.get("fieldtype") == "Table MultiSelect":
|
||||
values = []
|
||||
meta = frappe.get_meta(df.options)
|
||||
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0]
|
||||
values = [v.get(link_field.fieldname, 'asdf') for v in value]
|
||||
for v in value:
|
||||
v.update({'__link_titles': doc.get('__link_titles')})
|
||||
formatted_value = frappe.format_value(v.get(link_field.fieldname, ''), link_field, v)
|
||||
values.append(formatted_value)
|
||||
|
||||
return ', '.join(values)
|
||||
|
||||
elif df.get("fieldtype") == "Duration":
|
||||
|
|
@ -100,4 +105,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
|
|||
elif df.get("fieldtype") == "Text Editor":
|
||||
return "<div class='ql-snow'>{}</div>".format(value)
|
||||
|
||||
elif df.get("fieldtype") in ["Link", "Dynamic Link"]:
|
||||
if not doc or not doc.get("__link_titles") or not df.options:
|
||||
return value
|
||||
|
||||
doctype = df.options
|
||||
if df.get("fieldtype") == "Dynamic Link":
|
||||
if not df.parent:
|
||||
return value
|
||||
|
||||
meta = frappe.get_meta(df.parent)
|
||||
_field = meta.get_field(df.options)
|
||||
doctype = _field.options
|
||||
|
||||
return doc.__link_titles.get("{0}::{1}".format(doctype, value), value)
|
||||
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
|
|||
options: field.options,
|
||||
reqd: field.reqd,
|
||||
default: field.default,
|
||||
read_only: field.read_only,
|
||||
read_only: field.read_only || field.is_virtual,
|
||||
depends_on: field.depends_on,
|
||||
mandatory_depends_on: field.mandatory_depends_on,
|
||||
read_only_depends_on: field.read_only_depends_on,
|
||||
|
|
|
|||
|
|
@ -169,6 +169,48 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
|
|||
|
||||
return html
|
||||
|
||||
def set_link_titles(doc):
|
||||
# Adds name with title of link field doctype to __link_titles
|
||||
if not doc.get("__link_titles"):
|
||||
setattr(doc, "__link_titles", {})
|
||||
|
||||
meta = frappe.get_meta(doc.doctype)
|
||||
set_title_values_for_link_and_dynamic_link_fields(meta, doc)
|
||||
set_title_values_for_table_and_multiselect_fields(meta, doc)
|
||||
|
||||
def set_title_values_for_link_and_dynamic_link_fields(meta, doc, parent_doc=None):
|
||||
if parent_doc and not parent_doc.get("__link_titles"):
|
||||
setattr(parent_doc, "__link_titles", {})
|
||||
elif doc and not doc.get("__link_titles"):
|
||||
setattr(doc, "__link_titles", {})
|
||||
|
||||
for field in meta.get_link_fields() + meta.get_dynamic_link_fields():
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
# If link field, then get doctype from options
|
||||
# If dynamic link field, then get doctype from dependent field
|
||||
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.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field)
|
||||
if parent_doc:
|
||||
parent_doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
|
||||
elif doc:
|
||||
doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
|
||||
|
||||
def set_title_values_for_table_and_multiselect_fields(meta, doc):
|
||||
for field in meta.get_table_fields():
|
||||
if not doc.get(field.fieldname):
|
||||
continue
|
||||
|
||||
_meta = frappe.get_meta(field.options)
|
||||
for value in doc.get(field.fieldname):
|
||||
set_title_values_for_link_and_dynamic_link_fields(_meta, value, doc)
|
||||
|
||||
def convert_markdown(doc, meta):
|
||||
'''Convert text field values to markdown if necessary'''
|
||||
for field in meta.fields:
|
||||
|
|
@ -190,6 +232,7 @@ def get_html_and_style(doc, name=None, print_format=None, meta=None,
|
|||
doc = frappe.get_doc(json.loads(doc))
|
||||
|
||||
print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype))
|
||||
set_link_titles(doc)
|
||||
|
||||
try:
|
||||
html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue