@@ -0,0 +1,59 @@ | |||||
export default { | |||||
name: "Child Table Doctype 1", | |||||
actions: [], | |||||
custom: 1, | |||||
autoname: "format: Test-{####}", | |||||
creation: "2022-02-09 20:15:21.242213", | |||||
doctype: "DocType", | |||||
editable_grid: 1, | |||||
engine: "InnoDB", | |||||
fields: [ | |||||
{ | |||||
fieldname: "data", | |||||
fieldtype: "Data", | |||||
in_list_view: 1, | |||||
label: "Data" | |||||
}, | |||||
{ | |||||
fieldname: "barcode", | |||||
fieldtype: "Barcode", | |||||
in_list_view: 1, | |||||
label: "Barcode" | |||||
}, | |||||
{ | |||||
fieldname: "check", | |||||
fieldtype: "Check", | |||||
in_list_view: 1, | |||||
label: "Check" | |||||
}, | |||||
{ | |||||
fieldname: "rating", | |||||
fieldtype: "Rating", | |||||
in_list_view: 1, | |||||
label: "Rating" | |||||
}, | |||||
{ | |||||
fieldname: "duration", | |||||
fieldtype: "Duration", | |||||
in_list_view: 1, | |||||
label: "Duration" | |||||
}, | |||||
{ | |||||
fieldname: "date", | |||||
fieldtype: "Date", | |||||
in_list_view: 1, | |||||
label: "Date" | |||||
} | |||||
], | |||||
links: [], | |||||
istable: 1, | |||||
modified: "2022-02-10 12:03:12.603763", | |||||
modified_by: "Administrator", | |||||
module: "Custom", | |||||
naming_rule: "By fieldname", | |||||
owner: "Administrator", | |||||
permissions: [], | |||||
sort_field: 'modified', | |||||
sort_order: 'ASC', | |||||
track_changes: 1 | |||||
}; |
@@ -20,6 +20,12 @@ export default { | |||||
label: "Child Table", | label: "Child Table", | ||||
options: "Child Table Doctype", | options: "Child Table Doctype", | ||||
reqd: 1 | reqd: 1 | ||||
}, | |||||
{ | |||||
fieldname: "child_table_1", | |||||
fieldtype: "Table", | |||||
label: "Child Table 1", | |||||
options: "Child Table Doctype 1" | |||||
} | } | ||||
], | ], | ||||
links: [], | links: [], | ||||
@@ -1,5 +1,6 @@ | |||||
import doctype_with_child_table from '../fixtures/doctype_with_child_table'; | import doctype_with_child_table from '../fixtures/doctype_with_child_table'; | ||||
import child_table_doctype from '../fixtures/child_table_doctype'; | import child_table_doctype from '../fixtures/child_table_doctype'; | ||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1'; | |||||
import doctype_to_link from '../fixtures/doctype_to_link'; | import doctype_to_link from '../fixtures/doctype_to_link'; | ||||
const doctype_to_link_name = doctype_to_link.name; | const doctype_to_link_name = doctype_to_link.name; | ||||
const child_table_doctype_name = child_table_doctype.name; | const child_table_doctype_name = child_table_doctype.name; | ||||
@@ -9,6 +10,7 @@ context('Dashboard links', () => { | |||||
cy.visit('/login'); | cy.visit('/login'); | ||||
cy.login(); | cy.login(); | ||||
cy.insert_doc('DocType', child_table_doctype, true); | cy.insert_doc('DocType', child_table_doctype, true); | ||||
cy.insert_doc('DocType', child_table_doctype_1, true); | |||||
cy.insert_doc('DocType', doctype_with_child_table, true); | cy.insert_doc('DocType', doctype_with_child_table, true); | ||||
cy.insert_doc('DocType', doctype_to_link, true); | cy.insert_doc('DocType', doctype_to_link, true); | ||||
return cy.window().its('frappe').then(frappe => { | return cy.window().its('frappe').then(frappe => { | ||||
@@ -0,0 +1,107 @@ | |||||
import doctype_with_child_table from '../fixtures/doctype_with_child_table'; | |||||
import child_table_doctype from '../fixtures/child_table_doctype'; | |||||
import child_table_doctype_1 from '../fixtures/child_table_doctype_1'; | |||||
const doctype_with_child_table_name = doctype_with_child_table.name; | |||||
context('Grid Search', () => { | |||||
before(() => { | |||||
cy.visit('/login'); | |||||
cy.login(); | |||||
cy.visit('/app/website'); | |||||
cy.insert_doc('DocType', child_table_doctype, true); | |||||
cy.insert_doc('DocType', child_table_doctype_1, true); | |||||
cy.insert_doc('DocType', doctype_with_child_table, true); | |||||
return cy.window().its('frappe').then(frappe => { | |||||
return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", { | |||||
name: doctype_with_child_table_name | |||||
}); | |||||
}); | |||||
}); | |||||
it('Test search row visibility', () => { | |||||
cy.window().its('frappe').then(frappe => { | |||||
frappe.model.user_settings.save('Doctype With Child Table', 'GridView', { | |||||
'Child Table Doctype 1': [ | |||||
{'fieldname': 'data', 'columns': 2}, | |||||
{'fieldname': 'barcode', 'columns': 1}, | |||||
{'fieldname': 'check', 'columns': 1}, | |||||
{'fieldname': 'rating', 'columns': 2}, | |||||
{'fieldname': 'duration', 'columns': 2}, | |||||
{'fieldname': 'date', 'columns': 2} | |||||
] | |||||
}); | |||||
}); | |||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`); | |||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); | |||||
cy.get('@table').find('.grid-row-check:last').click(); | |||||
cy.get('@table').find('.grid-footer').contains('Delete').click(); | |||||
cy.get('.grid-heading-row .grid-row .search').should('not.exist'); | |||||
}); | |||||
it('test search field for different fieldtypes', () => { | |||||
cy.visit(`/app/doctype-with-child-table/Test Grid Search`); | |||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); | |||||
// Index Column | |||||
cy.get('@table').find('.grid-heading-row .row-index.search input').type('3'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); | |||||
cy.get('@table').find('.grid-heading-row .row-index.search input').clear(); | |||||
// Data Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear(); | |||||
// Barcode Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear(); | |||||
// Check Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); | |||||
// Rating Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear(); | |||||
// Duration Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear(); | |||||
// Date Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear(); | |||||
}); | |||||
it('test with multiple filter', () => { | |||||
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table'); | |||||
// Data Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10); | |||||
// Barcode Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8); | |||||
// Duration Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5); | |||||
// Date Column | |||||
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-'); | |||||
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); | |||||
}); | |||||
}); |
@@ -31,5 +31,8 @@ context('List Paging', () => { | |||||
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); | 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 .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'); | |||||
}); | }); | ||||
}); | }); |
@@ -286,7 +286,7 @@ function get_watch_config() { | |||||
notify_redis({ error }); | notify_redis({ error }); | ||||
} else { | } else { | ||||
let { | let { | ||||
assets_json, | |||||
new_assets_json, | |||||
prev_assets_json | prev_assets_json | ||||
} = await write_assets_json(result.metafile); | } = await write_assets_json(result.metafile); | ||||
@@ -294,7 +294,7 @@ function get_watch_config() { | |||||
if (prev_assets_json) { | if (prev_assets_json) { | ||||
changed_files = get_rebuilt_assets( | changed_files = get_rebuilt_assets( | ||||
prev_assets_json, | prev_assets_json, | ||||
assets_json | |||||
new_assets_json | |||||
); | ); | ||||
let timestamp = new Date().toLocaleTimeString(); | let timestamp = new Date().toLocaleTimeString(); | ||||
@@ -384,6 +384,7 @@ let prev_assets_json; | |||||
let curr_assets_json; | let curr_assets_json; | ||||
async function write_assets_json(metafile) { | async function write_assets_json(metafile) { | ||||
let rtl = false; | |||||
prev_assets_json = curr_assets_json; | prev_assets_json = curr_assets_json; | ||||
let out = {}; | let out = {}; | ||||
for (let output in metafile.outputs) { | for (let output in metafile.outputs) { | ||||
@@ -392,13 +393,14 @@ async function write_assets_json(metafile) { | |||||
if (info.entryPoint) { | if (info.entryPoint) { | ||||
let key = path.basename(info.entryPoint); | let key = path.basename(info.entryPoint); | ||||
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) { | if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) { | ||||
rtl = true; | |||||
key = `rtl_${key}`; | key = `rtl_${key}`; | ||||
} | } | ||||
out[key] = asset_path; | out[key] = asset_path; | ||||
} | } | ||||
} | } | ||||
let assets_json_path = path.resolve(assets_path, "assets.json"); | |||||
let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`); | |||||
let assets_json; | let assets_json; | ||||
try { | try { | ||||
assets_json = await fs.promises.readFile(assets_json_path, "utf-8"); | assets_json = await fs.promises.readFile(assets_json_path, "utf-8"); | ||||
@@ -407,21 +409,21 @@ async function write_assets_json(metafile) { | |||||
} | } | ||||
assets_json = JSON.parse(assets_json); | assets_json = JSON.parse(assets_json); | ||||
// update with new values | // update with new values | ||||
assets_json = Object.assign({}, assets_json, out); | |||||
curr_assets_json = assets_json; | |||||
let new_assets_json = Object.assign({}, assets_json, out); | |||||
curr_assets_json = new_assets_json; | |||||
await fs.promises.writeFile( | await fs.promises.writeFile( | ||||
assets_json_path, | assets_json_path, | ||||
JSON.stringify(assets_json, null, 4) | |||||
JSON.stringify(new_assets_json, null, 4) | |||||
); | ); | ||||
await update_assets_json_in_cache(assets_json); | |||||
await update_assets_json_in_cache(); | |||||
return { | return { | ||||
assets_json, | |||||
new_assets_json, | |||||
prev_assets_json | prev_assets_json | ||||
}; | }; | ||||
} | } | ||||
function update_assets_json_in_cache(assets_json) { | |||||
function update_assets_json_in_cache() { | |||||
// update assets_json cache in redis, so that it can be read directly by python | // update assets_json cache in redis, so that it can be read directly by python | ||||
return new Promise(resolve => { | return new Promise(resolve => { | ||||
let client = get_redis_subscriber("redis_cache"); | let client = get_redis_subscriber("redis_cache"); | ||||
@@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) { | |||||
client.on("error", _ => { | client.on("error", _ => { | ||||
log_warn("Cannot connect to redis_cache to update assets_json"); | log_warn("Cannot connect to redis_cache to update assets_json"); | ||||
}); | }); | ||||
client.set("assets_json", JSON.stringify(assets_json), err => { | |||||
client.del("assets_json", err => { | |||||
client.unref(); | client.unref(); | ||||
resolve(); | resolve(); | ||||
}); | }); | ||||
@@ -978,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa | |||||
def delete_doc_if_exists(doctype, name, force=0): | def delete_doc_if_exists(doctype, name, force=0): | ||||
"""Delete document if exists.""" | """Delete document if exists.""" | ||||
if db.exists(doctype, name): | |||||
delete_doc(doctype, name, force=force) | |||||
delete_doc(doctype, name, force=force, ignore_missing=True) | |||||
def reload_doctype(doctype, force=False, reset_permissions=False): | def reload_doctype(doctype, force=False, reset_permissions=False): | ||||
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" | """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" | ||||
@@ -1252,9 +1251,10 @@ def get_newargs(fn, kwargs): | |||||
if hasattr(fn, 'fnargs'): | if hasattr(fn, 'fnargs'): | ||||
fnargs = fn.fnargs | fnargs = fn.fnargs | ||||
else: | else: | ||||
fnargs = inspect.getfullargspec(fn).args | |||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs) | |||||
varkw = inspect.getfullargspec(fn).varkw | |||||
fullargspec = inspect.getfullargspec(fn) | |||||
fnargs = fullargspec.args | |||||
fnargs.extend(fullargspec.kwonlyargs) | |||||
varkw = fullargspec.varkw | |||||
newargs = {} | newargs = {} | ||||
for a in kwargs: | for a in kwargs: | ||||
@@ -1266,7 +1266,7 @@ def get_newargs(fn, kwargs): | |||||
return newargs | return newargs | ||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True): | |||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True): | |||||
"""Create a new **Property Setter** (for overriding DocType and DocField properties). | """Create a new **Property Setter** (for overriding DocType and DocField properties). | ||||
If doctype is not specified, it will create a property setter for all fields with the | If doctype is not specified, it will create a property setter for all fields with the | ||||
@@ -1297,6 +1297,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp | |||||
'property': args.property, | 'property': args.property, | ||||
'value': args.value, | 'value': args.value, | ||||
'property_type': args.property_type or "Data", | 'property_type': args.property_type or "Data", | ||||
'is_system_generated': is_system_generated, | |||||
'__islocal': 1 | '__islocal': 1 | ||||
}) | }) | ||||
ps.flags.ignore_validate = ignore_validate | ps.flags.ignore_validate = ignore_validate | ||||
@@ -677,7 +677,9 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site | |||||
try: | try: | ||||
if not no_backup: | if not no_backup: | ||||
scheduled_backup(ignore_files=False, force=True) | |||||
click.secho(f"Taking backup of {site}", fg="green") | |||||
odb = scheduled_backup(ignore_files=False, force=True, verbose=True) | |||||
odb.print_summary() | |||||
except Exception as err: | except Exception as err: | ||||
if force: | if force: | ||||
pass | pass | ||||
@@ -692,6 +694,7 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site | |||||
click.echo("\n".join(messages)) | click.echo("\n".join(messages)) | ||||
sys.exit(1) | sys.exit(1) | ||||
click.secho("Dropping site database and user", fg="green") | |||||
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) | drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) | ||||
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') | archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') | ||||
@@ -4,8 +4,8 @@ import unittest | |||||
from urllib.parse import quote | from urllib.parse import quote | ||||
import frappe | import frappe | ||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||||
from frappe.core.doctype.communication.communication import get_emails | from frappe.core.doctype.communication.communication import get_emails | ||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||||
test_records = frappe.get_test_records('Communication') | test_records = frappe.get_test_records('Communication') | ||||
@@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase): | |||||
self.assertIn(("Note", note.name), doc_links) | self.assertIn(("Note", note.name), doc_links) | ||||
def parse_emails(self): | |||||
def test_parse_emails(self): | |||||
emails = get_emails( | emails = get_emails( | ||||
[ | [ | ||||
'comm_recipient+DocType+DocName@example.com', | 'comm_recipient+DocType+DocName@example.com', | ||||
@@ -382,7 +382,7 @@ class TestFile(unittest.TestCase): | |||||
}).insert(ignore_permissions=True) | }).insert(ignore_permissions=True) | ||||
test_file.make_thumbnail() | test_file.make_thumbnail() | ||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg') | |||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg') | |||||
# test web image without extension | # test web image without extension | ||||
test_file = frappe.get_doc({ | test_file = frappe.get_doc({ | ||||
@@ -399,7 +399,7 @@ class TestFile(unittest.TestCase): | |||||
test_file.reload() | test_file.reload() | ||||
test_file.file_url = "/files/image_small.jpg" | test_file.file_url = "/files/image_small.jpg" | ||||
test_file.make_thumbnail(suffix="xs", crop=True) | test_file.make_thumbnail(suffix="xs", crop=True) | ||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg') | |||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg') | |||||
frappe.clear_messages() | frappe.clear_messages() | ||||
test_file.db_set('thumbnail_url', None) | test_file.db_set('thumbnail_url', None) | ||||
@@ -407,7 +407,7 @@ class TestFile(unittest.TestCase): | |||||
test_file.file_url = frappe.utils.get_url('unknown.jpg') | test_file.file_url = frappe.utils.get_url('unknown.jpg') | ||||
test_file.make_thumbnail(suffix="xs") | test_file.make_thumbnail(suffix="xs") | ||||
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") | self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") | ||||
self.assertEquals(test_file.thumbnail_url, None) | |||||
self.assertEqual(test_file.thumbnail_url, None) | |||||
def test_file_unzip(self): | def test_file_unzip(self): | ||||
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') | file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') | ||||
@@ -253,8 +253,8 @@ class User(Document): | |||||
self.email_new_password(new_password) | self.email_new_password(new_password) | ||||
except frappe.OutgoingEmailError: | except frappe.OutgoingEmailError: | ||||
print(frappe.get_traceback()) | |||||
pass # email server not set, don't send email | |||||
# email server not set, don't send email | |||||
frappe.log_error(frappe.get_traceback()) | |||||
@Document.hook | @Document.hook | ||||
def validate_reset_password(self): | def validate_reset_password(self): | ||||
@@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', { | |||||
set_applicable_for_constraint: frm => { | set_applicable_for_constraint: frm => { | ||||
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes); | frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes); | ||||
if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) { | if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) { | ||||
frm.set_value('applicable_for', null); | |||||
frm.set_value('applicable_for', null, null, true); | |||||
} | } | ||||
}, | }, | ||||
@@ -7,6 +7,7 @@ | |||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"is_system_generated", | |||||
"dt", | "dt", | ||||
"module", | "module", | ||||
"label", | "label", | ||||
@@ -425,13 +426,20 @@ | |||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"label": "Module (for export)", | "label": "Module (for export)", | ||||
"options": "Module Def" | "options": "Module Def" | ||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "is_system_generated", | |||||
"fieldtype": "Check", | |||||
"label": "Is System Generated", | |||||
"read_only": 1 | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-glass", | "icon": "fa fa-glass", | ||||
"idx": 1, | "idx": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2022-02-14 15:42:21.885999", | |||||
"modified": "2022-02-28 22:22:54.893269", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Custom Field", | "name": "Custom Field", | ||||
@@ -119,7 +119,7 @@ def create_custom_field_if_values_exist(doctype, df): | |||||
frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""): | frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""): | ||||
create_custom_field(doctype, df) | create_custom_field(doctype, df) | ||||
def create_custom_field(doctype, df, ignore_validate=False): | |||||
def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True): | |||||
df = frappe._dict(df) | df = frappe._dict(df) | ||||
if not df.fieldname and df.label: | if not df.fieldname and df.label: | ||||
df.fieldname = frappe.scrub(df.label) | df.fieldname = frappe.scrub(df.label) | ||||
@@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False): | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"fieldtype": 'Data', | "fieldtype": 'Data', | ||||
"hidden": 0, | "hidden": 0, | ||||
# Looks like we always use this programatically? | |||||
# "is_standard": 1 | |||||
"is_system_generated": is_system_generated | |||||
}) | }) | ||||
custom_field.update(df) | custom_field.update(df) | ||||
custom_field.flags.ignore_validate = ignore_validate | custom_field.flags.ignore_validate = ignore_validate | ||||
@@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", { | |||||
}, | }, | ||||
onload: function(frm) { | onload: function(frm) { | ||||
frm.disable_save(); | |||||
frm.set_query("doc_type", function() { | frm.set_query("doc_type", function() { | ||||
return { | return { | ||||
translate_values: false, | translate_values: false, | ||||
@@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", { | |||||
}, | }, | ||||
refresh: function(frm) { | refresh: function(frm) { | ||||
frm.disable_save(); | |||||
frm.disable_save(true); | |||||
frm.page.clear_icons(); | frm.page.clear_icons(); | ||||
if (frm.doc.doc_type) { | if (frm.doc.doc_type) { | ||||
@@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", { | |||||
doc_type = localStorage.getItem("customize_doctype"); | doc_type = localStorage.getItem("customize_doctype"); | ||||
} | } | ||||
if (doc_type) { | if (doc_type) { | ||||
setTimeout(() => frm.set_value("doc_type", doc_type), 1000); | |||||
setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000); | |||||
} | } | ||||
}, | }, | ||||
@@ -243,7 +242,8 @@ frappe.ui.form.on("Customize Form Field", { | |||||
}, | }, | ||||
fields_add: function(frm, cdt, cdn) { | fields_add: function(frm, cdt, cdn) { | ||||
var f = frappe.model.get_doc(cdt, cdn); | var f = frappe.model.get_doc(cdt, cdn); | ||||
f.is_custom_field = 1; | |||||
f.is_system_generated = false; | |||||
f.is_custom_field = true; | |||||
} | } | ||||
}); | }); | ||||
@@ -341,11 +341,11 @@ frappe.customize_form.confirm = function(msg, frm) { | |||||
} | } | ||||
frappe.customize_form.clear_locals_and_refresh = function(frm) { | frappe.customize_form.clear_locals_and_refresh = function(frm) { | ||||
delete frm.doc.__unsaved; | |||||
// clear doctype from locals | // clear doctype from locals | ||||
frappe.model.clear_doc("DocType", frm.doc.doc_type); | frappe.model.clear_doc("DocType", frm.doc.doc_type); | ||||
delete frappe.meta.docfield_copy[frm.doc.doc_type]; | delete frappe.meta.docfield_copy[frm.doc.doc_type]; | ||||
frm.refresh(); | frm.refresh(); | ||||
} | |||||
}; | |||||
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); | extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); |
@@ -402,7 +402,7 @@ class CustomizeForm(Document): | |||||
"property": prop, | "property": prop, | ||||
"value": value, | "value": value, | ||||
"property_type": property_type | "property_type": property_type | ||||
}) | |||||
}, is_system_generated=False) | |||||
def get_existing_property_value(self, property_name, fieldname=None): | def get_existing_property_value(self, property_name, fieldname=None): | ||||
# check if there is any need to make property setter! | # check if there is any need to make property setter! | ||||
@@ -6,6 +6,7 @@ | |||||
"document_type": "Setup", | "document_type": "Setup", | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
"is_system_generated", | |||||
"help", | "help", | ||||
"sb0", | "sb0", | ||||
"doctype_or_field", | "doctype_or_field", | ||||
@@ -103,13 +104,20 @@ | |||||
{ | { | ||||
"fieldname": "section_break_9", | "fieldname": "section_break_9", | ||||
"fieldtype": "Section Break" | "fieldtype": "Section Break" | ||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "is_system_generated", | |||||
"fieldtype": "Check", | |||||
"label": "Is System Generated", | |||||
"read_only": 1 | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-glass", | "icon": "fa fa-glass", | ||||
"idx": 1, | "idx": 1, | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2021-12-14 14:15:41.929071", | |||||
"modified": "2022-02-28 22:24:12.377693", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Property Setter", | "name": "Property Setter", | ||||
@@ -18,7 +18,8 @@ def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False | |||||
def drop_user_and_database(db_name, root_login=None, root_password=None): | def drop_user_and_database(db_name, root_login=None, root_password=None): | ||||
import frappe | import frappe | ||||
if frappe.conf.db_type == 'postgres': | if frappe.conf.db_type == 'postgres': | ||||
pass | |||||
import frappe.database.postgres.setup_db | |||||
return frappe.database.postgres.setup_db.drop_user_and_database(db_name, root_login, root_password) | |||||
else: | else: | ||||
import frappe.database.mariadb.setup_db | import frappe.database.mariadb.setup_db | ||||
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password) | return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password) | ||||
@@ -119,6 +119,9 @@ class Database(object): | |||||
if not run: | if not run: | ||||
return query | return query | ||||
# remove \n \t from start and end of query | |||||
query = re.sub(r'^\s*|\s*$', '', query) | |||||
if re.search(r'ifnull\(', query, flags=re.IGNORECASE): | if re.search(r'ifnull\(', query, flags=re.IGNORECASE): | ||||
# replaces ifnull in query with coalesce | # replaces ifnull in query with coalesce | ||||
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) | query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) | ||||
@@ -384,7 +387,7 @@ class Database(object): | |||||
""" | """ | ||||
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | ||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct) | |||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) | |||||
if not run: | if not run: | ||||
return ret | return ret | ||||
@@ -393,7 +396,7 @@ class Database(object): | |||||
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, | def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, | ||||
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, | debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, | ||||
run=True, pluck=False, distinct=False): | |||||
run=True, pluck=False, distinct=False, limit=None): | |||||
"""Returns multiple document properties. | """Returns multiple document properties. | ||||
:param doctype: DocType name. | :param doctype: DocType name. | ||||
@@ -423,14 +426,15 @@ class Database(object): | |||||
if isinstance(filters, list): | if isinstance(filters, list): | ||||
out = self._get_value_for_many_names( | out = self._get_value_for_many_names( | ||||
doctype, | |||||
filters, | |||||
fieldname, | |||||
order_by, | |||||
doctype=doctype, | |||||
names=filters, | |||||
field=fieldname, | |||||
order_by=order_by, | |||||
debug=debug, | debug=debug, | ||||
run=run, | run=run, | ||||
pluck=pluck, | pluck=pluck, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit=limit, | |||||
) | ) | ||||
else: | else: | ||||
@@ -444,17 +448,18 @@ class Database(object): | |||||
if order_by: | if order_by: | ||||
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by | order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by | ||||
out = self._get_values_from_table( | out = self._get_values_from_table( | ||||
fields, | |||||
filters, | |||||
doctype, | |||||
as_dict, | |||||
debug, | |||||
order_by, | |||||
update, | |||||
fields=fields, | |||||
filters=filters, | |||||
doctype=doctype, | |||||
as_dict=as_dict, | |||||
debug=debug, | |||||
order_by=order_by, | |||||
update=update, | |||||
for_update=for_update, | for_update=for_update, | ||||
run=run, | run=run, | ||||
pluck=pluck, | pluck=pluck, | ||||
distinct=distinct | |||||
distinct=distinct, | |||||
limit=limit, | |||||
) | ) | ||||
except Exception as e: | except Exception as e: | ||||
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): | if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): | ||||
@@ -623,6 +628,7 @@ class Database(object): | |||||
run=True, | run=True, | ||||
pluck=False, | pluck=False, | ||||
distinct=False, | distinct=False, | ||||
limit=None, | |||||
): | ): | ||||
field_objects = [] | field_objects = [] | ||||
@@ -641,6 +647,7 @@ class Database(object): | |||||
field_objects=field_objects, | field_objects=field_objects, | ||||
fields=fields, | fields=fields, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit=limit, | |||||
) | ) | ||||
if ( | if ( | ||||
fields == "*" | fields == "*" | ||||
@@ -654,7 +661,7 @@ class Database(object): | |||||
) | ) | ||||
return r | return r | ||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False): | |||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): | |||||
names = list(filter(None, names)) | names = list(filter(None, names)) | ||||
if names: | if names: | ||||
return self.get_all( | return self.get_all( | ||||
@@ -667,6 +674,7 @@ class Database(object): | |||||
as_list=1, | as_list=1, | ||||
run=run, | run=run, | ||||
distinct=distinct, | distinct=distinct, | ||||
limit_page_length=limit | |||||
) | ) | ||||
else: | else: | ||||
return {} | return {} | ||||
@@ -882,27 +890,39 @@ class Database(object): | |||||
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) | return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) | ||||
def exists(self, dt, dn=None, cache=False): | def exists(self, dt, dn=None, cache=False): | ||||
"""Returns true if document exists. | |||||
"""Return the document name of a matching document, or None. | |||||
:param dt: DocType name. | |||||
:param dn: Document name or filter dict.""" | |||||
if isinstance(dt, str): | |||||
if dt!="DocType" and dt==dn: | |||||
return True # single always exists (!) | |||||
try: | |||||
return self.get_value(dt, dn, "name", cache=cache) | |||||
except Exception: | |||||
return None | |||||
Note: `cache` only works if `dt` and `dn` are of type `str`. | |||||
elif isinstance(dt, dict) and dt.get('doctype'): | |||||
try: | |||||
conditions = [] | |||||
for d in dt: | |||||
if d == 'doctype': continue | |||||
conditions.append([d, '=', dt[d]]) | |||||
return self.get_all(dt['doctype'], filters=conditions, as_list=1) | |||||
except Exception: | |||||
return None | |||||
## Examples | |||||
Pass doctype and docname (only in this case we can cache the result) | |||||
``` | |||||
exists("User", "jane@example.org", cache=True) | |||||
``` | |||||
Pass a dict of filters including the `"doctype"` key: | |||||
``` | |||||
exists({"doctype": "User", "full_name": "Jane Doe"}) | |||||
``` | |||||
Pass the doctype and a dict of filters: | |||||
``` | |||||
exists("User", {"full_name": "Jane Doe"}) | |||||
``` | |||||
""" | |||||
if dt != "DocType" and dt == dn: | |||||
# single always exists (!) | |||||
return dn | |||||
if isinstance(dt, dict): | |||||
dt = dt.copy() # don't modify the original dict | |||||
dt, dn = dt.pop("doctype"), dt | |||||
return self.get_value(dt, dn, ignore=True, cache=cache) | |||||
def count(self, dt, filters=None, debug=False, cache=False): | def count(self, dt, filters=None, debug=False, cache=False): | ||||
"""Returns `COUNT(*)` for given DocType and filters.""" | """Returns `COUNT(*)` for given DocType and filters.""" | ||||
@@ -95,3 +95,11 @@ def get_root_connection(root_login=None, root_password=None): | |||||
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password) | frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password) | ||||
return frappe.local.flags.root_connection | return frappe.local.flags.root_connection | ||||
def drop_user_and_database(db_name, root_login, root_password): | |||||
root_conn = get_root_connection(frappe.flags.root_login or root_login, frappe.flags.root_password or root_password) | |||||
root_conn.commit() | |||||
root_conn.sql(f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", (db_name, )) | |||||
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}") | |||||
root_conn.sql(f"DROP USER IF EXISTS {db_name}") |
@@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv | |||||
from frappe.utils.xlsxutils import make_xlsx | from frappe.utils.xlsxutils import make_xlsx | ||||
from frappe.desk.query_report import build_xlsx_data | from frappe.desk.query_report import build_xlsx_data | ||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 | |||||
class AutoEmailReport(Document): | class AutoEmailReport(Document): | ||||
def autoname(self): | def autoname(self): | ||||
@@ -46,6 +44,8 @@ class AutoEmailReport(Document): | |||||
def validate_report_count(self): | def validate_report_count(self): | ||||
'''check that there are only 3 enabled reports per user''' | '''check that there are only 3 enabled reports per user''' | ||||
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] | count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] | ||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 | |||||
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): | if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): | ||||
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) | frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) | ||||
@@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase): | |||||
self.assertTrue(email_queue) | self.assertTrue(email_queue) | ||||
# check if description is changed after alert since set_property_after_alert is set | # check if description is changed after alert since set_property_after_alert is set | ||||
self.assertEquals(todo.description, 'Changed by Notification') | |||||
self.assertEqual(todo.description, 'Changed by Notification') | |||||
recipients = [d.recipient for d in email_queue.recipients] | recipients = [d.recipient for d in email_queue.recipients] | ||||
self.assertTrue('test2@example.com' in recipients) | self.assertTrue('test2@example.com' in recipients) | ||||
@@ -225,11 +225,10 @@ def ping(): | |||||
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | ||||
"""run a whitelisted controller method""" | """run a whitelisted controller method""" | ||||
import json | |||||
import inspect | |||||
from inspect import getfullargspec | |||||
if not args: | |||||
args = arg or "" | |||||
if not args and arg: | |||||
args = arg | |||||
if dt: # not called from a doctype (from a page) | if dt: # not called from a doctype (from a page) | ||||
if not dn: | if not dn: | ||||
@@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | |||||
doc = frappe.get_doc(dt, dn) | doc = frappe.get_doc(dt, dn) | ||||
else: | else: | ||||
if isinstance(docs, str): | |||||
docs = json.loads(docs) | |||||
docs = frappe.parse_json(docs) | |||||
doc = frappe.get_doc(docs) | doc = frappe.get_doc(docs) | ||||
doc._original_modified = doc.modified | doc._original_modified = doc.modified | ||||
doc.check_if_latest() | doc.check_if_latest() | ||||
@@ -248,16 +245,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): | |||||
throw_permission_error() | throw_permission_error() | ||||
try: | try: | ||||
args = json.loads(args) | |||||
args = frappe.parse_json(args) | |||||
except ValueError: | except ValueError: | ||||
args = args | |||||
pass | |||||
method_obj = getattr(doc, method) | method_obj = getattr(doc, method) | ||||
fn = getattr(method_obj, '__func__', method_obj) | fn = getattr(method_obj, '__func__', method_obj) | ||||
is_whitelisted(fn) | is_whitelisted(fn) | ||||
is_valid_http_method(fn) | is_valid_http_method(fn) | ||||
fnargs = inspect.getfullargspec(method_obj).args | |||||
fnargs = getfullargspec(method_obj).args | |||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): | if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): | ||||
response = doc.run_method(method) | response = doc.run_method(method) | ||||
@@ -963,7 +963,7 @@ class BaseDocument(object): | |||||
from frappe.model.meta import get_default_df | from frappe.model.meta import get_default_df | ||||
df = get_default_df(fieldname) | df = get_default_df(fieldname) | ||||
if not currency and df: | |||||
if df.fieldtype == "Currency" and not currency: | |||||
currency = self.get(df.get("options")) | currency = self.get(df.get("options")) | ||||
if not frappe.db.exists('Currency', currency, cache=True): | if not frappe.db.exists('Currency', currency, cache=True): | ||||
currency = None | currency = None | ||||
@@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options | |||||
frappe.patches.v13_0.replace_old_data_import # 2020-06-24 | frappe.patches.v13_0.replace_old_data_import # 2020-06-24 | ||||
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts | frappe.patches.v13_0.create_custom_dashboards_cards_and_charts | ||||
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart | frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart | ||||
frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 | |||||
frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15 | |||||
frappe.patches.v13_0.generate_theme_files_in_public_folder | frappe.patches.v13_0.generate_theme_files_in_public_folder | ||||
frappe.patches.v13_0.increase_password_length | frappe.patches.v13_0.increase_password_length | ||||
frappe.patches.v12_0.fix_email_id_formatting | frappe.patches.v12_0.fix_email_id_formatting | ||||
@@ -197,4 +197,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 | |||||
frappe.patches.v14_0.update_github_endpoints #08-11-2021 | frappe.patches.v14_0.update_github_endpoints #08-11-2021 | ||||
frappe.patches.v14_0.remove_db_aggregation | frappe.patches.v14_0.remove_db_aggregation | ||||
frappe.patches.v14_0.update_color_names_in_kanban_board_column | frappe.patches.v14_0.update_color_names_in_kanban_board_column | ||||
frappe.patches.v14_0.update_is_system_generated_flag | |||||
frappe.patches.v14_0.update_auto_account_deletion_duration | frappe.patches.v14_0.update_auto_account_deletion_duration |
@@ -0,0 +1,17 @@ | |||||
import frappe | |||||
def execute(): | |||||
# assuming all customization generated by Admin is system generated customization | |||||
custom_field = frappe.qb.DocType("Custom Field") | |||||
( | |||||
frappe.qb.update(custom_field) | |||||
.set(custom_field.is_system_generated, True) | |||||
.where(custom_field.owner == 'Administrator').run() | |||||
) | |||||
property_setter = frappe.qb.DocType("Property Setter") | |||||
( | |||||
frappe.qb.update(property_setter) | |||||
.set(property_setter.is_system_generated, True) | |||||
.where(property_setter.owner == 'Administrator').run() | |||||
) |
@@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype): | |||||
from frappe.core.utils import find | from frappe.core.utils import find | ||||
parent_meta = frappe.get_meta(parent_doctype) | parent_meta = frappe.get_meta(parent_doctype) | ||||
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) | child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) | ||||
return not parent_meta.istable and child_table_field_exists | |||||
return not parent_meta.istable and child_table_field_exists |
@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
if(this.frm) { | if(this.frm) { | ||||
me.parse_validate_and_set_in_model(null); | me.parse_validate_and_set_in_model(null); | ||||
me.refresh(); | me.refresh(); | ||||
me.frm.attachments.remove_attachment_by_filename(me.value, function() { | |||||
me.parse_validate_and_set_in_model(null); | |||||
me.frm.attachments.remove_attachment_by_filename(me.value, async () => { | |||||
await me.parse_validate_and_set_in_model(null); | |||||
me.refresh(); | me.refresh(); | ||||
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); | me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); | ||||
}); | }); | ||||
@@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro | |||||
return this.value || null; | return this.value || null; | ||||
} | } | ||||
on_upload_complete(attachment) { | |||||
async on_upload_complete(attachment) { | |||||
if(this.frm) { | if(this.frm) { | ||||
this.parse_validate_and_set_in_model(attachment.file_url); | |||||
await this.parse_validate_and_set_in_model(attachment.file_url); | |||||
this.frm.attachments.update_attachment(attachment); | this.frm.attachments.update_attachment(attachment); | ||||
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); | this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); | ||||
} | } | ||||
@@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline { | |||||
let edit_box = this.make_editable(edit_wrapper); | let edit_box = this.make_editable(edit_wrapper); | ||||
let content_wrapper = comment_wrapper.find('.content'); | let content_wrapper = comment_wrapper.find('.content'); | ||||
let more_actions_wrapper = comment_wrapper.find('.more-actions'); | let more_actions_wrapper = comment_wrapper.find('.more-actions'); | ||||
if (frappe.model.can_delete("Comment")) { | |||||
if (frappe.model.can_delete("Comment") && ( | |||||
frappe.session.user == doc.owner || | |||||
frappe.user.has_role("System Manager") | |||||
)) { | |||||
const delete_option = $(` | const delete_option = $(` | ||||
<li> | <li> | ||||
<a class="dropdown-item"> | <a class="dropdown-item"> | ||||
@@ -246,10 +246,12 @@ frappe.ui.form.Form = class FrappeForm { | |||||
var me = this; | var me = this; | ||||
// on main doc | // on main doc | ||||
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { | |||||
frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) { | |||||
// set input | // set input | ||||
if (cstr(doc.name) === me.docname) { | if (cstr(doc.name) === me.docname) { | ||||
me.dirty(); | |||||
if (!skip_dirty_trigger) { | |||||
me.dirty(); | |||||
} | |||||
let field = me.fields_dict[fieldname]; | let field = me.fields_dict[fieldname]; | ||||
field && field.refresh(fieldname); | field && field.refresh(fieldname); | ||||
@@ -953,10 +955,12 @@ frappe.ui.form.Form = class FrappeForm { | |||||
this.toolbar.set_primary_action(); | this.toolbar.set_primary_action(); | ||||
} | } | ||||
disable_save() { | |||||
disable_save(set_dirty=false) { | |||||
// IMPORTANT: this function should be called in refresh event | // IMPORTANT: this function should be called in refresh event | ||||
this.save_disabled = true; | this.save_disabled = true; | ||||
this.toolbar.current_status = null; | this.toolbar.current_status = null; | ||||
// field changes should make form dirty | |||||
this.set_dirty = set_dirty; | |||||
this.page.clear_primary_action(); | this.page.clear_primary_action(); | ||||
} | } | ||||
@@ -1447,7 +1451,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
return doc; | return doc; | ||||
} | } | ||||
set_value(field, value, if_missing) { | |||||
set_value(field, value, if_missing, skip_dirty_trigger=false) { | |||||
var me = this; | var me = this; | ||||
var _set = function(f, v) { | var _set = function(f, v) { | ||||
var fieldobj = me.fields_dict[f]; | var fieldobj = me.fields_dict[f]; | ||||
@@ -1467,7 +1471,7 @@ frappe.ui.form.Form = class FrappeForm { | |||||
me.refresh_field(f); | me.refresh_field(f); | ||||
return Promise.resolve(); | return Promise.resolve(); | ||||
} else { | } else { | ||||
return frappe.model.set_value(me.doctype, me.doc.name, f, v); | |||||
return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, skip_dirty_trigger); | |||||
} | } | ||||
} | } | ||||
} else { | } else { | ||||
@@ -35,7 +35,7 @@ export default class Grid { | |||||
&& this.frm.meta.__form_grid_templates[this.df.fieldname]) { | && this.frm.meta.__form_grid_templates[this.df.fieldname]) { | ||||
this.template = this.frm.meta.__form_grid_templates[this.df.fieldname]; | this.template = this.frm.meta.__form_grid_templates[this.df.fieldname]; | ||||
} | } | ||||
this.filter = {}; | |||||
this.is_grid = true; | this.is_grid = true; | ||||
this.debounced_refresh = this.refresh.bind(this); | this.debounced_refresh = this.refresh.bind(this); | ||||
this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100); | this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100); | ||||
@@ -274,6 +274,8 @@ export default class Grid { | |||||
} | } | ||||
make_head() { | make_head() { | ||||
if (this.prevent_build) return; | |||||
// labels | // labels | ||||
if (this.header_row) { | if (this.header_row) { | ||||
$(this.parent).find(".grid-heading-row .grid-row").remove(); | $(this.parent).find(".grid-heading-row .grid-row").remove(); | ||||
@@ -286,12 +288,42 @@ export default class Grid { | |||||
grid: this, | grid: this, | ||||
configure_columns: true | configure_columns: true | ||||
}); | }); | ||||
this.header_search = new GridRow({ | |||||
parent: $(this.parent).find(".grid-heading-row"), | |||||
parent_df: this.df, | |||||
docfields: this.docfields, | |||||
frm: this.frm, | |||||
grid: this, | |||||
show_search: true | |||||
}); | |||||
Object.keys(this.filter).length !== 0 && | |||||
this.update_search_columns(); | |||||
} | |||||
update_search_columns() { | |||||
for (const field in this.filter) { | |||||
if (this.filter[field] && !this.header_search.search_columns[field]) { | |||||
delete this.filter[field]; | |||||
this.data = this.get_data(Object.keys(this.filter).length !== 0); | |||||
break; | |||||
} | |||||
if (this.filter[field] && this.filter[field].value) { | |||||
let $input = this.header_search.row_index.find('input'); | |||||
if (field && field !== 'row-index') { | |||||
$input = this.header_search.search_columns[field].find('input'); | |||||
} | |||||
$input.val(this.filter[field].value); | |||||
} | |||||
} | |||||
} | } | ||||
refresh(force) { | |||||
refresh() { | |||||
if (this.frm && this.frm.setting_dependency) return; | if (this.frm && this.frm.setting_dependency) return; | ||||
this.data = this.get_data(); | |||||
this.data = this.get_data(Object.keys(this.filter).length !== 0); | |||||
!this.wrapper && this.make(); | !this.wrapper && this.make(); | ||||
let $rows = $(this.parent).find('.rows'); | let $rows = $(this.parent).find('.rows'); | ||||
@@ -453,7 +485,7 @@ export default class Grid { | |||||
} | } | ||||
make_sortable($rows) { | make_sortable($rows) { | ||||
new Sortable($rows.get(0), { | |||||
this.grid_sortable = new Sortable($rows.get(0), { | |||||
group: { name: this.df.fieldname }, | group: { name: this.df.fieldname }, | ||||
handle: '.sortable-handle', | handle: '.sortable-handle', | ||||
draggable: '.grid-row', | draggable: '.grid-row', | ||||
@@ -484,14 +516,78 @@ export default class Grid { | |||||
$(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]); | $(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]); | ||||
} | } | ||||
get_data() { | |||||
var data = this.frm ? | |||||
this.frm.doc[this.df.fieldname] || [] | |||||
: this.df.data || this.get_modal_data(); | |||||
// data.sort(function(a, b) { return a.idx - b.idx}); | |||||
get_data(filter_field) { | |||||
let data = []; | |||||
if (filter_field) { | |||||
data = this.get_filtered_data(); | |||||
} else { | |||||
data = this.frm ? | |||||
this.frm.doc[this.df.fieldname] || [] | |||||
: this.df.data || this.get_modal_data(); | |||||
} | |||||
return data; | return data; | ||||
} | } | ||||
get_filtered_data() { | |||||
if (!this.frm) return; | |||||
let all_data = this.frm.doc[this.df.fieldname]; | |||||
for (const field in this.filter) { | |||||
all_data = all_data.filter(data => { | |||||
let {df, value} = this.filter[field]; | |||||
return this.get_data_based_on_fieldtype(df, data, value.toLowerCase()); | |||||
}); | |||||
} | |||||
return all_data; | |||||
} | |||||
get_data_based_on_fieldtype(df, data, value) { | |||||
let fieldname = df.fieldname; | |||||
let fieldtype = df.fieldtype; | |||||
let fieldvalue = data[fieldname]; | |||||
if (fieldtype === "Check") { | |||||
value = frappe.utils.string_to_boolean(value); | |||||
return (Boolean(fieldvalue) === value) && data; | |||||
} else if (fieldtype === "Sr No" && data.idx.toString().includes(value)) { | |||||
return data; | |||||
} else if (fieldtype === "Duration" && fieldvalue) { | |||||
let formatted_duration = frappe.utils.get_formatted_duration(fieldvalue); | |||||
if (formatted_duration.includes(value)) { | |||||
return data; | |||||
} | |||||
} else if (fieldtype === "Barcode" && fieldvalue) { | |||||
let barcode = fieldvalue.startsWith('<svg') ? | |||||
$(fieldvalue).attr('data-barcode-value') : fieldvalue; | |||||
if (barcode.toLowerCase().includes(value)) { | |||||
return data; | |||||
} | |||||
} else if (["Datetime", "Date"].includes(fieldtype) && fieldvalue) { | |||||
let user_formatted_date = frappe.datetime.str_to_user(fieldvalue); | |||||
if (user_formatted_date.includes(value)) { | |||||
return data; | |||||
} | |||||
} else if (["Currency", "Float", "Int", "Percent", "Rating"].includes(fieldtype)) { | |||||
let num = fieldvalue || 0; | |||||
if (fieldtype === "Rating") { | |||||
let out_of_rating = parseInt(df.options) || 5; | |||||
num = num * out_of_rating; | |||||
} | |||||
if (num.toString().indexOf(value) > -1) { | |||||
return data; | |||||
} | |||||
} else if (fieldvalue && fieldvalue.toLowerCase().includes(value)) { | |||||
return data; | |||||
} | |||||
} | |||||
get_modal_data() { | get_modal_data() { | ||||
return this.df.get_data ? this.df.get_data().filter(data => { | return this.df.get_data ? this.df.get_data().filter(data => { | ||||
if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) { | if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) { | ||||
@@ -775,18 +871,19 @@ export default class Grid { | |||||
} | } | ||||
setup_user_defined_columns() { | setup_user_defined_columns() { | ||||
if (this.frm) { | |||||
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); | |||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { | |||||
this.user_defined_columns = user_settings[this.doctype].map(row => { | |||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname); | |||||
if (column) { | |||||
column.in_list_view = 1; | |||||
column.columns = row.columns; | |||||
return column; | |||||
} | |||||
}); | |||||
} | |||||
if (!this.frm) return; | |||||
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); | |||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { | |||||
this.user_defined_columns = user_settings[this.doctype].map(row => { | |||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname); | |||||
if (column) { | |||||
column.in_list_view = 1; | |||||
column.columns = row.columns; | |||||
return column; | |||||
} | |||||
}); | |||||
} | } | ||||
} | } | ||||
@@ -8,7 +8,7 @@ export default class GridRow { | |||||
this.set_docfields(); | this.set_docfields(); | ||||
this.columns = {}; | this.columns = {}; | ||||
this.columns_list = []; | this.columns_list = []; | ||||
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">'; | |||||
this.row_check_html = '<input type="checkbox" class="grid-row-check">'; | |||||
this.make(); | this.make(); | ||||
} | } | ||||
make() { | make() { | ||||
@@ -204,23 +204,65 @@ export default class GridRow { | |||||
})); | })); | ||||
} | } | ||||
render_row(refresh) { | render_row(refresh) { | ||||
var me = this; | |||||
if (this.show_search && !this.show_search_row()) return; | |||||
let me = this; | |||||
this.set_row_index(); | this.set_row_index(); | ||||
// index (1, 2, 3 etc) | // index (1, 2, 3 etc) | ||||
if(!this.row_index) { | |||||
if (!this.row_index && !this.show_search) { | |||||
// REDESIGN-TODO: Make translation contextual, this No is Number | // REDESIGN-TODO: Make translation contextual, this No is Number | ||||
var txt = (this.doc ? this.doc.idx : __("No.")); | var txt = (this.doc ? this.doc.idx : __("No.")); | ||||
this.row_index = $( | |||||
`<div class="row-index sortable-handle col"> | |||||
this.row_check = $( | |||||
`<div class="row-check sortable-handle col"> | |||||
${this.row_check_html} | ${this.row_check_html} | ||||
<span class="hidden-xs">${txt}</span></div>`) | |||||
</div>`) | |||||
.appendTo(this.row); | |||||
this.row_index = $( | |||||
`<div class="row-index sortable-handle col hidden-xs"> | |||||
<span>${txt}</span> | |||||
</div>`) | |||||
.appendTo(this.row) | .appendTo(this.row) | ||||
.on('click', function(e) { | .on('click', function(e) { | ||||
if(!$(e.target).hasClass('grid-row-check')) { | if(!$(e.target).hasClass('grid-row-check')) { | ||||
me.toggle_view(); | me.toggle_view(); | ||||
} | } | ||||
}); | }); | ||||
} else if (this.show_search) { | |||||
this.row_check = $( | |||||
`<div class="row-check col search"></div>` | |||||
).appendTo(this.row); | |||||
this.row_index = $( | |||||
`<div class="row-index col search hidden-xs"> | |||||
<input type="text" class="form-control input-xs text-center" > | |||||
</div>` | |||||
).appendTo(this.row); | |||||
this.row_index.find('input').on('keyup', frappe.utils.debounce((e) => { | |||||
let df = { | |||||
fieldtype: "Sr No" | |||||
}; | |||||
this.grid.filter['row-index'] = { | |||||
df: df, | |||||
value: e.target.value | |||||
}; | |||||
if (e.target.value == "") { | |||||
delete this.grid.filter['row-index']; | |||||
} | |||||
this.grid.grid_sortable | |||||
.option('disabled', Object.keys(this.grid.filter).length !== 0); | |||||
this.grid.prevent_build = true; | |||||
me.grid.refresh(); | |||||
this.grid.prevent_build = false; | |||||
}, 500)); | |||||
frappe.utils.only_allow_num_decimal(this.row_index.find('input')); | |||||
} else { | } else { | ||||
this.row_index.find('span').html(txt); | this.row_index.find('span').html(txt); | ||||
} | } | ||||
@@ -546,6 +588,7 @@ export default class GridRow { | |||||
setup_columns() { | setup_columns() { | ||||
this.focus_set = false; | this.focus_set = false; | ||||
this.search_columns = {}; | |||||
this.grid.setup_visible_columns(); | this.grid.setup_visible_columns(); | ||||
this.grid.visible_columns.forEach((col, ci) => { | this.grid.visible_columns.forEach((col, ci) => { | ||||
@@ -561,8 +604,10 @@ export default class GridRow { | |||||
txt = __(txt); | txt = __(txt); | ||||
} | } | ||||
let column; | let column; | ||||
if (!this.columns[df.fieldname]) { | |||||
if (!this.columns[df.fieldname] && !this.show_search) { | |||||
column = this.make_column(df, colsize, txt, ci); | column = this.make_column(df, colsize, txt, ci); | ||||
} else if (!this.columns[df.fieldname] && this.show_search) { | |||||
column = this.make_search_column(df, colsize); | |||||
} else { | } else { | ||||
column = this.columns[df.fieldname]; | column = this.columns[df.fieldname]; | ||||
this.refresh_field(df.fieldname, txt); | this.refresh_field(df.fieldname, txt); | ||||
@@ -580,6 +625,77 @@ export default class GridRow { | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
if (this.show_search) { | |||||
// last empty column | |||||
$(`<div class="col grid-static-col col-xs-1"></div>`) | |||||
.appendTo(this.row); | |||||
} | |||||
} | |||||
show_search_row() { | |||||
// show or remove search columns based on grid rows | |||||
this.show_search = this.frm && this.frm.doc && | |||||
this.frm.doc[this.grid.df.fieldname] && | |||||
this.frm.doc[this.grid.df.fieldname].length >= 20; | |||||
!this.show_search && this.wrapper.remove(); | |||||
return this.show_search; | |||||
} | |||||
make_search_column(df, colsize) { | |||||
let title = ""; | |||||
let input_class = ""; | |||||
let is_disabled = ""; | |||||
if (["Text", "Small Text"].includes(df.fieldtype)) { | |||||
input_class = "grid-overflow-no-ellipsis"; | |||||
} else if (["Int", "Currency", "Float", "Percent"].includes(df.fieldtype)) { | |||||
input_class = "text-right"; | |||||
} else if (df.fieldtype === "Check") { | |||||
title = __("1 = True & 0 = False"); | |||||
input_class = "text-center"; | |||||
} else if (df.fieldtype === 'Password') { | |||||
is_disabled = 'disabled'; | |||||
title = __('Password cannot be filtered'); | |||||
} | |||||
let $col = $('<div class="col grid-static-col col-xs-'+colsize+' search"></div>') | |||||
.appendTo(this.row); | |||||
let $search_input = $(` | |||||
<input | |||||
type="text" | |||||
class="form-control input-xs ${input_class}" | |||||
title="${title}" | |||||
data-fieldtype="${df.fieldtype}" | |||||
${is_disabled} | |||||
> | |||||
`).appendTo($col); | |||||
this.search_columns[df.fieldname] = $col; | |||||
$search_input.on('keyup', frappe.utils.debounce((e) => { | |||||
this.grid.filter[df.fieldname] = { | |||||
df: df, | |||||
value: e.target.value | |||||
}; | |||||
if (e.target.value == '') { | |||||
delete this.grid.filter[df.fieldname]; | |||||
} | |||||
this.grid.grid_sortable | |||||
.option('disabled', Object.keys(this.grid.filter).length !== 0); | |||||
this.grid.prevent_build = true; | |||||
this.grid.refresh(); | |||||
this.grid.prevent_build = false; | |||||
}, 500)); | |||||
["Currency", "Float", "Int", "Percent", "Rating"].includes(df.fieldtype) && | |||||
frappe.utils.only_allow_num_decimal($search_input); | |||||
return $col; | |||||
} | } | ||||
make_column(df, colsize, txt, ci) { | make_column(df, colsize, txt, ci) { | ||||
@@ -534,14 +534,14 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
}); | }); | ||||
} | } | ||||
show_title_as_dirty() { | show_title_as_dirty() { | ||||
if(this.frm.save_disabled) | |||||
if (this.frm.save_disabled && !this.frm.set_dirty) | |||||
return; | return; | ||||
if(this.frm.doc.__unsaved) { | |||||
if (this.frm.is_dirty()) { | |||||
this.page.set_indicator(__("Not Saved"), "orange"); | this.page.set_indicator(__("Not Saved"), "orange"); | ||||
} | } | ||||
$(this.frm.wrapper).attr("data-state", this.frm.doc.__unsaved ? "dirty" : "clean"); | |||||
$(this.frm.wrapper).attr("data-state", this.frm.is_dirty() ? "dirty" : "clean"); | |||||
} | } | ||||
show_jump_to_field_dialog() { | show_jump_to_field_dialog() { | ||||
@@ -375,7 +375,7 @@ export default class ListSettings { | |||||
let me = this; | let me = this; | ||||
if (me.removed_fields) { | if (me.removed_fields) { | ||||
me.removed_fields.concat(fields); | |||||
me.removed_fields = me.removed_fields.concat(fields); | |||||
} else { | } else { | ||||
me.removed_fields = fields; | me.removed_fields = fields; | ||||
} | } | ||||
@@ -412,7 +412,7 @@ $.extend(frappe.model, { | |||||
} | } | ||||
}, | }, | ||||
set_value: function(doctype, docname, fieldname, value, fieldtype) { | |||||
set_value: function(doctype, docname, fieldname, value, fieldtype, skip_dirty_trigger=false) { | |||||
/* help: Set a value locally (if changed) and execute triggers */ | /* help: Set a value locally (if changed) and execute triggers */ | ||||
var doc; | var doc; | ||||
@@ -438,11 +438,11 @@ $.extend(frappe.model, { | |||||
} | } | ||||
doc[key] = value; | doc[key] = value; | ||||
tasks.push(() => frappe.model.trigger(key, value, doc)); | |||||
tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger)); | |||||
} else { | } else { | ||||
// execute link triggers (want to reselect to execute triggers) | // execute link triggers (want to reselect to execute triggers) | ||||
if(in_list(["Link", "Dynamic Link"], fieldtype) && doc) { | if(in_list(["Link", "Dynamic Link"], fieldtype) && doc) { | ||||
tasks.push(() => frappe.model.trigger(key, value, doc)); | |||||
tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger)); | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
@@ -467,7 +467,7 @@ $.extend(frappe.model, { | |||||
frappe.model.events[doctype][fieldname].push(fn); | frappe.model.events[doctype][fieldname].push(fn); | ||||
}, | }, | ||||
trigger: function(fieldname, value, doc) { | |||||
trigger: function(fieldname, value, doc, skip_dirty_trigger=false) { | |||||
const tasks = []; | const tasks = []; | ||||
function enqueue_events(events) { | function enqueue_events(events) { | ||||
@@ -477,7 +477,7 @@ $.extend(frappe.model, { | |||||
if (!fn) continue; | if (!fn) continue; | ||||
tasks.push(() => { | tasks.push(() => { | ||||
const return_value = fn(fieldname, value, doc); | |||||
const return_value = fn(fieldname, value, doc, skip_dirty_trigger); | |||||
// if the trigger returns a promise, return it, | // if the trigger returns a promise, return it, | ||||
// or use the default promise frappe.after_ajax | // or use the default promise frappe.after_ajax | ||||
@@ -47,13 +47,17 @@ frappe.ui.Page = class Page { | |||||
} | } | ||||
setup_scroll_handler() { | setup_scroll_handler() { | ||||
window.addEventListener('scroll', () => { | |||||
if (document.documentElement.scrollTop) { | |||||
$('.page-head').toggleClass('drop-shadow', true); | |||||
let last_scroll = 0; | |||||
window.addEventListener('scroll', frappe.utils.throttle(() => { | |||||
$('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop); | |||||
let current_scroll = document.documentElement.scrollTop; | |||||
if (current_scroll > 0 && last_scroll <= current_scroll) { | |||||
$('.page-head').css("top", "-15px"); | |||||
} else { | } else { | ||||
$('.page-head').removeClass('drop-shadow'); | |||||
$('.page-head').css("top", "var(--navbar-height)"); | |||||
} | } | ||||
}); | |||||
last_scroll = current_scroll; | |||||
}), 500); | |||||
} | } | ||||
get_empty_state(title, message, primary_action) { | get_empty_state(title, message, primary_action) { | ||||
@@ -231,7 +231,7 @@ Object.assign(frappe.utils, { | |||||
if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) { | if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) { | ||||
part.push(t); | part.push(t); | ||||
} else { | } else { | ||||
out.concat(part); | |||||
out = out.concat(part); | |||||
out.push(t); | out.push(t); | ||||
part = []; | part = []; | ||||
} | } | ||||
@@ -1102,7 +1102,7 @@ Object.assign(frappe.utils, { | |||||
seconds: round(seconds % 60) | seconds: round(seconds % 60) | ||||
}; | }; | ||||
if (duration_options.hide_days) { | |||||
if (duration_options && duration_options.hide_days) { | |||||
total_duration.hours = round(seconds / 3600); | total_duration.hours = round(seconds / 3600); | ||||
total_duration.days = 0; | total_duration.days = 0; | ||||
} | } | ||||
@@ -1462,5 +1462,23 @@ Object.assign(frappe.utils, { | |||||
console.log(error); // eslint-disable-line | console.log(error); // eslint-disable-line | ||||
return Promise.resolve(name); | return Promise.resolve(name); | ||||
} | } | ||||
}, | |||||
only_allow_num_decimal(input) { | |||||
input.on('input', (e) => { | |||||
let self = $(e.target); | |||||
self.val(self.val().replace(/[^0-9.]/g, '')); | |||||
if ((e.which != 46 || self.val().indexOf('.') != -1) && (e.which < 48 || e.which > 57)) { | |||||
e.preventDefault(); | |||||
} | |||||
}); | |||||
}, | |||||
string_to_boolean(string) { | |||||
switch (string.toLowerCase().trim()) { | |||||
case "t": case "true": case "y": case "yes": case "1": return true; | |||||
case "f": case "false": case "n": case "no": case "0": case null: return false; | |||||
default: return string; | |||||
} | |||||
} | } | ||||
}); | }); |
@@ -29,7 +29,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||||
.then(() => { | .then(() => { | ||||
this.page_title = __('{0} Calendar', [this.page_title]); | this.page_title = __('{0} Calendar', [this.page_title]); | ||||
this.calendar_settings = frappe.views.calendar[this.doctype] || {}; | this.calendar_settings = frappe.views.calendar[this.doctype] || {}; | ||||
this.calendar_name = frappe.utils.to_title_case(frappe.get_route()[3] || ''); | |||||
this.calendar_name = frappe.get_route()[3]; | |||||
}); | }); | ||||
} | } | ||||
@@ -72,12 +72,17 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView { | |||||
const calendar_name = this.calendar_name; | const calendar_name = this.calendar_name; | ||||
return new Promise(resolve => { | return new Promise(resolve => { | ||||
if (calendar_name === 'Default') { | |||||
if (calendar_name === 'default') { | |||||
Object.assign(options, frappe.views.calendar[this.doctype]); | Object.assign(options, frappe.views.calendar[this.doctype]); | ||||
resolve(options); | resolve(options); | ||||
} else { | } else { | ||||
frappe.model.with_doc('Calendar View', calendar_name, () => { | frappe.model.with_doc('Calendar View', calendar_name, () => { | ||||
const doc = frappe.get_doc('Calendar View', calendar_name); | const doc = frappe.get_doc('Calendar View', calendar_name); | ||||
if (!doc) { | |||||
frappe.show_alert(__("{0} is not a valid Calendar. Redirecting to default Calendar.", [calendar_name.bold()])); | |||||
frappe.set_route("List", this.doctype, "Calendar", "default"); | |||||
return; | |||||
} | |||||
Object.assign(options, { | Object.assign(options, { | ||||
field_map: { | field_map: { | ||||
id: "name", | id: "name", | ||||
@@ -1026,7 +1026,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||||
} | } | ||||
if (!docfield || docfield.report_hide) return; | if (!docfield || docfield.report_hide) return; | ||||
let title = __(docfield ? docfield.label : toTitle(fieldname)); | |||||
let title = __(docfield.label); | |||||
if (doctype !== this.doctype) { | if (doctype !== this.doctype) { | ||||
title += ` (${__(doctype)})`; | title += ` (${__(doctype)})`; | ||||
} | } | ||||
@@ -16,7 +16,8 @@ export default class WebFormList { | |||||
if (this.table) { | if (this.table) { | ||||
Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); | Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); | ||||
let check = document.getElementById('select-all'); | let check = document.getElementById('select-all'); | ||||
check.checked = false; | |||||
if (check) | |||||
check.checked = false; | |||||
} | } | ||||
this.rows = []; | this.rows = []; | ||||
this.page_length = 20; | this.page_length = 20; | ||||
@@ -131,9 +132,39 @@ export default class WebFormList { | |||||
this.make_table_head(); | this.make_table_head(); | ||||
} | } | ||||
this.append_rows(this.data); | |||||
this.wrapper.appendChild(this.table); | |||||
if (this.data.length) { | |||||
this.append_rows(this.data); | |||||
this.wrapper.appendChild(this.table); | |||||
} else { | |||||
let new_button = ""; | |||||
let empty_state = document.createElement("div"); | |||||
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center"); | |||||
frappe.has_permission(this.doctype, "", "create", () => { | |||||
new_button = ` | |||||
<a | |||||
class="btn btn-primary btn-sm btn-new-doc hidden-xs" | |||||
href="${window.location.pathname}?new=1"> | |||||
${__("Create a new {0}", [__(this.doctype)])} | |||||
</a> | |||||
`; | |||||
empty_state.innerHTML = ` | |||||
<div class="text-center"> | |||||
<div> | |||||
<img | |||||
src="/assets/frappe/images/ui-states/list-empty-state.svg" | |||||
alt="Generic Empty State" | |||||
class="null-state"> | |||||
</div> | |||||
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p> | |||||
${new_button} | |||||
</div> | |||||
`; | |||||
this.wrapper.appendChild(empty_state); | |||||
}); | |||||
} | |||||
} | } | ||||
make_table_head() { | make_table_head() { | ||||
@@ -212,8 +243,7 @@ export default class WebFormList { | |||||
"btn", | "btn", | ||||
"btn-secondary", | "btn-secondary", | ||||
"btn-sm", | "btn-sm", | ||||
"ml-2", | |||||
"text-white" | |||||
"ml-2" | |||||
); | ); | ||||
} | } | ||||
else if (type == "danger") { | else if (type == "danger") { | ||||
@@ -89,6 +89,29 @@ | |||||
height: 34px; | height: 34px; | ||||
padding: 8px; | padding: 8px; | ||||
max-height: 200px; | max-height: 200px; | ||||
&.search { | |||||
padding: 7px !important; | |||||
input { | |||||
height: -webkit-fill-available; | |||||
padding: 3px 7px; | |||||
} | |||||
} | |||||
} | |||||
.row-check { | |||||
height: 34px; | |||||
padding: 8px 3px !important; | |||||
text-align: center; | |||||
input { | |||||
margin-right: 0 !important; | |||||
} | |||||
&.search { | |||||
padding: 0 !important; | |||||
} | |||||
} | } | ||||
.grid-row-check { | .grid-row-check { | ||||
@@ -124,7 +147,6 @@ | |||||
.grid-row > .row { | .grid-row > .row { | ||||
.col:last-child { | .col:last-child { | ||||
margin-right: calc(-1 * var(--margin-sm)); | |||||
border-right: none; | border-right: none; | ||||
} | } | ||||
@@ -427,6 +449,7 @@ | |||||
} | } | ||||
.page-number { | .page-number { | ||||
background-color: var(--fg-color); | |||||
padding: 0 3px; | padding: 0 3px; | ||||
} | } | ||||
@@ -88,6 +88,7 @@ | |||||
top: var(--navbar-height); | top: var(--navbar-height); | ||||
background: var(--bg-color); | background: var(--bg-color); | ||||
margin-bottom: 5px; | margin-bottom: 5px; | ||||
transition: 0.5s top; | |||||
.page-head-content { | .page-head-content { | ||||
height: var(--page-head-height); | height: var(--page-head-height); | ||||
} | } | ||||
@@ -311,3 +311,16 @@ h5.modal-title { | |||||
.empty-list-icon { | .empty-list-icon { | ||||
height: 70px; | height: 70px; | ||||
} | } | ||||
.null-state { | |||||
height: 60px; | |||||
width: auto; | |||||
margin-bottom: var(--margin-md); | |||||
img { | |||||
fill: var(--fg-color); | |||||
} | |||||
} | |||||
.no-result { | |||||
min-height: #{"calc(100vh - 284px)"}; | |||||
} |
@@ -7,12 +7,12 @@ class TestBaseDocument(unittest.TestCase): | |||||
def test_docstatus(self): | def test_docstatus(self): | ||||
doc = BaseDocument({"docstatus": 0}) | doc = BaseDocument({"docstatus": 0}) | ||||
self.assertTrue(doc.docstatus.is_draft()) | self.assertTrue(doc.docstatus.is_draft()) | ||||
self.assertEquals(doc.docstatus, 0) | |||||
self.assertEqual(doc.docstatus, 0) | |||||
doc.docstatus = 1 | doc.docstatus = 1 | ||||
self.assertTrue(doc.docstatus.is_submitted()) | self.assertTrue(doc.docstatus.is_submitted()) | ||||
self.assertEquals(doc.docstatus, 1) | |||||
self.assertEqual(doc.docstatus, 1) | |||||
doc.docstatus = 2 | doc.docstatus = 2 | ||||
self.assertTrue(doc.docstatus.is_cancelled()) | self.assertTrue(doc.docstatus.is_cancelled()) | ||||
self.assertEquals(doc.docstatus, 2) | |||||
self.assertEqual(doc.docstatus, 2) |
@@ -14,7 +14,7 @@ from frappe.database.database import Database | |||||
from frappe.query_builder import Field | from frappe.query_builder import Field | ||||
from frappe.query_builder.functions import Concat_ws | from frappe.query_builder.functions import Concat_ws | ||||
from frappe.tests.test_query_builder import db_type_is, run_only_if | from frappe.tests.test_query_builder import db_type_is, run_only_if | ||||
from frappe.utils import add_days, now, random_string | |||||
from frappe.utils import add_days, now, random_string, cint | |||||
from frappe.utils.testutils import clear_custom_fields | from frappe.utils.testutils import clear_custom_fields | ||||
@@ -84,6 +84,27 @@ class TestDB(unittest.TestCase): | |||||
), | ), | ||||
) | ) | ||||
def test_get_value_limits(self): | |||||
# check both dict and list style filters | |||||
filters = [{"enabled": 1}, [["enabled", "=", 1]]] | |||||
for filter in filters: | |||||
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1))) | |||||
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2))) | |||||
self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount)) | |||||
# without limits length == count | |||||
self.assertEqual(len(frappe.db.get_values("User", filters=filter)), | |||||
frappe.db.count("User", filter)) | |||||
frappe.db.get_value("User", filters=filter) | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
frappe.db.exists("User", filter) | |||||
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) | |||||
def test_escape(self): | def test_escape(self): | ||||
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) | frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) | ||||
@@ -301,6 +322,20 @@ class TestDB(unittest.TestCase): | |||||
# recover transaction to continue other tests | # recover transaction to continue other tests | ||||
raise Exception | raise Exception | ||||
def test_exists(self): | |||||
dt, dn = "User", "Administrator" | |||||
self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn) | |||||
self.assertEqual(frappe.db.exists(dt, dn), dn) | |||||
self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn) | |||||
filters = {"doctype": dt, "name": ("like", "Admin%")} | |||||
self.assertEqual(frappe.db.exists(filters), dn) | |||||
self.assertEqual( | |||||
filters["doctype"], dt | |||||
) # make sure that doctype was not removed from filters | |||||
self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn) | |||||
@run_only_if(db_type_is.MARIADB) | @run_only_if(db_type_is.MARIADB) | ||||
class TestDDLCommandsMaria(unittest.TestCase): | class TestDDLCommandsMaria(unittest.TestCase): | ||||
@@ -357,7 +392,7 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||||
WHERE Key_name = '{index_name}'; | WHERE Key_name = '{index_name}'; | ||||
""" | """ | ||||
) | ) | ||||
self.assertEquals(len(indexs_in_table), 2) | |||||
self.assertEqual(len(indexs_in_table), 2) | |||||
class TestDBSetValue(unittest.TestCase): | class TestDBSetValue(unittest.TestCase): | ||||
@@ -561,7 +596,7 @@ class TestDDLCommandsPost(unittest.TestCase): | |||||
AND indexname = '{index_name}' ; | AND indexname = '{index_name}' ; | ||||
""", | """, | ||||
) | ) | ||||
self.assertEquals(len(indexs_in_table), 1) | |||||
self.assertEqual(len(indexs_in_table), 1) | |||||
@run_only_if(db_type_is.POSTGRES) | @run_only_if(db_type_is.POSTGRES) | ||||
def test_modify_query(self): | def test_modify_query(self): | ||||
@@ -260,15 +260,15 @@ class TestDocument(unittest.TestCase): | |||||
'doctype': 'Test Formatted', | 'doctype': 'Test Formatted', | ||||
'currency': 100000 | 'currency': 100000 | ||||
}) | }) | ||||
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||||
self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||||
def test_limit_for_get(self): | def test_limit_for_get(self): | ||||
doc = frappe.get_doc("DocType", "DocType") | doc = frappe.get_doc("DocType", "DocType") | ||||
# assuming DocType has more than 3 Data fields | # assuming DocType has more than 3 Data fields | ||||
self.assertEquals(len(doc.get("fields", limit=3)), 3) | |||||
self.assertEqual(len(doc.get("fields", limit=3)), 3) | |||||
# limit with filters | # limit with filters | ||||
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) | |||||
self.assertEqual(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) | |||||
def test_virtual_fields(self): | def test_virtual_fields(self): | ||||
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked | """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked | ||||
@@ -70,10 +70,10 @@ class TestSearch(unittest.TestCase): | |||||
result = frappe.response['results'] | result = frappe.response['results'] | ||||
# Check whether the result is sorted or not | # Check whether the result is sorted or not | ||||
self.assertEquals(self.parent_doctype_name, result[0]['value']) | |||||
self.assertEqual(self.parent_doctype_name, result[0]['value']) | |||||
# Check whether searching for parent also list out children | # Check whether searching for parent also list out children | ||||
self.assertEquals(len(result), len(self.child_doctypes_names) + 1) | |||||
self.assertEqual(len(result), len(self.child_doctypes_names) + 1) | |||||
#Search for the word "pay", part of the word "pays" (country) in french. | #Search for the word "pay", part of the word "pays" (country) in french. | ||||
def test_link_search_in_foreign_language(self): | def test_link_search_in_foreign_language(self): | ||||
@@ -136,13 +136,14 @@ def create_contact_records(): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def create_multiple_todo_records(): | def create_multiple_todo_records(): | ||||
values = [] | |||||
if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}): | if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}): | ||||
return | return | ||||
for index in range(501): | |||||
frappe.get_doc({ | |||||
'doctype': 'ToDo', | |||||
'description': 'Multiple ToDo {}'.format(index+1) | |||||
}).insert() | |||||
for index in range(1, 1002): | |||||
values.append(('100{}'.format(index), 'Multiple ToDo {}'.format(index))) | |||||
frappe.db.bulk_insert('ToDo', fields=['name', 'description'], values=set(values)) | |||||
def insert_contact(first_name, phone_number): | def insert_contact(first_name, phone_number): | ||||
doc = frappe.get_doc({ | doc = frappe.get_doc({ | ||||
@@ -271,4 +272,47 @@ def update_child_table(name): | |||||
'options': 'Doctype to Link' | 'options': 'Doctype to Link' | ||||
}) | }) | ||||
doc.save() | |||||
doc.save() | |||||
@frappe.whitelist() | |||||
def insert_doctype_with_child_table_record(name): | |||||
if frappe.db.get_all(name, {'title': 'Test Grid Search'}): | |||||
return | |||||
def insert_child(doc, data, barcode, check, rating, duration, date): | |||||
doc.append('child_table_1', { | |||||
'data': data, | |||||
'barcode': barcode, | |||||
'check': check, | |||||
'rating': rating, | |||||
'duration': duration, | |||||
'date': date, | |||||
}) | |||||
doc = frappe.new_doc(name) | |||||
doc.title = 'Test Grid Search' | |||||
doc.append('child_table', {'title': 'Test Grid Search'}) | |||||
insert_child(doc, 'Data', '09709KJKKH2432', 1, 0.5, 266851, "2022-02-21") | |||||
insert_child(doc, 'Test', '09209KJHKH2432', 1, 0.8, 547877, "2021-05-27") | |||||
insert_child(doc, 'New', '09709KJHYH1132', 0, 0.1, 3, "2019-03-02") | |||||
insert_child(doc, 'Old', '09701KJHKH8750', 0, 0, 127455, "2022-01-11") | |||||
insert_child(doc, 'Alpha', '09204KJHKH2432', 0, 0.6, 364, "2019-12-31") | |||||
insert_child(doc, 'Delta', '09709KSPIO2432', 1, 0.9, 1242000, "2020-04-21") | |||||
insert_child(doc, 'Update', '76989KJLVA2432', 0, 1, 183845, "2022-02-10") | |||||
insert_child(doc, 'Delete', '29189KLHVA1432', 0, 0, 365647, "2021-05-07") | |||||
insert_child(doc, 'Make', '09689KJHAA2431', 0, 0.3, 24, "2020-11-11") | |||||
insert_child(doc, 'Create', '09709KLKKH2432', 1, 0.3, 264851, "2021-02-21") | |||||
insert_child(doc, 'Group', '09209KJLKH2432', 1, 0.8, 537877, "2020-03-15") | |||||
insert_child(doc, 'Slide', '01909KJHYH1132', 0, 0.5, 9, "2018-03-02") | |||||
insert_child(doc, 'Drop', '09701KJHKH8750', 1, 0, 127255, "2018-01-01") | |||||
insert_child(doc, 'Beta', '09204QJHKN2432', 0, 0.6, 354, "2017-12-30") | |||||
insert_child(doc, 'Flag', '09709KXPIP2432', 1, 0, 1241000, "2021-04-21") | |||||
insert_child(doc, 'Upgrade', '75989ZJLVA2432', 0.8, 1, 183645, "2020-08-13") | |||||
insert_child(doc, 'Down', '28189KLHRA1432', 1, 0, 362647, "2020-06-17") | |||||
insert_child(doc, 'Note', '09689DJHAA2431', 0, 0.1, 29, "2021-09-11") | |||||
insert_child(doc, 'Click', '08189DJHAA2431', 1, 0.3, 209, "2020-07-04") | |||||
insert_child(doc, 'Drag', '08189DIHAA2981', 0, 0.7, 342628, "2022-05-04") | |||||
doc.insert() |
@@ -650,8 +650,6 @@ def extract_messages_from_code(code): | |||||
if isinstance(e, InvalidIncludePath): | if isinstance(e, InvalidIncludePath): | ||||
frappe.clear_last_message() | frappe.clear_last_message() | ||||
pass | |||||
messages = [] | messages = [] | ||||
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | ||||
@@ -796,22 +796,33 @@ def get_assets_json(): | |||||
# using .get instead of .get_value to avoid pickle.loads | # using .get instead of .get_value to avoid pickle.loads | ||||
try: | try: | ||||
assets_json = cache.get("assets_json") | |||||
except ConnectionError: | |||||
assets_json = None | |||||
# if value found, decode it | |||||
if assets_json is not None: | |||||
try: | |||||
assets_json = assets_json.decode('utf-8') | |||||
except (UnicodeDecodeError, AttributeError): | |||||
if not frappe.conf.developer_mode: | |||||
assets_json = cache.get("assets_json").decode('utf-8') | |||||
else: | |||||
assets_json = None | assets_json = None | ||||
except (UnicodeDecodeError, AttributeError, ConnectionError): | |||||
assets_json = None | |||||
if not assets_json: | if not assets_json: | ||||
assets_json = frappe.read_file("assets/assets.json") | |||||
cache.set_value("assets_json", assets_json, shared=True) | |||||
frappe.local.assets_json = frappe.safe_decode(assets_json) | |||||
# get merged assets.json and assets-rtl.json | |||||
assets_dict = frappe.parse_json( | |||||
frappe.read_file("assets/assets.json") | |||||
) | |||||
assets_rtl = frappe.read_file("assets/assets-rtl.json") | |||||
if assets_rtl: | |||||
assets_dict.update( | |||||
frappe.parse_json(assets_rtl) | |||||
) | |||||
frappe.local.assets_json = frappe.as_json(assets_dict) | |||||
# save in cache | |||||
cache.set_value("assets_json", frappe.local.assets_json, | |||||
shared=True) | |||||
return assets_dict | |||||
else: | |||||
# from cache, decode and send | |||||
frappe.local.assets_json = frappe.safe_decode(assets_json) | |||||
return frappe.parse_json(frappe.local.assets_json) | return frappe.parse_json(frappe.local.assets_json) | ||||
@@ -15,7 +15,7 @@ import click | |||||
# imports - module imports | # imports - module imports | ||||
import frappe | import frappe | ||||
from frappe import _, conf | |||||
from frappe import conf | |||||
from frappe.utils import get_file_size, get_url, now, now_datetime, cint | from frappe.utils import get_file_size, get_url, now, now_datetime, cint | ||||
from frappe.utils.password import get_encryption_key | from frappe.utils.password import get_encryption_key | ||||
@@ -505,7 +505,7 @@ download only after 24 hours.""" % { | |||||
datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" | datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" | ||||
) | ) | ||||
frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject) | |||||
frappe.sendmail(recipients=recipient_list, message=msg, subject=subject) | |||||
return recipient_list | return recipient_list | ||||
@@ -779,7 +779,7 @@ if __name__ == "__main__": | |||||
db_type=db_type, | db_type=db_type, | ||||
db_port=db_port, | db_port=db_port, | ||||
) | ) | ||||
odb.send_email("abc.sql.gz") | |||||
odb.send_email() | |||||
if cmd == "delete_temp_backups": | if cmd == "delete_temp_backups": | ||||
delete_temp_backups() | delete_temp_backups() |
@@ -255,6 +255,12 @@ def add_standard_navbar_items(): | |||||
'item_type': 'Action', | 'item_type': 'Action', | ||||
'action': 'frappe.ui.toolbar.show_shortcuts(event)', | 'action': 'frappe.ui.toolbar.show_shortcuts(event)', | ||||
'is_standard': 1 | 'is_standard': 1 | ||||
}, | |||||
{ | |||||
'item_label': 'Frappe Support', | |||||
'item_type': 'Route', | |||||
'route': 'https://frappe.io/support', | |||||
'is_standard': 1 | |||||
} | } | ||||
] | ] | ||||
@@ -227,7 +227,6 @@ class NestedSet(Document): | |||||
update_nsm(self) | update_nsm(self) | ||||
except frappe.DoesNotExistError: | except frappe.DoesNotExistError: | ||||
if self.flags.on_rollback: | if self.flags.on_rollback: | ||||
pass | |||||
frappe.message_log.pop() | frappe.message_log.pop() | ||||
else: | else: | ||||
raise | raise | ||||