diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 5d33355a1b..340f4f8772 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -3,7 +3,10 @@ import sys errors_encounter = 0 pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") -start_pattern = re.compile(r"_{1,2}\([\"']{1,3}") +words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") +start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") +f_string_pattern = re.compile(r"_\(f[\"']") +starts_with_f_pattern = re.compile(r"_\(f") # skip first argument files = sys.argv[1:] @@ -14,9 +17,25 @@ for _file in files_to_scan: print(f'Checking: {_file}') file_lines = f.readlines() for line_number, line in enumerate(file_lines, 1): + if 'frappe-lint: disable-translate' in line: + continue + start_matches = start_pattern.search(line) if start_matches: + starts_with_f = starts_with_f_pattern.search(line) + + if starts_with_f: + has_f_string = f_string_pattern.search(line) + if has_f_string: + errors_encounter += 1 + print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}') + continue + else: + continue + match = pattern.search(line) + error_found = False + if not match and line.endswith(',\n'): # concat remaining text to validate multiline pattern line = "".join(file_lines[line_number - 1:]) @@ -24,11 +43,18 @@ for _file in files_to_scan: match = pattern.match(line) if not match: + error_found = True + print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}') + + if not error_found and not words_pattern.search(line): + error_found = True + print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}') + + if error_found: errors_encounter += 1 - print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}') if errors_encounter > 0: - print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.') + print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.') sys.exit(1) else: print('\nGood To Go!') diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 4b1147e79f..510e7c7678 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,9 +1,10 @@ -name: Trigger Docker build on release +name: 'Trigger Docker build on release' on: release: types: [released] jobs: curl: + name: 'Trigger Docker build on release' runs-on: ubuntu-latest container: image: alpine:latest diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index cdf676dd67..90453cd1b4 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -1,10 +1,11 @@ -name: 'Documentation Required' +name: 'Documentation Check' on: pull_request: types: [ opened, synchronize, reopened, edited ] jobs: - build: + docs-required: + name: 'Documentation Required' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml index ee633ef039..2a934a6795 100644 --- a/.github/workflows/publish-assets-develop.yml +++ b/.github/workflows/publish-assets-develop.yml @@ -1,11 +1,12 @@ -name: Build and Publish Assets for Development +name: 'Frappe Assets' on: push: branches: [ develop ] jobs: - build: + build-dev-and-publish: + name: 'Build and Publish Assets for Development' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml index 5c412ea1b0..e86f884f35 100644 --- a/.github/workflows/publish-assets-releases.yml +++ b/.github/workflows/publish-assets-releases.yml @@ -1,4 +1,4 @@ -name: Build and Publish Assets built for Releases +name: 'Frappe Assets' on: release: @@ -8,7 +8,8 @@ env: GITHUB_TOKEN: ${{ github.token }} jobs: - build: + build-release-and-publish: + name: 'Build and Publish Assets built for Releases' runs-on: ubuntu-latest steps: @@ -44,4 +45,3 @@ jobs: asset_path: build/assets.tar.gz asset_name: assets.tar.gz asset_content_type: application/octet-stream - diff --git a/.mergify.yml b/.mergify.yml index 582bbc2ee5..5b0ec71b1c 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -5,7 +5,7 @@ pull_request_rules: - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk (frappe) - - label!=don't-merge + - label!=dont-merge - label!=squash - "#approved-reviews-by>=1" actions: @@ -17,7 +17,7 @@ pull_request_rules: - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk (frappe) - - label!=don't-merge + - label!=dont-merge - label=squash - "#approved-reviews-by>=1" actions: diff --git a/.travis.yml b/.travis.yml index 2331217363..63895675ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,12 +31,12 @@ matrix: - name: "Python 3.7 MariaDB" python: 3.7 env: DB=mariadb TYPE=server - script: bench --verbose --site test_site run-tests --coverage + script: bench --site test_site run-tests --coverage - name: "Python 3.7 PostgreSQL" python: 3.7 env: DB=postgres TYPE=server - script: bench --verbose --site test_site run-tests --coverage + script: bench --site test_site run-tests --coverage - name: "Cypress" python: 3.7 diff --git a/CODEOWNERS b/CODEOWNERS index b23f98b034..5753d85cfa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,10 +8,10 @@ website/ @scmmishra web_form/ @scmmishra templates/ @scmmishra www/ @scmmishra -integrations/ @Mangesh-Khairnar +integrations/ @nextchamp-saqib patches/ @sahil28297 dashboard/ @prssanna -email/ @Thunderbottom +email/ @saurabh6790 event_streaming/ @ruchamahabal data_import* @netchampfaris core/ @surajshetty3416 diff --git a/README.md b/README.md index 57d4ca243d..f99988ae79 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Full-stack web application framework that uses Python and MariaDB on the server ## Contributing -1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Pull-Request-Guidelines) +1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) 1. [Translations](https://translate.erpnext.com) ### Website diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index 5e9a264189..f9f44675db 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -20,10 +20,14 @@ context('FileUploader', () => { open_upload_dialog(); cy.fixture('example.json').then(fileContent => { - cy.get_open_dialog().find('.file-upload-area').upload( - { fileContent, fileName: 'example.json', mimeType: 'application/json' }, - { subjectType: 'drag-n-drop' }, - ); + cy.get_open_dialog().find('.file-upload-area').upload({ + fileContent, + fileName: 'example.json', + mimeType: 'application/json' + }, { + subjectType: 'drag-n-drop', + force: true + }); cy.get_open_dialog().find('.file-info').should('contain', 'example.json'); cy.server(); cy.route('POST', '/api/method/upload_file').as('upload_file'); diff --git a/frappe/__init__.py b/frappe/__init__.py index 554f1f9747..fac0927428 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -312,7 +312,7 @@ def log(msg): debug_log.append(as_unicode(msg)) -def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None): +def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None): """Print a message to the user (via HTTP response). Messages are sent in the `__server_messages` property in the response JSON and shown in a pop-up / modal. @@ -321,6 +321,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, :param title: [optional] Message title. :param raise_exception: [optional] Raise given exception and show message. :param as_table: [optional] If `msg` is a list of lists, render as HTML table. + :param as_list: [optional] If `msg` is a list, render as un-ordered list. :param primary_action: [optional] Bind a primary server/client side action. :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal @@ -346,16 +347,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, return if as_table and type(msg) in (list, tuple): + out.as_table = 1 - table_rows = '' - for row in msg: - table_row_data = '' - for data in row: - table_row_data += '{}'.format(data) - table_rows += '{}'.format(table_row_data) - - out.message = '''{}
'''.format(table_rows) + if as_list and type(msg) in (list, tuple) and len(msg) > 1: + out.as_list = 1 if flags.print_messages and out.message: print(f"Message: {repr(out.message).encode('utf-8')}") @@ -405,12 +400,12 @@ def clear_last_message(): if len(local.message_log) > 0: local.message_log = local.message_log[:-1] -def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None): +def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, as_list=False): """Throw execption and show message (`msgprint`). :param msg: Message. :param exc: Exception class. Default `frappe.ValidationError`""" - msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide) + msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list) def emit_js(js, user=False, **kwargs): if user == False: @@ -801,11 +796,17 @@ def get_doc(*args, **kwargs): return doc -def get_last_doc(doctype): +def get_last_doc(doctype, filters=None, order_by="creation desc"): """Get last created document of this type.""" - d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1) + d = get_all( + doctype, + filters=filters, + limit_page_length=1, + order_by=order_by, + pluck="name" + ) if d: - return get_doc(doctype, d[0].name) + return get_doc(doctype, d[0]) else: raise DoesNotExistError @@ -1159,6 +1160,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp 'doctype_or_field': args.doctype_or_field, 'doc_type': doctype, 'field_name': args.fieldname, + 'row_name': args.row_name, 'property': args.property, 'value': args.value, 'property_type': args.property_type or "Data", diff --git a/frappe/app.py b/frappe/app.py index c4d6a0235a..82471c4e32 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -160,6 +160,10 @@ def handle_exception(e): http_status_code = getattr(e, "http_status_code", 500) return_as_message = False + if frappe.conf.get('developer_mode'): + # don't fail silently + print(frappe.get_traceback()) + if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')): # handle ajax responses first # if the request is ajax, send back the trace or error message diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js index 3e86f6cefa..774befc15e 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.js +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js @@ -3,14 +3,69 @@ frappe.ui.form.on('Assignment Rule', { refresh: function(frm) { + frm.trigger('setup_assignment_days_buttons'); + frm.trigger('set_options'); // refresh description frm.events.rule(frm); }, + + document_type: function(frm) { + frm.trigger('set_options'); + }, + + setup_assignment_days_buttons: function(frm) { + const labels = ['Weekends', 'Weekdays', 'All Days']; + let get_days = (label) => { + const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; + const weekends = ['Saturday', 'Sunday']; + return { + 'All Days': weekdays.concat(weekends), + 'Weekdays': weekdays, + 'Weekends': weekends, + }[label]; + }; + + let set_days = (e) => { + frm.clear_table('assignment_days'); + const label = $(e.currentTarget).text(); + get_days(label).forEach((day) => + frm.add_child('assignment_days', { day: day }) + ); + frm.refresh_field('assignment_days'); + }; + + labels.forEach(label => + frm.fields_dict['assignment_days'].grid.add_custom_button( + label, + set_days, + 'top' + ) + ); + }, + rule: function(frm) { - if (frm.doc.rule === 'Round Robin') { - frm.get_field('rule').set_description(__('Assign one by one, in sequence')); - } else { - frm.get_field('rule').set_description(__('Assign to the one who has the least assignments')); + const description_map = { + 'Round Robin': __('Assign one by one, in sequence'), + 'Load Balancing': __('Assign to the one who has the least assignments'), + 'Based on Field': __('Assign to the user set in this field'), + }; + frm.get_field('rule').set_description(description_map[frm.doc.rule]); + }, + + set_options(frm) { + const doctype = frm.doc.document_type; + frm.set_fields_as_options( + 'field', + doctype, + (df) => df.fieldtype == 'Link' && df.options == 'User', + [{ label: 'Owner', value: 'owner' }] + ); + if (doctype) { + frm.set_fields_as_options( + 'due_date_based_on', + doctype, + (df) => ['Date', 'Datetime'].includes(df.fieldtype) + ).then(options => frm.set_df_property('due_date_based_on', 'hidden', !options.length)); } - } + }, }); diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.json b/frappe/automation/doctype/assignment_rule/assignment_rule.json index eb79b9e3a8..0a57e06da6 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.json +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "Prompt", "creation": "2019-02-28 17:12:18.815830", @@ -8,6 +9,7 @@ "engine": "InnoDB", "field_order": [ "document_type", + "due_date_based_on", "priority", "disabled", "column_break_4", @@ -22,6 +24,7 @@ "assignment_days", "assign_to_users_section", "rule", + "field", "users", "last_user" ], @@ -91,15 +94,16 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Rule", - "options": "Round Robin\nLoad Balancing", + "options": "Round Robin\nLoad Balancing\nBased on Field", "reqd": 1 }, { + "depends_on": "eval: doc.rule !== 'Based on Field'", "fieldname": "users", "fieldtype": "Table MultiSelect", "label": "Users", - "options": "Assignment Rule User", - "reqd": 1 + "mandatory_depends_on": "eval: doc.rule !== 'Based on Field'", + "options": "Assignment Rule User" }, { "fieldname": "last_user", @@ -129,9 +133,25 @@ "label": "Assignment Days", "options": "Assignment Rule Day", "reqd": 1 + }, + { + "depends_on": "document_type", + "description": "Value from this field will be set as the due date in the ToDo", + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "label": "Due Date Based On" + }, + { + "depends_on": "eval: doc.rule == 'Based on Field'", + "fieldname": "field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval: doc.rule == 'Based on Field'" } ], - "modified": "2019-09-25 14:52:12.214514", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-20 14:47:20.662954", "modified_by": "Administrator", "module": "Automation", "name": "Assignment Rule", diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index c4bd49b870..c85cb149ea 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -19,14 +19,14 @@ class AssignmentRule(Document): repeated_days = get_repeated(assignment_days) frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days))) - def on_update(self): # pylint: disable=no-self-use - frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type) + def on_update(self): + clear_assignment_rule_cache(self) - def after_rename(self, old, new, merge): # pylint: disable=no-self-use - frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type) + def after_rename(self, old, new, merge): + clear_assignment_rule_cache(self) - def on_trash(self): # pylint: disable=no-self-use - frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type) + def on_trash(self): + clear_assignment_rule_cache(self) def apply_unassign(self, doc, assignments): if (self.unassign_condition and @@ -38,26 +38,30 @@ class AssignmentRule(Document): def apply_assign(self, doc): if self.safe_eval('assign_condition', doc): - self.do_assignment(doc) - return True + return self.do_assignment(doc) def do_assignment(self, doc): # clear existing assignment, to reassign assign_to.clear(doc.get('doctype'), doc.get('name')) - user = self.get_user() + user = self.get_user(doc) - assign_to.add(dict( - assign_to = [user], - doctype = doc.get('doctype'), - name = doc.get('name'), - description = frappe.render_template(self.description, doc), - assignment_rule = self.name, - notify = True - )) + if user: + assign_to.add(dict( + assign_to = [user], + doctype = doc.get('doctype'), + name = doc.get('name'), + description = frappe.render_template(self.description, doc), + assignment_rule = self.name, + notify = True, + date = doc.get(self.due_date_based_on) if self.due_date_based_on else None + )) - # set for reference in round robin - self.db_set('last_user', user) + # set for reference in round robin + self.db_set('last_user', user) + return True + + return False def clear_assignment(self, doc): '''Clear assignments''' @@ -69,7 +73,7 @@ class AssignmentRule(Document): if self.safe_eval('close_condition', doc): return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name')) - def get_user(self): + def get_user(self, doc): ''' Get the next user for assignment ''' @@ -77,6 +81,8 @@ class AssignmentRule(Document): return self.get_user_round_robin() elif self.rule == 'Load Balancing': return self.get_user_load_balancing() + elif self.rule == 'Based on Field': + return doc.get(self.field) def get_user_round_robin(self): ''' @@ -188,7 +194,7 @@ def apply(doc, method=None, doctype=None, name=None): # multiple auto assigns for d in assignment_rules: - assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.get('name'))) + assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name'))) if not assignment_rule_docs: return @@ -237,6 +243,40 @@ def apply(doc, method=None, doctype=None, name=None): break assignment_rule.close_assignments(doc) +def update_due_date(doc, state=None): + # called from hook + if (frappe.flags.in_patch + or frappe.flags.in_install + or frappe.flags.in_migrate + or frappe.flags.in_import + or frappe.flags.in_setup_wizard): + return + assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict( + document_type = doc.doctype, + disabled = 0, + due_date_based_on = ['is', 'set'] + )) + for rule in assignment_rules: + rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name')) + due_date_field = rule_doc.due_date_based_on + if doc.meta.has_field(due_date_field) and \ + doc.has_value_changed(due_date_field) and rule.get('name'): + assignment_todos = frappe.get_all('ToDo', { + 'assignment_rule': rule.get('name'), + 'status': 'Open', + 'reference_type': doc.doctype, + 'reference_name': doc.name + }) + for todo in assignment_todos: + todo_doc = frappe.get_doc('ToDo', todo.name) + todo_doc.date = doc.get(due_date_field) + todo_doc.flags.updater_reference = { + 'doctype': 'Assignment Rule', + 'docname': rule.get('name'), + 'label': _('via Assignment Rule') + } + todo_doc.save(ignore_permissions=True) + def get_assignment_rules(): return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))] @@ -250,3 +290,7 @@ def get_repeated(values): if value not in diff: diff.append(str(value)) return " ".join(diff) + +def clear_assignment_rule_cache(rule): + frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type) + frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type) \ No newline at end of file diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py index 7c68e63d95..cb1e0ff8f4 100644 --- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -20,6 +20,7 @@ class TestAutoAssign(unittest.TestCase): dict(day = 'Friday'), dict(day = 'Saturday'), ] + self.days = days self.assignment_rule = get_assignment_rule([days, days]) clear_assignments() @@ -87,6 +88,30 @@ class TestAutoAssign(unittest.TestCase): for user in ('test@example.com', 'test1@example.com', 'test2@example.com'): self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10) + def test_based_on_field(self): + self.assignment_rule.rule = 'Based on Field' + self.assignment_rule.field = 'owner' + self.assignment_rule.save() + + frappe.set_user('test1@example.com') + note = make_note(dict(public=1)) + # check if auto assigned to doc owner, test1@example.com + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test1@example.com') + + frappe.set_user('test2@example.com') + note = make_note(dict(public=1)) + # check if auto assigned to doc owner, test2@example.com + self.assertEqual(frappe.db.get_value('ToDo', dict( + reference_type = 'Note', + reference_name = note.name, + status = 'Open' + ), 'owner'), 'test2@example.com') + + frappe.set_user('Administrator') def test_assign_condition(self): # check condition @@ -180,6 +205,55 @@ class TestAutoAssign(unittest.TestCase): status = 'Open' ), 'owner'), ['test3@example.com']) + def test_assignment_rule_condition(self): + frappe.db.sql("DELETE FROM `tabAssignment Rule`") + + # Add expiry_date custom field + from frappe.custom.doctype.custom_field.custom_field import create_custom_field + df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date') + create_custom_field('Note', df) + + assignment_rule = frappe.get_doc(dict( + name = 'Assignment with Due Date', + doctype = 'Assignment Rule', + document_type = 'Note', + assign_condition = 'public == 0', + due_date_based_on = 'expiry_date', + assignment_days = self.days, + users = [ + dict(user = 'test@example.com'), + ] + )).insert() + + expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) + note1 = make_note({'expiry_date': expiry_date}) + note2 = make_note({'expiry_date': expiry_date}) + + note1_todo = frappe.get_all('ToDo', filters=dict( + reference_type = 'Note', + reference_name = note1.name, + status = 'Open' + ))[0] + + note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name) + self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date) + + # due date should be updated if the reference doc's date is updated. + note1.expiry_date = frappe.utils.add_days(expiry_date, 2) + note1.save() + note1_todo_doc.reload() + self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date) + + # saving one note's expiry should not update other note todo's due date + note2_todo = frappe.get_all('ToDo', filters=dict( + reference_type = 'Note', + reference_name = note2.name, + status = 'Open' + ), fields=['name', 'date'])[0] + self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date) + self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date) + assignment_rule.delete() + def clear_assignments(): frappe.db.sql("delete from tabToDo where reference_type = 'Note'") @@ -237,4 +311,4 @@ def make_note(values=None): note.insert() - return note \ No newline at end of file + return note diff --git a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json index f529772c8e..5a159c8267 100644 --- a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json +++ b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json @@ -1,76 +1,34 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], + "allow_read": 1, "creation": "2019-02-27 11:41:46.602400", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "user" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "user", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "User", - "length": 0, - "no_copy": 0, "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, + "index_web_pages_for_search": 1, "istable": 1, - "max_attachments": 0, - "modified": "2019-02-27 17:16:41.399261", + "links": [], + "modified": "2020-09-29 20:12:14.456785", "modified_by": "Administrator", "module": "Automation", "name": "Assignment Rule User", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/build.py b/frappe/build.py index 767217a9b9..f14b250a92 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -40,6 +40,7 @@ def build_missing_files(): # check which files dont exist yet from the build.json and tell build.js to build only those! missing_assets = [] current_asset_files = [] + frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json") for type in ["css", "js"]: current_asset_files.extend( @@ -49,7 +50,7 @@ def build_missing_files(): ] ) - with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f: + with open(frappe_build) as f: all_asset_files = json.load(f).keys() for asset in all_asset_files: @@ -111,13 +112,21 @@ def download_frappe_assets(verbose=True): if assets_archive: import tarfile + directories_created = set() click.secho("\nExtracting assets...\n", fg="yellow") with tarfile.open(assets_archive) as tar: for file in tar: if not file.isdir(): dest = "." + file.name.replace("./frappe-bench/sites", "") + asset_directory = os.path.dirname(dest) show = dest.replace("./assets/", "") + + if asset_directory not in directories_created: + if not os.path.exists(asset_directory): + os.makedirs(asset_directory, exist_ok=True) + directories_created.add(asset_directory) + tar.makefile(file, dest) print("{0} Restored {1}".format(green('✔'), show)) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 446166d44c..8af0b422ba 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -265,14 +265,12 @@ def disable_user(context, email): user.save(ignore_permissions=True) frappe.db.commit() - @click.command('migrate') @click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run") @click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents") @pass_context def migrate(context, skip_failing=False, skip_search_index=False): "Run patches, sync schema and rebuild files/translations" - import compileall import re from frappe.migrate import migrate @@ -291,9 +289,6 @@ def migrate(context, skip_failing=False, skip_search_index=False): if not context.sites: raise SiteNotSpecifiedError - print("Compiling Python files...") - compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*')) - @click.command('migrate-to') @click.argument('frappe_provider') @pass_context @@ -636,8 +631,10 @@ def browse(context, site): @click.command('start-recording') @pass_context def start_recording(context): + import frappe.recorder for site in context.sites: frappe.init(site=site) + frappe.set_user("Administrator") frappe.recorder.start() if not context.sites: raise SiteNotSpecifiedError @@ -646,8 +643,10 @@ def start_recording(context): @click.command('stop-recording') @pass_context def stop_recording(context): + import frappe.recorder for site in context.sites: frappe.init(site=site) + frappe.set_user("Administrator") frappe.recorder.stop() if not context.sites: raise SiteNotSpecifiedError diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 5a5986ff57..31b84ee98a 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -460,11 +460,21 @@ def console(context): frappe.init(site=site) frappe.connect() frappe.local.lang = frappe.db.get_default("lang") + import IPython all_apps = frappe.get_installed_apps() + failed_to_import = [] + for app in all_apps: - locals()[app] = __import__(app) + try: + locals()[app] = __import__(app) + except ModuleNotFoundError: + failed_to_import.append(app) + print("Apps in this namespace:\n{}".format(", ".join(all_apps))) + if failed_to_import: + print("\nFailed to import:\n{}".format(", ".join(failed_to_import))) + IPython.embed(display_banner="", header="", colors="neutral") @@ -554,10 +564,24 @@ def run_ui_tests(context, app, headless=False): site_env = 'CYPRESS_baseUrl={}'.format(site_url) password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else '' + os.chdir(app_base_path) + + node_bin = subprocess.getoutput("npm bin") + cypress_path = "{0}/cypress".format(node_bin) + plugin_path = "{0}/cypress-file-upload".format(node_bin) + + # check if cypress in path...if not, install it. + if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)): + # install cypress + click.secho("Installing Cypress...", fg="yellow") + frappe.commands.popen("yarn add cypress@3 cypress-file-upload@^3.1 --no-lockfile") + # run for headless mode run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' - command = '{site_env} {password_env} yarn run cypress {run_or_open}' - formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open) + command = '{site_env} {password_env} {cypress} {run_or_open}' + formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open) + + click.secho("Running Cypress...", fg="yellow") frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True) diff --git a/frappe/config/customization.py b/frappe/config/customization.py index 3d587e6839..95fa5d355c 100644 --- a/frappe/config/customization.py +++ b/frappe/config/customization.py @@ -54,12 +54,6 @@ def get_data(): "label": _("Custom Translations"), "name": "Translation", "description": _("Add your own translations") - }, - { - "type": "doctype", - "label": _("Package"), - "name": "Package", - "description": _("Import and Export Packages.") } ] } diff --git a/frappe/config/settings.py b/frappe/config/settings.py index e43abd9fcb..0112c7ccff 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -23,6 +23,11 @@ def get_data(): "description": _("Company, Fiscal Year and Currency defaults"), "hide_count": True }, + { + "type": "doctype", + "name": "Log Settings", + "description": _("Log cleanup and notification configuration") + }, { "type": "doctype", "name": "Error Log", diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index 2f634074d2..e85a89ff1a 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:32", @@ -24,7 +25,6 @@ "is_shipping_address", "disabled", "linked_with", - "is_your_company_address", "links" ], "fields": [ @@ -75,7 +75,7 @@ { "fieldname": "state", "fieldtype": "Data", - "label": "State" + "label": "State/Province" }, { "fieldname": "country", @@ -138,12 +138,6 @@ "label": "Reference", "options": "fa fa-pushpin" }, - { - "default": "0", - "fieldname": "is_your_company_address", - "fieldtype": "Check", - "label": "Is Your Company Address" - }, { "fieldname": "links", "fieldtype": "Table", @@ -153,7 +147,8 @@ ], "icon": "fa fa-map-marker", "idx": 5, - "modified": "2019-09-08 11:41:04.145589", + "links": [], + "modified": "2020-10-21 16:14:37.284830", "modified_by": "Administrator", "module": "Contacts", "name": "Address", diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index e82ab9b26e..84b925d50e 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -39,14 +39,13 @@ class Address(Document): def validate(self): self.link_address() - self.validate_reference() self.validate_preferred_address() set_link_title(self) deduplicate_dynamic_links(self) def link_address(self): """Link address based on owner""" - if not self.links and not self.is_your_company_address: + if not self.links: contact_name = frappe.db.get_value("Contact", {"email_id": self.owner}) if contact_name: contact = frappe.get_cached_doc('Contact', contact_name) @@ -56,12 +55,6 @@ class Address(Document): return False - def validate_reference(self): - if self.is_your_company_address: - if not [row for row in self.links if row.link_doctype == "Company"]: - frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table below."), - title =_("Company not Linked")) - def validate_preferred_address(self): preferred_fields = ['is_primary_address', 'is_shipping_address'] @@ -204,25 +197,6 @@ def get_address_templates(address): else: return result -@frappe.whitelist() -def get_shipping_address(company, address = None): - filters = [ - ["Dynamic Link", "link_doctype", "=", "Company"], - ["Dynamic Link", "link_name", "=", company], - ["Address", "is_your_company_address", "=", 1] - ] - fields = ["*"] - if address and frappe.db.get_value('Dynamic Link', - {'parent': address, 'link_name': company}): - filters.append(["Address", "name", "=", address]) - - address = frappe.get_all("Address", filters=filters, fields=fields) or {} - - if address: - address_as_dict = address[0] - name, address_template = get_address_templates(address_as_dict) - return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) - def get_company_address(company): ret = frappe._dict() ret.company_address = get_default_address('Company', company) diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py index 27a2892ca8..98dc91806d 100644 --- a/frappe/core/doctype/activity_log/activity_log.py +++ b/frappe/core/doctype/activity_log/activity_log.py @@ -40,7 +40,11 @@ def add_authentication_log(subject, user, operation="Login", status="Success"): "operation": operation, }).insert(ignore_permissions=True, ignore_links=True) -def clear_authentication_logs(): - """clear 100 day old authentication logs""" +def clear_activity_logs(days=None): + """clear 90 day old authentication logs or configured in log settings""" + + if not days: + days = 90 + frappe.db.sql("""delete from `tabActivity Log` where \ - creation< (NOW() - INTERVAL '100' DAY)""") \ No newline at end of file + creation< (NOW() - INTERVAL '{0}' DAY)""".format(days)) \ No newline at end of file diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index fc929351d4..d893e80617 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -260,10 +260,8 @@ class Communication(Document): # Timeline Links def set_timeline_links(self): contacts = [] - if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \ - frappe.flags.in_test: - - contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc]) + create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact") + contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled) for contact_name in contacts: self.add_link('Contact', contact_name) @@ -342,7 +340,7 @@ def get_permission_query_conditions_for_communication(user): return """`tabCommunication`.email_account in ({email_accounts})"""\ .format(email_accounts=','.join(email_accounts)) -def get_contacts(email_strings): +def get_contacts(email_strings, auto_create_contact=False): email_addrs = [] for email_string in email_strings: @@ -357,7 +355,7 @@ def get_contacts(email_strings): email = get_email_without_link(email) contact_name = get_contact_name(email) - if not contact_name and email: + if not contact_name and email and auto_create_contact: email_parts = email.split("@") first_name = frappe.unscrub(email_parts[0]) diff --git a/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv b/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv index 656985b519..e48208ea72 100644 --- a/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv +++ b/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv @@ -1,2 +1,2 @@ -Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number -Test 26 ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7 +Title,Description,Number,another_number,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number +Test 26,test description,1,2,"",child title,child description,child title,14-08-2019,4,child title again,22-09-2020,5,7 diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 5271690527..7880648b6f 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -616,7 +616,9 @@ class Row: id_field = get_id_field(doctype) id_value = doc.get(id_field.fieldname) if id_value and frappe.db.exists(doctype, id_value): - doc = frappe.get_doc(doctype, id_value) + existing_doc = frappe.get_doc(doctype, id_value) + existing_doc.update(doc) + doc = existing_doc else: # for table rows being inserted in update # create a new doc with defaults set diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index 249451fd4d..b083b9eaaa 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import unittest import frappe +from frappe.core.doctype.data_import.importer import Importer from frappe.utils import getdate, format_duration doctype_name = 'DocType for Import' class TestImporter(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): create_doctype_if_not_exists(doctype_name) def test_data_import_from_file(self): @@ -71,19 +73,28 @@ class TestImporter(unittest.TestCase): self.assertEqual(warnings[2]['message'], "Title is a mandatory field") def test_data_import_update(self): - if not frappe.db.exists(doctype_name, 'Test 26'): - frappe.get_doc( - doctype=doctype_name, - title='Test 26' - ).insert() + existing_doc = frappe.get_doc( + doctype=doctype_name, + title=frappe.generate_hash(doctype_name, 8), + table_field_1=[{'child_title': 'child title to update'}] + ) + existing_doc.save() + frappe.db.commit() import_file = get_import_file('sample_import_file_for_update') data_import = self.get_importer(doctype_name, import_file, update=True) - data_import.start_import() + i = Importer(data_import.reference_doctype, data_import=data_import) - updated_doc = frappe.get_doc(doctype_name, 'Test 26') + # update child table id in template date + i.import_file.raw_data[1][4] = existing_doc.table_field_1[0].name + i.import_file.raw_data[1][0] = existing_doc.name + i.import_file.parse_data_from_template() + i.import_data() + + updated_doc = frappe.get_doc(doctype_name, existing_doc.name) self.assertEqual(updated_doc.description, 'test description') self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title') + self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name) self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description') self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again') diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index e420d3b775..ca134665b8 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -13,6 +13,7 @@ "fieldname", "precision", "length", + "non_negative", "hide_days", "hide_seconds", "reqd", @@ -473,13 +474,20 @@ "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" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-28 11:28:21.252853", + "modified": "2020-10-29 06:09:26.454990", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 9d37849746..8a9c130fbe 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import re, copy, os, shutil import json +from frappe.cache_manager import clear_user_cache # imports - third party imports import six @@ -103,6 +104,10 @@ class DocType(Document): self.owner = 'Administrator' self.modified_by = 'Administrator' + def after_insert(self): + # clear user cache so that on the next reload this doctype is included in boot + clear_user_cache(frappe.session.user) + def set_default_in_list_view(self): '''Set default in-list-view for first 4 mandatory fields''' if not [d.fieldname for d in self.fields if d.in_list_view]: @@ -747,8 +752,8 @@ def validate_fields(meta): def check_illegal_default(d): if d.fieldtype == "Check" and not d.default: d.default = '0' - if d.fieldtype == "Check" and d.default not in ('0', '1'): - frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'")) + if d.fieldtype == "Check" and cint(d.default) not in (0, 1): + frappe.throw(_("Default for 'Check' type of field {0} must be either '0' or '1'").format(frappe.bold(d.fieldname))) if d.fieldtype == "Select" and d.default: if not d.options: frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname))) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 00e80ce4e7..6f4a400577 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -12,41 +12,22 @@ from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMan class TestDocType(unittest.TestCase): - def new_doctype(self, name, unique=0, depends_on=''): - return frappe.get_doc({ - "doctype": "DocType", - "module": "Core", - "custom": 1, - "fields": [{ - "label": "Some Field", - "fieldname": "some_fieldname", - "fieldtype": "Data", - "unique": unique, - "depends_on": depends_on, - }], - "permissions": [{ - "role": "System Manager", - "read": 1, - }], - "name": name - }) - def test_validate_name(self): - self.assertRaises(frappe.NameError, self.new_doctype("_Some DocType").insert) - self.assertRaises(frappe.NameError, self.new_doctype("8Some DocType").insert) - self.assertRaises(frappe.NameError, self.new_doctype("Some (DocType)").insert) + self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) + self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) + self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) for name in ("Some DocType", "Some_DocType"): if frappe.db.exists("DocType", name): frappe.delete_doc("DocType", name) - doc = self.new_doctype(name).insert() + doc = new_doctype(name).insert() doc.delete() def test_doctype_unique_constraint_dropped(self): if frappe.db.exists("DocType", "With_Unique"): frappe.delete_doc("DocType", "With_Unique") - dt = self.new_doctype("With_Unique", unique=1) + dt = new_doctype("With_Unique", unique=1) dt.insert() doc1 = frappe.new_doc("With_Unique") @@ -67,7 +48,7 @@ class TestDocType(unittest.TestCase): doc2.delete() def test_validate_search_fields(self): - doc = self.new_doctype("Test Search Fields") + doc = new_doctype("Test Search Fields") doc.search_fields = "some_fieldname" doc.insert() self.assertEqual(doc.name, "Test Search Fields") @@ -85,7 +66,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(frappe.ValidationError, doc.save) def test_depends_on_fields(self): - doc = self.new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") + doc = new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") doc.insert() # check if the assignment operation is allowed in depends_on @@ -261,7 +242,7 @@ class TestDocType(unittest.TestCase): frappe.flags.allow_doctype_export = 0 def test_unique_field_name_for_two_fields(self): - doc = self.new_doctype('Test Unique Field') + doc = new_doctype('Test Unique Field') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Data' @@ -273,7 +254,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(UniqueFieldnameError, doc.insert) def test_fieldname_is_not_name(self): - doc = self.new_doctype('Test Name Field') + doc = new_doctype('Test Name Field') field_1 = doc.append('fields', {}) field_1.label = 'Name' field_1.fieldtype = 'Data' @@ -283,7 +264,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(InvalidFieldNameError, doc.save) def test_illegal_mandatory_validation(self): - doc = self.new_doctype('Test Illegal mandatory') + doc = new_doctype('Test Illegal mandatory') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Section Break' @@ -292,7 +273,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(IllegalMandatoryError, doc.insert) def test_link_with_wrong_and_no_options(self): - doc = self.new_doctype('Test link') + doc = new_doctype('Test link') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Link' @@ -304,7 +285,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) def test_hidden_and_mandatory_without_default(self): - doc = self.new_doctype('Test hidden and mandatory') + doc = new_doctype('Test hidden and mandatory') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Data' @@ -314,7 +295,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) def test_field_can_not_be_indexed_validation(self): - doc = self.new_doctype('Test index') + doc = new_doctype('Test index') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Long Text' @@ -327,14 +308,14 @@ class TestDocType(unittest.TestCase): from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs #create doctype - link_doc = self.new_doctype('Test Linked Doctype') + link_doc = new_doctype('Test Linked Doctype') link_doc.is_submittable = 1 for data in link_doc.get('permissions'): data.submit = 1 data.cancel = 1 link_doc.insert() - doc = self.new_doctype('Test Doctype') + doc = new_doctype('Test Doctype') doc.is_submittable = 1 field_2 = doc.append('fields', {}) field_2.label = 'Test Linked Doctype' @@ -377,12 +358,12 @@ class TestDocType(unittest.TestCase): doc.delete() frappe.db.commit() - def test_ignore_cancelation_of_linked_doctype_during_cancell(self): + def test_ignore_cancelation_of_linked_doctype_during_cancel(self): import json from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs #create linked doctype - link_doc = self.new_doctype('Test Linked Doctype 1') + link_doc = new_doctype('Test Linked Doctype 1') link_doc.is_submittable = 1 for data in link_doc.get('permissions'): data.submit = 1 @@ -390,7 +371,7 @@ class TestDocType(unittest.TestCase): link_doc.insert() #create first parent doctype - test_doc_1 = self.new_doctype('Test Doctype 1') + test_doc_1 = new_doctype('Test Doctype 1') test_doc_1.is_submittable = 1 field_2 = test_doc_1.append('fields', {}) @@ -405,7 +386,7 @@ class TestDocType(unittest.TestCase): test_doc_1.insert() #crete second parent doctype - doc = self.new_doctype('Test Doctype 2') + doc = new_doctype('Test Doctype 2') doc.is_submittable = 1 field_2 = doc.append('fields', {}) @@ -469,3 +450,28 @@ class TestDocType(unittest.TestCase): doc.delete() test_doc_1.delete() frappe.db.commit() + +def new_doctype(name, unique=0, depends_on='', fields=None): + doc = frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "fields": [{ + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data", + "unique": unique, + "depends_on": depends_on, + }], + "permissions": [{ + "role": "System Manager", + "read": 1, + }], + "name": name + }) + + if fields: + for f in fields: + doc.append('fields', f) + + return doc \ No newline at end of file diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json index 0f9da802eb..080755c479 100644 --- a/frappe/core/doctype/doctype_action/doctype_action.json +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -9,7 +9,8 @@ "action_type", "action", "group", - "hidden" + "hidden", + "custom" ], "fields": [ { @@ -48,12 +49,19 @@ "fieldname": "hidden", "fieldtype": "Check", "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-21 14:44:03.845315", + "modified": "2020-09-24 14:19:05.549835", "modified_by": "Administrator", "module": "Core", "name": "DocType Action", diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json index 752b4bb5da..0453894467 100644 --- a/frappe/core/doctype/doctype_link/doctype_link.json +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -7,7 +7,9 @@ "field_order": [ "link_doctype", "link_fieldname", - "group" + "group", + "hidden", + "custom" ], "fields": [ { @@ -30,10 +32,25 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Group" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-09-24 11:41:25.291377", + "links": [], + "modified": "2020-09-24 14:19:25.189511", "modified_by": "Administrator", "module": "Core", "name": "DocType Link", diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json index 79eebdbe64..4a88e3be6e 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json @@ -34,7 +34,8 @@ "fieldname": "prefix", "fieldtype": "Data", "label": "Prefix", - "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"" + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"", + "reqd": 1 }, { "fieldname": "counter", @@ -48,7 +49,8 @@ "fieldname": "prefix_digits", "fieldtype": "Int", "label": "Digits", - "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"" + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"", + "reqd": 1 }, { "fieldname": "naming_section", @@ -69,7 +71,7 @@ "options": "Document Naming Rule Condition" }, { - "description": "Rules with higher priority will be applied first.", + "description": "Rules with higher priority number will be applied first.", "fieldname": "priority", "fieldtype": "Int", "label": "Priority" @@ -77,7 +79,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-21 10:23:34.401539", + "modified": "2020-11-04 14:38:14.836056", "modified_by": "Administrator", "module": "Core", "name": "Document Naming Rule", diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 2de7552dc1..3ff47facc3 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -13,7 +13,7 @@ class DocumentNamingRule(Document): Apply naming rules for the given document. Will set `name` if the rule is matched. ''' if self.conditions: - if not evaluate_filters(doc, [(d.field, d.condition, d.value) for d in self.conditions]): + if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]): return counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py index 6cc8275404..ec02aaf446 100644 --- a/frappe/core/doctype/error_log/error_log.py +++ b/frappe/core/doctype/error_log/error_log.py @@ -17,9 +17,6 @@ def set_old_logs_as_seen(): frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1 WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""") - # clear old logs - frappe.db.sql("""DELETE FROM `tabError Log` WHERE `creation` < (NOW() - INTERVAL '30' DAY)""") - @frappe.whitelist() def clear_error_logs(): '''Flush all Error Logs''' diff --git a/frappe/custom/doctype/custom_link/__init__.py b/frappe/core/doctype/log_setting_user/__init__.py similarity index 100% rename from frappe/custom/doctype/custom_link/__init__.py rename to frappe/core/doctype/log_setting_user/__init__.py diff --git a/frappe/core/doctype/log_setting_user/log_setting_user.js b/frappe/core/doctype/log_setting_user/log_setting_user.js new file mode 100644 index 0000000000..a1eb824e22 --- /dev/null +++ b/frappe/core/doctype/log_setting_user/log_setting_user.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Log Setting User', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/log_setting_user/log_setting_user.json b/frappe/core/doctype/log_setting_user/log_setting_user.json new file mode 100644 index 0000000000..7f4b0ef874 --- /dev/null +++ b/frappe/core/doctype/log_setting_user/log_setting_user.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "autoname": "field:user", + "creation": "2020-10-08 13:09:36.034430", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1, + "unique": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-10-08 17:22:04.690348", + "modified_by": "Administrator", + "module": "Core", + "name": "Log Setting User", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/custom_link/custom_link.py b/frappe/core/doctype/log_setting_user/log_setting_user.py similarity index 88% rename from frappe/custom/doctype/custom_link/custom_link.py rename to frappe/core/doctype/log_setting_user/log_setting_user.py index 11316d5751..df6d55f0a9 100644 --- a/frappe/custom/doctype/custom_link/custom_link.py +++ b/frappe/core/doctype/log_setting_user/log_setting_user.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class CustomLink(Document): +class LogSettingUser(Document): pass diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/core/doctype/log_setting_user/test_log_setting_user.py similarity index 78% rename from frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py rename to frappe/core/doctype/log_setting_user/test_log_setting_user.py index 8332240543..507c02d87d 100644 --- a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py +++ b/frappe/core/doctype/log_setting_user/test_log_setting_user.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestPackagePublishTool(unittest.TestCase): +class TestLogSettingUser(unittest.TestCase): pass diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/core/doctype/log_settings/__init__.py similarity index 100% rename from frappe/custom/doctype/package_document_type/__init__.py rename to frappe/core/doctype/log_settings/__init__.py diff --git a/frappe/core/doctype/log_settings/log_settings.js b/frappe/core/doctype/log_settings/log_settings.js new file mode 100644 index 0000000000..09a2086a1d --- /dev/null +++ b/frappe/core/doctype/log_settings/log_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Log Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/log_settings/log_settings.json b/frappe/core/doctype/log_settings/log_settings.json new file mode 100644 index 0000000000..8a2596b35c --- /dev/null +++ b/frappe/core/doctype/log_settings/log_settings.json @@ -0,0 +1,83 @@ +{ + "actions": [], + "creation": "2020-10-08 12:12:21.694424", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "error_log_notification_section", + "users_to_notify", + "log_cleanup_section", + "clear_error_log_after", + "clear_activity_log_after", + "column_break_4", + "clear_email_queue_after" + ], + "fields": [ + { + "fieldname": "log_cleanup_section", + "fieldtype": "Section Break", + "label": "Log Cleanup" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "error_log_notification_section", + "fieldtype": "Section Break", + "label": "Error Log Notification" + }, + { + "fieldname": "users_to_notify", + "fieldtype": "Table MultiSelect", + "label": "Users To Notify", + "options": "Log Setting User" + }, + { + "default": "90", + "description": "In Days", + "fieldname": "clear_error_log_after", + "fieldtype": "Int", + "label": "Clear Error log After" + }, + { + "default": "90", + "description": "In Days", + "fieldname": "clear_activity_log_after", + "fieldtype": "Int", + "label": "Clear Activity Log After" + }, + { + "default": "90", + "description": "In Days", + "fieldname": "clear_email_queue_after", + "fieldtype": "Int", + "label": "Clear Email Queue After" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-10-13 12:18:48.649038", + "modified_by": "Administrator", + "module": "Core", + "name": "Log Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py new file mode 100644 index 0000000000..6d59cdeb29 --- /dev/null +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document + +class LogSettings(Document): + def clear_logs(self): + self.clear_error_logs() + self.clear_activity_logs() + self.clear_email_queue() + + def clear_error_logs(self): + frappe.db.sql(""" DELETE FROM `tabError Log` + WHERE `creation` < (NOW() - INTERVAL '{0}' DAY) + """.format(self.clear_error_log_after)) + + def clear_activity_logs(self): + from frappe.core.doctype.activity_log.activity_log import clear_activity_logs + clear_activity_logs(days=self.clear_activity_log_after) + + def clear_email_queue(self): + from frappe.email.queue import clear_outbox + clear_outbox(days=self.clear_email_queue_after) + +def run_log_clean_up(): + doc = frappe.get_doc("Log Settings") + doc.clear_logs() + +@frappe.whitelist() +def has_unseen_error_log(user): + + def _get_response(show_alert=True): + return { + 'show_alert': True, + 'message': _("You have unseen {0}").format(' Error Logs ') + } + + if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"): + log_settings = frappe.get_cached_doc('Log Settings') + + if log_settings.users_to_notify: + if user in [u.user for u in log_settings.users_to_notify]: + return _get_response() + else: + return _get_response(show_alert=False) + else: + return _get_response() \ No newline at end of file diff --git a/frappe/custom/doctype/custom_link/test_custom_link.py b/frappe/core/doctype/log_settings/test_log_settings.py similarity index 81% rename from frappe/custom/doctype/custom_link/test_custom_link.py rename to frappe/core/doctype/log_settings/test_log_settings.py index a292f73ad0..2824c71c88 100644 --- a/frappe/custom/doctype/custom_link/test_custom_link.py +++ b/frappe/core/doctype/log_settings/test_log_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestCustomLink(unittest.TestCase): +class TestLogSettings(unittest.TestCase): pass diff --git a/frappe/core/doctype/navbar_item/navbar_item.json b/frappe/core/doctype/navbar_item/navbar_item.json index 3bfea52558..541d785710 100644 --- a/frappe/core/doctype/navbar_item/navbar_item.json +++ b/frappe/core/doctype/navbar_item/navbar_item.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2020-08-01 23:38:41.783206", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "item_label", @@ -30,6 +29,7 @@ "in_list_view": 1, "label": "Item Type", "options": "Route\nAction\nSeparator", + "read_only_depends_on": "eval:doc.is_standard", "show_days": 1, "show_seconds": 1 }, @@ -59,6 +59,7 @@ "in_list_view": 1, "label": "Route", "mandatory_depends_on": "eval:doc.item_type == 'Route'", + "read_only_depends_on": "eval:doc.is_standard", "show_days": 1, "show_seconds": 1 }, @@ -68,13 +69,14 @@ "fieldtype": "Data", "label": "Action", "mandatory_depends_on": "eval:doc.item_type == 'Action'", + "read_only_depends_on": "eval:doc.is_standard", "show_days": 1, "show_seconds": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-06 16:32:49.597060", + "modified": "2020-11-02 10:57:37.709262", "modified_by": "Administrator", "module": "Core", "name": "Navbar Item", diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js index 14e9b3a901..f78fd3e812 100644 --- a/frappe/core/doctype/report/report.js +++ b/frappe/core/doctype/report/report.js @@ -2,7 +2,9 @@ frappe.ui.form.on('Report', { refresh: function(frm) { if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) { // make the document read-only - frm.set_read_only(); + frm.disable_form(); + } else { + frm.enable_save(); } let doc = frm.doc; @@ -32,8 +34,6 @@ frappe.ui.form.on('Report', { }); }, doc.disabled ? "fa fa-check" : "fa fa-off"); } - - frm.events.report_type(frm); }, ref_doctype: function(frm) { diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 8b7a03aa28..9d30409a2a 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -49,6 +49,10 @@ class Report(Document): self.export_doc() def on_trash(self): + if (self.is_standard == 'Yes' + and not cint(getattr(frappe.local.conf, 'developer_mode', 0)) + and not frappe.flags.in_patch): + frappe.throw(_("You are not allowed to delete Standard Report")) delete_custom_role('report', self.name) def get_columns(self): diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 805b903300..d76a1470e4 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals import frappe, json, os import unittest +from frappe.desk.query_report import run, save_report +from frappe.custom.doctype.customize_form.customize_form import reset_customization test_records = frappe.get_test_records('Report') test_dependencies = ['User'] @@ -27,7 +29,57 @@ class TestReport(unittest.TestCase): columns, data = report.get_data(filters={'user': 'Administrator', 'doctype': 'DocType'}) self.assertEqual(columns[0].get('label'), 'Name') self.assertEqual(columns[1].get('label'), 'Module') - self.assertTrue('User' in [d[0] for d in data]) + self.assertTrue('User' in [d.get('name') for d in data]) + + def test_custom_report(self): + reset_customization('User') + custom_report_name = save_report( + 'Permitted Documents For User', + 'Permitted Documents For User Custom', + json.dumps([{ + 'fieldname': 'email', + 'fieldtype': 'Data', + 'label': 'Email', + 'insert_after_index': 0, + 'link_field': 'name', + 'doctype': 'User', + 'options': 'Email', + 'width': 100, + 'id':'email', + 'name': 'Email' + }])) + custom_report = frappe.get_doc('Report', custom_report_name) + columns, result = custom_report.run_query_report( + filters={ + 'user': 'Administrator', + 'doctype': 'User' + }, user=frappe.session.user) + + self.assertListEqual(['email'], [column.get('fieldname') for column in columns]) + admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator') + self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict) + + def test_report_with_custom_column(self): + reset_customization('User') + response = run('Permitted Documents For User', + filters={'user': 'Administrator', 'doctype': 'User'}, + custom_columns=[{ + 'fieldname': 'email', + 'fieldtype': 'Data', + 'label': 'Email', + 'insert_after_index': 0, + 'link_field': 'name', + 'doctype': 'User', + 'options': 'Email', + 'width': 100, + 'id':'email', + 'name': 'Email' + }]) + result = response.get('result') + columns = response.get('columns') + self.assertListEqual(['name', 'email', 'user_type'], [column.get('fieldname') for column in columns]) + admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator') + self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict) def test_report_permissions(self): frappe.set_user('test@example.com') diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index 2a9c1a4573..d4d79b21fb 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -36,7 +36,7 @@ }, { "default": "0", - "depends_on": "eval:doc.queue==='All'", + "depends_on": "eval:doc.frequency==='All'", "fieldname": "create_log", "fieldtype": "Check", "label": "Create Log" @@ -49,7 +49,7 @@ }, { "allow_in_quick_entry": 1, - "depends_on": "eval:doc.queue==='Cron'", + "depends_on": "eval:doc.frequency==='Cron'", "fieldname": "cron_format", "fieldtype": "Data", "label": "Cron Format", @@ -81,7 +81,7 @@ "link_fieldname": "scheduled_job_type" } ], - "modified": "2020-04-05 17:27:33.480562", + "modified": "2020-10-07 10:39:24.519460", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index fa854f579e..0d6aa3d7d1 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -20,9 +20,9 @@ class ScheduledJobType(Document): # force logging for all events other than continuous ones (ALL) self.create_log = 1 - def enqueue(self): + def enqueue(self, force=False): # enqueue event if last execution is done - if self.is_event_due(): + if self.is_event_due() or force: if frappe.flags.enqueued_jobs: frappe.flags.enqueued_jobs.append(self.method) @@ -114,7 +114,7 @@ class ScheduledJobType(Document): def execute_event(doc): frappe.only_for('System Manager') doc = json.loads(doc) - frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue() + frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True) def run_scheduled_job(job_type): diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index e7db6f9045..d0a65defa4 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -22,7 +22,7 @@ class TestScheduledJobType(unittest.TestCase): self.assertEqual(all_job.frequency, 'All') daily_job = frappe.get_doc('Scheduled Job Type', - dict(method='frappe.email.queue.clear_outbox')) + dict(method='frappe.email.queue.set_expiry_for_email_queue')) self.assertEqual(daily_job.frequency, 'Daily') # check if cron jobs are synced @@ -38,7 +38,7 @@ class TestScheduledJobType(unittest.TestCase): self.assertEqual(updated_scheduled_job.frequency, "Hourly") def test_daily_job(self): - job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox')) + job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.set_expiry_for_email_queue')) job.db_set('last_execution', '2019-01-01 00:00:00') self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06'))) self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06'))) diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 97209cd8ea..c928939119 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -47,7 +47,7 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters): user = filters.get("user") user_perms = frappe.utils.user.UserPermissions(user) user_perms.build_permissions() - can_read = user_perms.can_read + can_read = user_perms.can_read # Does not include child tables single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})] diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 3946568bb6..2f0819ab68 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -30,6 +30,7 @@ "mandatory_depends_on", "read_only_depends_on", "properties", + "non_negative", "reqd", "unique", "read_only", @@ -403,13 +404,20 @@ "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" } ], "icon": "fa fa-glass", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-28 11:28:44.377753", + "modified": "2020-10-29 06:14:43.073329", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index bc325b654e..ee6e3b9c61 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -32,6 +32,7 @@ class CustomField(Document): self.fieldname = self.fieldname.lower() def before_insert(self): + self.set_fieldname() meta = frappe.get_meta(self.dt, cached=False) fieldnames = [df.fieldname for df in meta.get("fields")] diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js deleted file mode 100644 index 8662724b1a..0000000000 --- a/frappe/custom/doctype/custom_link/custom_link.js +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Custom Link', { - refresh: function(frm) { - frm.set_query("document_type", function () { - return { - filters: { - custom: 0, - istable: 0, - module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] - } - }; - }); - - frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() { - frappe.set_route('List', frm.doc.document_type); - }); - } -}); diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index b1743a96a5..2d220b864c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -5,6 +5,7 @@ frappe.provide("frappe.customize_form"); frappe.ui.form.on("Customize Form", { onload: function(frm) { + frm.disable_save(); frm.set_query("doc_type", function() { return { translate_values: false, @@ -27,7 +28,7 @@ frappe.ui.form.on("Customize Form", { }); $(frm.wrapper).on("grid-row-render", function(e, grid_row) { - if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") { + if (grid_row.doc && grid_row.doc.fieldtype=="Section Break") { $(grid_row.row).css({"font-weight": "bold"}); } }); @@ -40,19 +41,25 @@ frappe.ui.form.on("Customize Form", { frm.trigger("setup_sortable"); }); + if (localStorage['customize_doctype']) { + // set default value from customize form + frm.set_value('doc_type', localStorage['customize_doctype']); + } + }, doc_type: function(frm) { - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { return frm.call({ method: "fetch_to_customize", doc: frm.doc, freeze: true, callback: function(r) { - if(r) { - if(r._server_messages && r._server_messages.length) { + if (r) { + if (r._server_messages && r._server_messages.length) { frm.set_value("doc_type", ""); } else { + localStorage['customize_doctype'] = frm.doc.doc_type; frm.refresh(); frm.trigger("setup_sortable"); } @@ -69,7 +76,7 @@ frappe.ui.form.on("Customize Form", { frm.doc.fields.forEach(function(f, i) { var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row'); - if(f.is_custom_field) { + if (f.is_custom_field) { data_row.addClass("highlight"); } else { f._sortable = false; @@ -82,26 +89,26 @@ frappe.ui.form.on("Customize Form", { frm.disable_save(); frm.page.clear_icons(); - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { frappe.customize_form.set_primary_action(frm); frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() { frappe.set_route('List', frm.doc.doc_type); - }); + }, __('Actions')); - frm.add_custom_button(__('Refresh Form'), function() { + frm.add_custom_button(__('Reload'), function() { frm.script_manager.trigger("doc_type"); - }, "fa fa-refresh", "btn-default"); + }, __('Actions')); frm.add_custom_button(__('Reset to defaults'), function() { frappe.customize_form.confirm(__('Remove all customizations?'), frm); - }, "fa fa-eraser", "btn-default"); + }, __('Actions')); frm.add_custom_button(__('Set Permissions'), function() { frappe.set_route('permission-manager', frm.doc.doc_type); - }, "fa fa-lock", "btn-default"); + }, __('Actions')); - if(frappe.boot.developer_mode) { + if (frappe.boot.developer_mode) { frm.add_custom_button(__('Export Customizations'), function() { frappe.prompt( [ @@ -124,34 +131,36 @@ frappe.ui.form.on("Customize Form", { }); }, __("Select Module")); - }); + }, __('Actions')); } } // sort order select - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { var fields = $.map(frm.doc.fields, - function(df) { return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; }); + function(df) { + return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; + }); fields = ["", "name", "modified"].concat(fields); frm.set_df_property("sort_field", "options", fields); } - if(frappe.route_options && frappe.route_options.doc_type) { + if (frappe.route_options && frappe.route_options.doc_type) { setTimeout(function() { frm.set_value("doc_type", frappe.route_options.doc_type); frappe.route_options = null; }, 1000); } - } }); +// can't delete standard fields frappe.ui.form.on("Customize Form Field", { before_fields_remove: function(frm, doctype, name) { var row = frappe.get_doc(doctype, name); - if(!(row.is_custom_field || row.__islocal)) { + if (!(row.is_custom_field || row.__islocal)) { frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); - throw "cannot delete custom field"; + throw "cannot delete standard field"; } }, fields_add: function(frm, cdt, cdn) { @@ -160,16 +169,46 @@ frappe.ui.form.on("Customize Form Field", { } }); +// can't delete standard links +frappe.ui.form.on("DocType Link", { + before_links_remove: function(frm, doctype, name) { + let row = frappe.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + frappe.msgprint(__("Cannot delete standard link. You can hide it if you want")); + throw "cannot delete standard link"; + } + }, + links_add: function(frm, cdt, cdn) { + let f = frappe.model.get_doc(cdt, cdn); + f.custom = 1; + } +}); + +// can't delete standard actions +frappe.ui.form.on("DocType Action", { + before_actions_remove: function(frm, doctype, name) { + let row = frappe.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + frappe.msgprint(__("Cannot delete standard action. You can hide it if you want")); + throw "cannot delete standard action"; + } + }, + actions_add: function(frm, cdt, cdn) { + let f = frappe.model.get_doc(cdt, cdn); + f.custom = 1; + } +}); + frappe.customize_form.set_primary_action = function(frm) { frm.page.set_primary_action(__("Update"), function() { - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { return frm.call({ doc: frm.doc, freeze: true, btn: frm.page.btn_primary, method: "save_customization", callback: function(r) { - if(!r.exc) { + if (!r.exc) { frappe.customize_form.clear_locals_and_refresh(frm); frm.script_manager.trigger("doc_type"); } @@ -180,7 +219,7 @@ frappe.customize_form.set_primary_action = function(frm) { }; frappe.customize_form.confirm = function(msg, frm) { - if(!frm.doc.doc_type) return; + if (!frm.doc.doc_type) return; var d = new frappe.ui.Dialog({ title: 'Reset To Defaults', @@ -192,7 +231,7 @@ frappe.customize_form.confirm = function(msg, frm) { doc: frm.doc, method: "reset_to_defaults", callback: function(r) { - if(r.exc) { + if (r.exc) { frappe.msgprint(r.exc); } else { d.hide(); diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index cd57aa23fe..ff102b3c08 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -10,8 +10,9 @@ "doc_type", "properties", "label", - "default_print_format", "max_attachments", + "search_fields", + "column_break_5", "allow_copy", "istable", "editable_grid", @@ -20,22 +21,27 @@ "track_views", "allow_auto_repeat", "allow_import", - "show_preview_popup", - "image_view", - "column_break_5", + "fields_section_break", + "fields", + "view_settings_section", "title_field", "image_field", - "search_fields", - "section_break_8", - "sort_field", - "column_break_10", - "sort_order", - "section_break_23", + "default_print_format", + "column_break_29", + "show_preview_popup", + "image_view", + "email_settings_section", "email_append_to", "sender_field", "subject_field", - "fields_section_break", - "fields" + "document_actions_section", + "actions", + "document_links_section", + "links", + "section_break_8", + "sort_field", + "column_break_10", + "sort_order" ], "fields": [ { @@ -130,9 +136,11 @@ "label": "Search Fields" }, { + "collapsible": 1, "depends_on": "doc_type", "fieldname": "section_break_8", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "List Settings" }, { "fieldname": "sort_field", @@ -161,7 +169,8 @@ "fieldname": "fields", "fieldtype": "Table", "label": "Fields", - "options": "Customize Form Field" + "options": "Customize Form Field", + "reqd": 1 }, { "default": "0", @@ -200,24 +209,67 @@ "fieldtype": "Check", "label": "Allow document creation via Email" }, - { - "depends_on": "doc_type", - "fieldname": "section_break_23", - "fieldtype": "Section Break" - }, { "default": "0", "fieldname": "show_preview_popup", "fieldtype": "Check", "label": "Show Preview Popup" + }, + { + "collapsible": 1, + "depends_on": "doc_type", + "fieldname": "view_settings_section", + "fieldtype": "Section Break", + "label": "View Settings" + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "email_append_to", + "depends_on": "doc_type", + "fieldname": "email_settings_section", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "collapsible": 1, + "collapsible_depends_on": "links", + "depends_on": "doc_type", + "fieldname": "document_links_section", + "fieldtype": "Section Break", + "label": "Document Links" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" + }, + { + "collapsible": 1, + "collapsible_depends_on": "actions", + "depends_on": "doc_type", + "fieldname": "document_actions_section", + "fieldtype": "Section Break", + "label": "Document Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" } ], "hide_toolbar": 1, "icon": "fa fa-glass", "idx": 1, + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-04-10 12:16:01.320411", + "modified": "2020-09-24 14:16:49.594012", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index d4eeba3f93..9ce602906c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals Customize Form is a Single DocType used to mask the Property Setter Thus providing a better UI from user perspective """ +import json import frappe import frappe.translate from frappe import _ @@ -14,80 +15,9 @@ from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.custom.doctype.property_setter.property_setter import delete_property_setter from frappe.model.docfield import supports_translation -doctype_properties = { - 'search_fields': 'Data', - 'title_field': 'Data', - 'image_field': 'Data', - 'sort_field': 'Data', - 'sort_order': 'Data', - 'default_print_format': 'Data', - 'allow_copy': 'Check', - 'istable': 'Check', - 'quick_entry': 'Check', - 'editable_grid': 'Check', - 'max_attachments': 'Int', - 'track_changes': 'Check', - 'track_views': 'Check', - 'allow_auto_repeat': 'Check', - 'allow_import': 'Check', - 'show_preview_popup': 'Check', - 'email_append_to': 'Check', - 'subject_field': 'Data', - 'sender_field': 'Data' -} - -docfield_properties = { - 'idx': 'Int', - 'label': 'Data', - 'fieldtype': 'Select', - 'options': 'Text', - 'fetch_from': 'Small Text', - 'fetch_if_empty': 'Check', - 'permlevel': 'Int', - 'width': 'Data', - 'print_width': 'Data', - 'reqd': 'Check', - 'unique': 'Check', - 'ignore_user_permissions': 'Check', - 'in_list_view': 'Check', - 'in_standard_filter': 'Check', - 'in_global_search': 'Check', - 'in_preview': 'Check', - 'bold': 'Check', - 'hidden': 'Check', - 'collapsible': 'Check', - 'collapsible_depends_on': 'Data', - 'print_hide': 'Check', - 'print_hide_if_no_value': 'Check', - 'report_hide': 'Check', - 'allow_on_submit': 'Check', - 'translatable': 'Check', - 'mandatory_depends_on': 'Data', - 'read_only_depends_on': 'Data', - 'depends_on': 'Data', - 'description': 'Text', - 'default': 'Text', - 'precision': 'Select', - 'read_only': 'Check', - 'length': 'Int', - 'columns': 'Int', - 'remember_last_selected_value': 'Check', - 'allow_bulk_edit': 'Check', - 'auto_repeat': 'Link', - 'allow_in_quick_entry': 'Check', - 'hide_border': 'Check', - 'hide_days': 'Check', - 'hide_seconds': 'Check' -} - -allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), - ('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), ('Data', 'Select'), - ('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'), ('Table', 'Table MultiSelect')) - -allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data') - class CustomizeForm(Document): def on_update(self): frappe.db.sql("delete from tabSingles where doctype='Customize Form'") @@ -100,37 +30,64 @@ class CustomizeForm(Document): meta = frappe.get_meta(self.doc_type) - if self.doc_type in core_doctypes_list: - return frappe.msgprint(_("Core DocTypes cannot be customized.")) + self.validate_doctype(meta) - if meta.issingle: - return frappe.msgprint(_("Single DocTypes cannot be customized.")) - - if meta.custom: - return frappe.msgprint(_("Only standard DocTypes are allowed to be customized from Customize Form.")) - - # doctype properties - for property in doctype_properties: - self.set(property, meta.get(property)) - - for d in meta.get("fields"): - new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name} - for property in docfield_properties: - new_d[property] = d.get(property) - self.append("fields", new_d) + # load the meta properties on the customize (self) object + self.load_properties(meta) # load custom translation translation = self.get_name_translation() self.label = translation.translated_text if translation else '' - #If allow_auto_repeat is set, add auto_repeat custom field. + self.create_auto_repeat_custom_field_if_requried(meta) + + # NOTE doc (self) is sent to clientside by run_method + + def validate_doctype(self, meta): + ''' + Check if the doctype is allowed to be customized. + ''' + if self.doc_type in core_doctypes_list: + frappe.throw(_("Core DocTypes cannot be customized.")) + + if meta.issingle: + frappe.throw(_("Single DocTypes cannot be customized.")) + + if meta.custom: + frappe.throw(_("Only standard DocTypes are allowed to be customized from Customize Form.")) + + def load_properties(self, meta): + ''' + Load the customize object (this) with the metadata properties + ''' + # doctype properties + for prop in doctype_properties: + self.set(prop, meta.get(prop)) + + for d in meta.get("fields"): + new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name} + for prop in docfield_properties: + new_d[prop] = d.get(prop) + self.append("fields", new_d) + + for fieldname in ('links', 'actions'): + for d in meta.get(fieldname): + self.append(fieldname, d) + + def create_auto_repeat_custom_field_if_requried(self, meta): if self.allow_auto_repeat: - if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}): + if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', + 'dt': self.doc_type}): insert_after = self.fields[len(self.fields) - 1].fieldname - df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1) + df = dict( + fieldname='auto_repeat', + label='Auto Repeat', + fieldtype='Link', + options='Auto Repeat', + insert_after=insert_after, + read_only=1, no_copy=1, print_hide=1) create_custom_field(self.doc_type, df) - # NOTE doc is sent to clientside by run_method def get_name_translation(self): '''Get translation object if exists of current doctype name in the default language''' @@ -195,72 +152,142 @@ class CustomizeForm(Document): def set_property_setters(self): meta = frappe.get_meta(self.doc_type) - # doctype property setters - for property in doctype_properties: - if self.get(property) != meta.get(property): - self.make_property_setter(property=property, value=self.get(property), - property_type=doctype_properties[property]) + # doctype + self.set_property_setters_for_doctype(meta) + # docfield for df in self.get("fields"): meta_df = meta.get("fields", {"fieldname": df.fieldname}) - if not meta_df or meta_df[0].get("is_custom_field"): continue + self.set_property_setters_for_docfield(meta, df, meta_df) - for property in docfield_properties: - if property != "idx" and (df.get(property) or '') != (meta_df[0].get(property) or ''): - if property == "fieldtype": - self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) + # action and links + self.set_property_setters_for_actions_and_links(meta) - elif property == "allow_on_submit" and df.get(property): - if not frappe.db.get_value("DocField", - {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): - frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ - .format(df.idx)) - continue + def set_property_setters_for_doctype(self, meta): + for prop, prop_type in doctype_properties.items(): + if self.get(prop) != meta.get(prop): + self.make_property_setter(prop, self.get(prop), prop_type) - elif property == "reqd" and \ - ((frappe.db.get_value("DocField", - {"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \ - and (df.get(property) == 0)): - frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\ - .format(df.idx)) - continue + def set_property_setters_for_docfield(self, meta, df, meta_df): + for prop, prop_type in docfield_properties.items(): + if prop != "idx" and (df.get(prop) or '') != (meta_df[0].get(prop) or ''): + if not self.allow_property_change(prop, meta_df, df): + continue - elif property == "in_list_view" and df.get(property) \ - and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields: - frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}") - .format(df.fieldtype, df.idx)) - continue + self.make_property_setter(prop, df.get(prop), prop_type, + fieldname=df.fieldname) - elif property == "precision" and cint(df.get("precision")) > 6 \ - and cint(df.get("precision")) > cint(meta_df[0].get("precision")): - self.flags.update_db = True + def allow_property_change(self, prop, meta_df, df): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) - elif property == "unique": - self.flags.update_db = True + elif prop == "allow_on_submit" and df.get(prop): + if not frappe.db.get_value("DocField", + {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): + frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ + .format(df.idx)) + return False - elif (property == "read_only" and cint(df.get("read_only"))==0 - and frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "read_only")==1): - # if docfield has read_only checked and user is trying to make it editable, don't allow it - frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) - continue + elif prop == "reqd" and \ + ((frappe.db.get_value("DocField", + {"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \ + and (df.get(prop) == 0)): + frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\ + .format(df.idx)) + return False - elif property == "options" and df.get("fieldtype") not in allowed_fieldtype_for_options_change: - frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) - continue + elif prop == "in_list_view" and df.get(prop) \ + and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields: + frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}") + .format(df.fieldtype, df.idx)) + return False - elif property == 'translatable' and not supports_translation(df.get('fieldtype')): - frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label)) - continue + elif prop == "precision" and cint(df.get("precision")) > 6 \ + and cint(df.get("precision")) > cint(meta_df[0].get("precision")): + self.flags.update_db = True - elif (property == 'in_global_search' and - df.in_global_search != meta_df[0].get("in_global_search")): - self.flags.rebuild_doctype_for_global_search = True + elif prop == "unique": + self.flags.update_db = True - self.make_property_setter(property=property, value=df.get(property), - property_type=docfield_properties[property], fieldname=df.fieldname) + elif (prop == "read_only" and cint(df.get("read_only"))==0 + and frappe.db.get_value("DocField", {"parent": self.doc_type, + "fieldname": df.fieldname}, "read_only")==1): + # if docfield has read_only checked and user is trying to make it editable, don't allow it + frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) + return False + + elif prop == "options" and df.get("fieldtype") not in ALLOWED_OPTIONS_CHANGE: + frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) + return False + + elif prop == 'translatable' and not supports_translation(df.get('fieldtype')): + frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label)) + return False + + elif (prop == 'in_global_search' and + df.in_global_search != meta_df[0].get("in_global_search")): + self.flags.rebuild_doctype_for_global_search = True + + return True + + def set_property_setters_for_actions_and_links(self, meta): + ''' + Apply property setters or create custom records for DocType Action and DocType Link + ''' + for doctype, fieldname, field_map in ( + ('DocType Link', 'links', doctype_link_properties), + ('DocType Action', 'actions', doctype_action_properties) + ): + has_custom = False + items = [] + for i, d in enumerate(self.get(fieldname) or []): + d.idx = i + if frappe.db.exists(doctype, d.name) and not d.custom: + # check property and apply property setter + original = frappe.get_doc(doctype, d.name) + for prop, prop_type in field_map.items(): + if d.get(prop) != original.get(prop): + self.make_property_setter(prop, d.get(prop), prop_type, + apply_on=doctype, row_name=d.name) + items.append(d.name) + else: + # custom - just insert/update + d.parent = self.doc_type + d.custom = 1 + d.save(ignore_permissions=True) + has_custom = True + items.append(d.name) + + self.update_order_property_setter(has_custom, fieldname) + self.clear_removed_items(doctype, items) + + def update_order_property_setter(self, has_custom, fieldname): + ''' + We need to maintain the order of the link/actions if the user has shuffled them. + So we create a new property (ex `links_order`) to keep a list of items. + ''' + property_name = '{}_order'.format(fieldname) + if has_custom: + # save the order of the actions and links + self.make_property_setter(property_name, + json.dumps([d.name for d in self.get(fieldname)]), 'Small Text') + else: + frappe.db.delete('Property Setter', dict(property=property_name, + doc_type=self.doc_type)) + + + def clear_removed_items(self, doctype, items): + ''' + Clear rows that do not appear in `items`. These have been removed by the user. + ''' + if items: + frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1, + name=('not in', items))) + else: + frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1)) def update_custom_fields(self): for i, df in enumerate(self.get("fields")): @@ -278,8 +305,8 @@ class CustomizeForm(Document): d.dt = self.doc_type - for property in docfield_properties: - d.set(property, df.get(property)) + for prop in docfield_properties: + d.set(prop, df.get(prop)) if i!=0: d.insert_after = self.fields[i-1].fieldname @@ -297,12 +324,12 @@ class CustomizeForm(Document): custom_field = frappe.get_doc("Custom Field", meta_df[0].name) changed = False - for property in docfield_properties: - if df.get(property) != custom_field.get(property): - if property == "fieldtype": - self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) + for prop in docfield_properties: + if df.get(prop) != custom_field.get(prop): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) - custom_field.set(property, df.get(property)) + custom_field.set(prop, df.get(prop)) changed = True # check and update `insert_after` property @@ -328,32 +355,28 @@ class CustomizeForm(Document): if df.get("is_custom_field"): frappe.delete_doc("Custom Field", df.name) - def make_property_setter(self, property, value, property_type, fieldname=None): - self.delete_existing_property_setter(property, fieldname) + def make_property_setter(self, prop, value, property_type, fieldname=None, + apply_on=None, row_name = None): + delete_property_setter(self.doc_type, prop, fieldname) - property_value = self.get_existing_property_value(property, fieldname) + property_value = self.get_existing_property_value(prop, fieldname) if property_value==value: return + if not apply_on: + apply_on = "DocField" if fieldname else "DocType" + # create a new property setter - # ignore validation becuase it will be done at end frappe.make_property_setter({ "doctype": self.doc_type, - "doctype_or_field": "DocField" if fieldname else "DocType", + "doctype_or_field": apply_on, "fieldname": fieldname, - "property": property, + "row_name": row_name, + "property": prop, "value": value, "property_type": property_type - }, ignore_validate=True) - - def delete_existing_property_setter(self, property, fieldname=None): - # first delete existing property setter - existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.doc_type, - "property": property, "field_name['']": fieldname or ''}) - - if existing_property_setter: - frappe.db.sql("delete from `tabProperty Setter` where name=%s", existing_property_setter) + }) def get_existing_property_value(self, property_name, fieldname=None): # check if there is any need to make property setter! @@ -361,20 +384,17 @@ class CustomizeForm(Document): property_value = frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": fieldname}, property_name) else: - try: + if frappe.db.has_column("DocType", property_name): property_value = frappe.db.get_value("DocType", self.doc_type, property_name) - except Exception as e: - if frappe.db.is_column_missing(e): - property_value = None - else: - raise + else: + property_value = None return property_value def validate_fieldtype_change(self, df, old_value, new_value): allowed = False self.check_length_for_fieldtypes = [] - for allowed_changes in allowed_fieldtype_change: + for allowed_changes in ALLOWED_FIELDTYPE_CHANGE: if (old_value in allowed_changes and new_value in allowed_changes): allowed = True old_value_length = cint(frappe.db.type_map.get(old_value)[1]) @@ -425,8 +445,109 @@ class CustomizeForm(Document): if not self.doc_type: return - frappe.db.sql("""DELETE FROM `tabProperty Setter` WHERE doc_type=%s - and `field_name`!='naming_series' - and `property`!='options'""", self.doc_type) - frappe.clear_cache(doctype=self.doc_type) + reset_customization(self.doc_type) self.fetch_to_customize() + +def reset_customization(doctype): + frappe.db.sql(""" + DELETE FROM `tabProperty Setter` WHERE doc_type=%s + and `field_name`!='naming_series' + and `property`!='options' + """, doctype) + frappe.clear_cache(doctype=doctype) + +doctype_properties = { + 'search_fields': 'Data', + 'title_field': 'Data', + 'image_field': 'Data', + 'sort_field': 'Data', + 'sort_order': 'Data', + 'default_print_format': 'Data', + 'allow_copy': 'Check', + 'istable': 'Check', + 'quick_entry': 'Check', + 'editable_grid': 'Check', + 'max_attachments': 'Int', + 'track_changes': 'Check', + 'track_views': 'Check', + 'allow_auto_repeat': 'Check', + 'allow_import': 'Check', + 'show_preview_popup': 'Check', + 'email_append_to': 'Check', + 'subject_field': 'Data', + 'sender_field': 'Data' +} + +docfield_properties = { + 'idx': 'Int', + 'label': 'Data', + 'fieldtype': 'Select', + 'options': 'Text', + 'fetch_from': 'Small Text', + 'fetch_if_empty': 'Check', + 'permlevel': 'Int', + 'width': 'Data', + 'print_width': 'Data', + 'non_negative': 'Check', + 'reqd': 'Check', + 'unique': 'Check', + 'ignore_user_permissions': 'Check', + 'in_list_view': 'Check', + 'in_standard_filter': 'Check', + 'in_global_search': 'Check', + 'in_preview': 'Check', + 'bold': 'Check', + 'hidden': 'Check', + 'collapsible': 'Check', + 'collapsible_depends_on': 'Data', + 'print_hide': 'Check', + 'print_hide_if_no_value': 'Check', + 'report_hide': 'Check', + 'allow_on_submit': 'Check', + 'translatable': 'Check', + 'mandatory_depends_on': 'Data', + 'read_only_depends_on': 'Data', + 'depends_on': 'Data', + 'description': 'Text', + 'default': 'Text', + 'precision': 'Select', + 'read_only': 'Check', + 'length': 'Int', + 'columns': 'Int', + 'remember_last_selected_value': 'Check', + 'allow_bulk_edit': 'Check', + 'auto_repeat': 'Link', + 'allow_in_quick_entry': 'Check', + 'hide_border': 'Check', + 'hide_days': 'Check', + 'hide_seconds': 'Check' +} + +doctype_link_properties = { + 'link_doctype': 'Link', + 'link_fieldname': 'Data', + 'group': 'Data', + 'hidden': 'Check' +} + +doctype_action_properties = { + 'label': 'Link', + 'action_type': 'Select', + 'action': 'Small Text', + 'group': 'Data', + 'hidden': 'Check' +} + + +ALLOWED_FIELDTYPE_CHANGE = ( + ('Currency', 'Float', 'Percent'), + ('Small Text', 'Data'), + ('Text', 'Data'), + ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), + ('Data', 'Select'), + ('Text', 'Small Text'), + ('Text', 'Data', 'Barcode'), + ('Code', 'Geolocation'), + ('Table', 'Table MultiSelect')) + +ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data') diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index cace25a03d..46a2f2f9df 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, unittest, json from frappe.test_runner import make_test_records_for_doctype from frappe.core.doctype.doctype.doctype import InvalidFieldNameError +from frappe.core.doctype.doctype.test_doctype import new_doctype test_dependencies = ["Custom Field", "Property Setter"] class TestCustomizeForm(unittest.TestCase): @@ -24,6 +25,7 @@ class TestCustomizeForm(unittest.TestCase): def setUp(self): self.insert_custom_field() + frappe.db.delete('Property Setter', dict(doc_type='Event')) frappe.db.commit() frappe.clear_cache(doctype="Event") @@ -185,9 +187,75 @@ class TestCustomizeForm(unittest.TestCase): d.run_method("save_customization") def test_core_doctype_customization(self): - d = self.get_customize_form('User') - e = self.get_customize_form('Custom Field') + self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User') - # core doctype is invalid, hence no attributes are set - self.assertEquals(d.get("fields"), []) - self.assertEquals(e.get("fields"), []) + def test_custom_link(self): + try: + # create a dummy doctype linked to Event + testdt_name = 'Test Link for Event' + testdt = new_doctype(testdt_name, fields=[ + dict(fieldtype='Link', fieldname='event', options='Event') + ]).insert() + + testdt_name1 = 'Test Link for Event 1' + testdt1 = new_doctype(testdt_name1, fields=[ + dict(fieldtype='Link', fieldname='event', options='Event') + ]).insert() + + # add a custom link + d = self.get_customize_form("Event") + + d.append('links', dict(link_doctype=testdt_name, link_fieldname='event', group='Tests')) + d.append('links', dict(link_doctype=testdt_name1, link_fieldname='event', group='Tests')) + + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + # check links exist + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name]) + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name1]) + + # check order + order = json.loads(event.links_order) + self.assertListEqual(order, [d.name for d in event.links]) + + # remove the link + d = self.get_customize_form("Event") + d.links = [] + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + self.assertFalse([d.name for d in (event.links or []) if d.link_doctype == testdt_name]) + finally: + testdt.delete() + testdt1.delete() + + def test_custom_action(self): + test_route = '#List/DocType' + + # create a dummy action (route) + d = self.get_customize_form("Event") + d.append('actions', dict(label='Test Action', action_type='Route', action=test_route)) + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + # check if added to meta + action = [d for d in event.actions if d.label=='Test Action'] + self.assertEqual(len(action), 1) + self.assertEqual(action[0].action, test_route) + + # clear the action + d = self.get_customize_form("Event") + d.actions = [] + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + action = [d for d in event.actions if d.label=='Test Action'] + self.assertEqual(len(action), 0) diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 1c7349ef01..227114137c 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -11,8 +11,7 @@ "label", "fieldtype", "fieldname", - "hide_seconds", - "hide_days", + "non_negative", "reqd", "unique", "in_list_view", @@ -23,6 +22,7 @@ "allow_in_quick_entry", "translatable", "column_break_7", + "default", "precision", "length", "options", @@ -47,8 +47,9 @@ "column_break_33", "read_only_depends_on", "display", - "default", "in_filter", + "hide_seconds", + "hide_days", "column_break_21", "description", "print_hide", @@ -100,6 +101,7 @@ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", "fieldname": "reqd", "fieldtype": "Check", + "in_list_view": 1, "label": "Mandatory", "oldfieldname": "reqd", "oldfieldtype": "Check", @@ -283,7 +285,7 @@ }, { "fieldname": "default", - "fieldtype": "Text", + "fieldtype": "Small Text", "label": "Default", "oldfieldname": "default", "oldfieldtype": "Text" @@ -413,13 +415,20 @@ "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" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-28 11:28:59.084060", + "modified": "2020-10-29 06:11:57.661039", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json deleted file mode 100644 index 6d011bd4e4..0000000000 --- a/frappe/custom/doctype/package_document_type/package_document_type.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-14 16:45:47.196395", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "document_type", - "column_break_2", - "attachments", - "overwrite", - "section_break_4", - "filters_json" - ], - "fields": [ - { - "fieldname": "document_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Document Type", - "options": "DocType", - "reqd": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "attachments", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Include Attachments" - }, - { - "default": "0", - "fieldname": "overwrite", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Overwrite" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "filters_json", - "fieldtype": "Code", - "label": "Filters", - "options": "JSON" - } - ], - "istable": 1, - "links": [], - "modified": "2020-05-14 16:45:47.196395", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Document Type", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json deleted file mode 100644 index baeb7cb8bc..0000000000 --- a/frappe/custom/doctype/package_publish_target/package_publish_target.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-13 16:04:32.724663", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "instance_url", - "username", - "password" - ], - "fields": [ - { - "fieldname": "instance_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Site URL", - "reqd": 1 - }, - { - "fieldname": "username", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Username", - "reqd": 1 - }, - { - "fieldname": "password", - "fieldtype": "Password", - "in_list_view": 1, - "label": "Password", - "reqd": 1 - } - ], - "istable": 1, - "links": [], - "modified": "2020-05-15 17:35:16.282235", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Publish Target", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/custom/doctype/package_publish_target/package_publish_target.py deleted file mode 100644 index 34eee02562..0000000000 --- a/frappe/custom/doctype/package_publish_target/package_publish_target.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class PackagePublishTarget(Document): - pass diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js deleted file mode 100644 index a0190a8d8c..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Package Publish Tool', { - refresh: function(frm) { - frm.set_query("document_type", "package_details", function () { - return { - filters: { - "istable": 0, - } - }; - }); - - frappe.realtime.on("package", (data) => { - frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); - if ((data.progress+1) != data.total) { - frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message])); - } else { - frm.dashboard.hide_progress(); - } - }); - - frm.trigger("show_instructions"); - frm.trigger("last_deployed_on"); - frm.trigger("set_dirty_trigger"); - frm.trigger("set_deploy_primary_action"); - }, - last_deployed_on: function(frm) { - if (frm.doc.last_deployed_on) { - frm.trigger("show_indicator"); - } - }, - show_indicator: function(frm) { - let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on); - frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue"); - }, - set_dirty_trigger: function(frm) { - $(frm.wrapper).on("dirty", function() { - frm.page.set_primary_action(__('Save'), () => frm.save()); - }); - }, - set_deploy_primary_action: function(frm) { - if (frm.doc.package_details.length && frm.doc.instances.length) { - frm.page.set_primary_action(__("Publish"), function () { - frappe.show_alert({ - message: __("Publishing documents..."), - indicator: "green" - }); - - frappe.call({ - method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package", - callback: function() { - frm.reload_doc(); - frappe.msgprint(__("Documents have been published.")); - } - }); - }); - } - }, - show_instructions: function(frm) { - let field = frm.get_field("html_info"); - field.html(` -

- Package Publish Tool let's you copy documents from your site to any other remote site. - Follow the steps below to publish. -

-
    -
  1. Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
  2. -
  3. Add the Sites URL where you want to copy these documents, and enter the Username and Password.
  4. -
  5. Click on Save. Now, you can click on Publish and the documents will be copied.
  6. -
- `); - } -}); - -frappe.ui.form.on('Package Document Type', { - form_render: function (frm, cdt, cdn) { - function _show_filters(filters, table) { - table.find('tbody').empty(); - - if (filters.length > 0) { - filters.forEach(filter => { - const filter_row = - $(` - ${filter[1]} - ${filter[2] || ""} - ${filter[3]} - `); - - table.find('tbody').append(filter_row); - }); - } else { - const filter_row = $(` - ${__("Click to Set Filters")}`); - table.find('tbody').append(filter_row); - } - } - - let row = frappe.get_doc(cdt, cdn); - - let wrapper = $(`[data-fieldname="filters_json"]`).empty(); - let table = $(` - - - - - - - - - -
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); - $(`

${__("Click table to edit")}

`).appendTo(wrapper); - - let filters = JSON.parse(row.filters_json || '[]'); - _show_filters(filters, table); - - table.on('click', () => { - if (!row.document_type) { - frappe.msgprint(__("Select Document Type.")); - return; - } - - frappe.model.with_doctype(row.document_type, function() { - let dialog = new frappe.ui.Dialog({ - title: __('Set Filters'), - fields: [ - { - fieldtype: 'HTML', - label: 'Filters', - fieldname: 'filter_area', - } - ], - primary_action: function() { - let values = filter_group.get_filters(); - let flt = []; - if (values) { - values.forEach(function(value) { - flt.push([value[0], value[1], value[2], value[3]]); - }); - } - row.filters_json = JSON.stringify(flt); - _show_filters(flt, table); - dialog.hide(); - }, - primary_action_label: "Set" - }); - - let filter_group = new frappe.ui.FilterGroup({ - parent: dialog.get_field('filter_area').$wrapper, - doctype: row.document_type, - on_change: () => {}, - }); - filter_group.add_filters_to_filter_group(filters); - dialog.show(); - }); - }); - }, -}); diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json deleted file mode 100644 index 0f85ae0348..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "actions": [], - "creation": "2020-05-13 15:54:38.082657", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "html_info", - "sb_00", - "package_details", - "sb_01", - "instances", - "last_deployed_on" - ], - "fields": [ - { - "description": "Click on the row for accessing filters.", - "fieldname": "package_details", - "fieldtype": "Table", - "label": "Document Types", - "options": "Package Document Type", - "reqd": 1 - }, - { - "fieldname": "instances", - "fieldtype": "Table", - "label": "Sites", - "options": "Package Publish Target", - "reqd": 1 - }, - { - "fieldname": "html_info", - "fieldtype": "HTML" - }, - { - "fieldname": "last_deployed_on", - "fieldtype": "Datetime", - "hidden": 1, - "label": "Last Deployed On", - "read_only": 1 - }, - { - "fieldname": "sb_00", - "fieldtype": "Section Break" - }, - { - "fieldname": "sb_01", - "fieldtype": "Section Break" - } - ], - "issingle": 1, - "links": [], - "modified": "2020-05-15 17:31:37.060199", - "modified_by": "Administrator", - "module": "Custom", - "name": "Package Publish Tool", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "All", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py deleted file mode 100644 index b73f93a628..0000000000 --- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -import json -import datetime -import base64 -from frappe.model.document import Document -from frappe.utils.file_manager import save_file, get_file -from frappe import _ -from six import string_types -from frappe.frappeclient import FrappeClient -from frappe.utils import get_datetime_str, get_datetime -from frappe.utils.password import get_decrypted_password - -class PackagePublishTool(Document): - pass - -@frappe.whitelist() -def deploy_package(): - package, doc = export_package() - - file_name = "Package-" + get_datetime_str(get_datetime()) - - length = len(doc.instances) - for idx, instance in enumerate(doc.instances): - frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")}, - user=frappe.session.user) - - install_package_to_remote(package, instance) - - frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime()) - -def install_package_to_remote(package, instance): - try: - connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name)) - except Exception: - frappe.log_error(frappe.get_traceback()) - frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url)) - - try: - connection.post_request({ - "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package", - "package": json.dumps(package) - }) - except Exception: - frappe.log_error(frappe.get_traceback()) - frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url)) - -@frappe.whitelist() -def export_package(): - """Export package as JSON.""" - package_doc = frappe.get_single("Package Publish Tool") - package = [] - - for doctype in package_doc.package_details: - filters = [] - - if doctype.get("filters_json"): - filters = json.loads(doctype.get("filters_json")) - - docs = frappe.get_all(doctype.get("document_type"), filters=filters) - length = len(docs) - - for idx, doc in enumerate(docs): - frappe.publish_realtime("package", { - "progress":idx, "total":length, - "message":doctype.get("document_type"), - "prefix": _("Exporting") - }, - user=frappe.session.user) - - document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict() - attachments = [] - - if doctype.attachments: - filters = { - "attached_to_doctype": document.get("doctype"), - "attached_to_name": document.get("name") - } - - for f in frappe.get_list("File", filters=filters): - fname, fcontents = get_file(f.name) - attachments.append({ - "fname": fname, - "content": base64.b64encode(fcontents).decode('ascii') - }) - - document.update({ - "__attachments": attachments, - "__overwrite": True if doctype.overwrite else False - }) - - package.append(document) - - return post_process(package), package_doc - -@frappe.whitelist() -def import_package(package=None): - """Import package from JSON.""" - frappe.only_for("System Manager") - if isinstance(package, string_types): - package = json.loads(package) - - for doc in package: - modified = doc.pop("modified") - overwrite = doc.pop("__overwrite") - attachments = doc.pop("__attachments") - exists = frappe.db.exists(doc.get("doctype"), doc.get("name")) - - if not exists: - d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True) - if attachments: - add_attachment(attachments, d) - else: - docname = doc.pop("name") - document = frappe.get_doc(doc.get("doctype"), docname) - - if overwrite: - update_document(document, doc, attachments) - - else: - if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified): - update_document(document, doc, attachments) - -def update_document(document, doc, attachments): - document.update(doc) - document.save() - if attachments: - add_attachment(attachments, document) - -def add_attachment(attachments, doc): - for attachment in attachments: - save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name")) - -def post_process(package): - """Remove the keys from Document and Child Document. Convert datetime, date, time to str.""" - del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus') - child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name') - - for doc in package: - for key in del_keys: - if key in doc: - del doc[key] - - for key, value in doc.items(): - stringified_value = get_stringified_value(value) - if stringified_value: - doc[key] = stringified_value - - if not isinstance(value, list): - continue - - for child in value: - for child_key in child_del_keys: - if child_key in child: - del child[child_key] - - for child_key, child_value in child.items(): - stringified_value = get_stringified_value(child_value) - if stringified_value: - child[child_key] = stringified_value - - return package - -def get_stringified_value(value): - if isinstance(value, datetime.datetime): - return frappe.utils.get_datetime_str(value) - - if isinstance(value, datetime.date): - return frappe.utils.get_date_str(value) - - if isinstance(value, datetime.timedelta): - return frappe.utils.get_time_str(value) - - return None diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json index 5888e11969..b318d92c5a 100644 --- a/frappe/custom/doctype/property_setter/property_setter.json +++ b/frappe/custom/doctype/property_setter/property_setter.json @@ -1,358 +1,133 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-01-10 16:34:04", - "custom": 0, - "description": "Property Setter overrides a standard DocType or Field property", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "creation": "2013-01-10 16:34:04", + "description": "Property Setter overrides a standard DocType or Field property", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "help", + "sb0", + "doctype_or_field", + "doc_type", + "field_name", + "row_name", + "column_break0", + "property", + "property_type", + "value", + "default_value" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Help", - "length": 0, - "no_copy": 0, - "options": "
Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "
Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb0", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "sb0", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.__islocal", - "fieldname": "doctype_or_field", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "DocType or Field", - "length": 0, - "no_copy": 0, - "options": "\nDocField\nDocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "doctype_or_field", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Applied On", + "options": "\nDocField\nDocType\nDocType Link\nDocType Action", + "read_only_depends_on": "eval:!doc.__islocal", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "New value to be set", - "fieldname": "value", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Set Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "New value to be set", + "fieldname": "value", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Set Value" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "doc_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "doc_type", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.doctype_or_field=='DocField'", - "description": "ID (name) of the entity whose property is to be set", - "fieldname": "field_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Field Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:doc.doctype_or_field=='DocField'", + "description": "ID (name) of the entity whose property is to be set", + "fieldname": "field_name", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Field Name", + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "property", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Property", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "property", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Property", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "property_type", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Property Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "property_type", + "fieldtype": "Data", + "label": "Property Type" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "default_value", + "fieldtype": "Data", + "label": "Default Value" + }, + { + "description": "For DocType Link / DocType Action", + "fieldname": "row_name", + "fieldtype": "Data", + "label": "Row Name" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-glass", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:39:50.172883", - "modified_by": "Administrator", - "module": "Custom", - "name": "Property Setter", - "owner": "Administrator", + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-24 14:42:38.599684", + "modified_by": "Administrator", + "module": "Custom", + "name": "Property Setter", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "doc_type,property", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "search_fields": "doc_type,property", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index d8ab5ede73..56e5829271 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -11,13 +11,16 @@ not_allowed_fieldtype_change = ['naming_series'] class PropertySetter(Document): def autoname(self): - self.name = self.doc_type + "-" \ - + (self.field_name and (self.field_name + "-") or "") \ - + self.property + self.name = '{doctype}-{field}-{property}'.format( + doctype = self.doc_type, + field = self.field_name or self.row_name or 'main', + property = self.property + ) def validate(self): self.validate_fieldtype_change() - self.delete_property_setter() + if self.is_new(): + delete_property_setter(self.doc_type, self.property, self.field_name) # clear cache frappe.clear_cache(doctype = self.doc_type) @@ -27,15 +30,6 @@ class PropertySetter(Document): self.property == 'fieldtype': frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name)) - def delete_property_setter(self): - """delete other property setters on this, if this is new""" - if self.get('__islocal'): - frappe.db.sql("""delete from `tabProperty Setter` where - doctype_or_field = %(doctype_or_field)s - and doc_type = %(doc_type)s - and coalesce(field_name,'') = coalesce(%(field_name)s, '') - and property = %(property)s""", self.get_valid_dict()) - def get_property_list(self, dt): return frappe.db.get_all('DocField', fields=['fieldname', 'label', 'fieldtype'], @@ -89,3 +83,12 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.flags.validate_fields_for_doctype = validate_fields_for_doctype property_setter.insert() return property_setter + +def delete_property_setter(doc_type, property, field_name=None): + """delete other property setters on this, if this is new""" + filters = dict(doc_type = doc_type, property=property) + if field_name: + filters['field_name'] = field_name + + frappe.db.delete('Property Setter', filters) + diff --git a/frappe/data/sample_site_config.json b/frappe/data/sample_site_config.json index 36818ef286..715cd7b9fa 100644 --- a/frappe/data/sample_site_config.json +++ b/frappe/data/sample_site_config.json @@ -22,6 +22,9 @@ "use_ssl": 0, "auto_email_id": "hello@example.com", + "google_analytics_id": "google_analytics_id", + "google_analytics_anonymize_ip": 1, + "google_login": { "client_id": "google_client_id", "client_secret": "google_client_secret" diff --git a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.js b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.js index c3cf701d92..0898fcf4e7 100644 --- a/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.js +++ b/frappe/data_migration/doctype/data_migration_connector/data_migration_connector.js @@ -30,7 +30,7 @@ frappe.ui.form.on('Data Migration Connector', { frm.set_value('connector_type', 'Custom'); frm.set_value('python_module', r.message); frm.save(); - frappe.show_alert(__(`New module created ${r.message}`)); + frappe.show_alert(__("New module created {0}", [r.message])); d.hide(); } }); diff --git a/frappe/database/database.py b/frappe/database/database.py index d9755abd33..616dd3c3ec 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -319,8 +319,7 @@ class Database(object): nres.append(nr) return nres - @staticmethod - def build_conditions(filters): + def build_conditions(self, filters): """Convert filters sent as dict, lists to SQL conditions. filter's key is passed by map function, build conditions like: @@ -341,18 +340,12 @@ class Database(object): value = filters.get(key) values[key] = value if isinstance(value, (list, tuple)): - # value is a tuble like ("!=", 0) + # value is a tuple like ("!=", 0) _operator = value[0] values[key] = value[1] if isinstance(value[1], (tuple, list)): # value is a list in tuple ("in", ("A", "B")) - inner_list = [] - for i, v in enumerate(value[1]): - inner_key = "{0}_{1}".format(key, i) - values[inner_key] = v - inner_list.append("%({0})s".format(inner_key)) - - _rhs = " ({0})".format(", ".join(inner_list)) + _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]])) del values[key] if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]: @@ -787,6 +780,9 @@ class Database(object): """Returns True if table for given doctype exists.""" return ("tab" + doctype) in self.get_tables() + def has_table(self, doctype): + return self.table_exists(doctype) + def get_tables(self): tables = frappe.cache().get_value('db_tables') if not tables: @@ -959,13 +955,13 @@ class Database(object): query = sql_dict.get(current_dialect) return self.sql(query, values, **kwargs) - def delete(self, doctype, conditions): + def delete(self, doctype, conditions, debug=False): if conditions: conditions, values = self.build_conditions(conditions) return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format( doctype=doctype, conditions=conditions - ), values) + ), values, debug=debug) else: frappe.throw(_('No conditions provided')) diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 3d997864e4..4faea78551 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -140,11 +140,11 @@ class PostgresDatabase(Database): @staticmethod def is_table_missing(e): - return e.pgcode == '42P01' + return getattr(e, 'pgcode', None) == '42P01' @staticmethod def is_missing_column(e): - return e.pgcode == '42703' + return getattr(e, 'pgcode', None) == '42703' @staticmethod def is_access_denied(e): diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 1dc1ea4c97..f53872db82 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -20,8 +20,11 @@ def setup_database(force, source_sql=None, verbose=False): source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') subprocess.check_output([ - 'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U', - frappe.conf.db_name, '-f', source_sql + 'psql', frappe.conf.db_name, + '-h', frappe.conf.db_host or 'localhost', + '-p', str(frappe.conf.db_port or '5432'), + '-U', frappe.conf.db_name, + '-f', source_sql ], env=subprocess_env) frappe.connect() diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 52dc2ba917..daabbaa61c 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -186,7 +186,7 @@ class DbColumn: column_def += ' not null default {0}'.format(default_value) elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \ - and not self.default.startswith(":") and column_def not in ('text', 'longtext'): + and not cstr(self.default).startswith(":") and column_def not in ('text', 'longtext'): column_def += " default {}".format(frappe.db.escape(self.default)) if self.unique and (column_def not in ('text', 'longtext')): diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 72c4519120..4dab313892 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -375,7 +375,7 @@ def get_desk_sidebar_items(flatten=False, cache=True): # pages sorted based on pinned to top and then by name order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - all_pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True) + all_pages = frappe.get_all("Desk Page", fields=["name", "category", "module"], filters=filters, order_by=order_by, ignore_permissions=True) pages = [] # Filter Page based on Permission diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 7f26bd9101..f5d1ee0df5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -21,8 +21,10 @@ frappe.ui.form.on('Dashboard Chart', { refresh: function(frm) { frm.chart_filters = null; + frm.is_disabled = !frappe.boot.developer_mode && frm.doc.is_standard; - if (!frappe.boot.developer_mode && frm.doc.is_standard) { + if (frm.is_disabled) { + !frm.doc.custom_options && frm.set_df_property('chart_options_section', 'hidden', 1); frm.disable_form(); } @@ -169,7 +171,7 @@ frappe.ui.form.on('Dashboard Chart', { frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields); if (!frm.field_options.numeric_fields.length) { - frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); + frappe.msgprint(__("Report has no numeric fields, please change the Report Name")); } else { let y_field_df = frappe.meta.get_docfield('Dashboard Chart Field', 'y_field', frm.doc.name); y_field_df.options = frm.field_options.numeric_fields; @@ -333,6 +335,7 @@ frappe.ui.form.on('Dashboard Chart', { } table.on('click', () => { + frm.is_disabled && frappe.throw(__('Cannot edit filters for standard charts')); let dialog = new frappe.ui.Dialog({ title: __('Set Filters'), diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index c4c6077e85..a6126f1f9b 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -61,7 +61,7 @@ def make_notification_logs(doc, users): from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled for user in users: - if frappe.db.exists('User', user): + if frappe.db.exists('User', {"name": user, "enabled": 1}): if is_notifications_enabled(user): if doc.type == 'Energy Point' and not is_energy_point_enabled(): return diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js index b8b7f37a4f..88dc145be2 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.js +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -2,12 +2,19 @@ // For license information, please see license.txt frappe.ui.form.on('Notification Settings', { - onload: () => { + onload: (frm) => { frappe.breadcrumbs.add({ label: __('Settings'), route: '#modules/Settings', type: 'Custom' }); + frm.set_query('subscribed_documents', () => { + return { + filters: { + istable: 0 + } + }; + }); }, refresh: (frm) => { diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json index 85f93e156e..fc12022e89 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.json +++ b/frappe/desk/doctype/notification_settings/notification_settings.json @@ -22,68 +22,52 @@ "default": "1", "fieldname": "enabled", "fieldtype": "Check", - "label": "Enabled", - "show_days": 1, - "show_seconds": 1 + "label": "Enabled" }, { "fieldname": "subscribed_documents", "fieldtype": "Table MultiSelect", - "label": "Subscribed Documents", - "options": "Notification Subscribed Document", - "show_days": 1, - "show_seconds": 1 + "label": "Open Documents", + "options": "Notification Subscribed Document" }, { "fieldname": "column_break_3", "fieldtype": "Section Break", - "label": "Email Settings", - "show_days": 1, - "show_seconds": 1 + "label": "Email Settings" }, { "default": "1", "fieldname": "enable_email_notifications", "fieldtype": "Check", - "label": "Enable Email Notifications", - "show_days": 1, - "show_seconds": 1 + "label": "Enable Email Notifications" }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_mention", "fieldtype": "Check", - "label": "Mentions", - "show_days": 1, - "show_seconds": 1 + "label": "Mentions" }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_assignment", "fieldtype": "Check", - "label": "Assignments", - "show_days": 1, - "show_seconds": 1 + "label": "Assignments" }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_energy_point", "fieldtype": "Check", - "label": "Energy Points", - "show_days": 1, - "show_seconds": 1 + "label": "Energy Points" }, { "default": "1", "depends_on": "enable_email_notifications", "fieldname": "enable_email_share", "fieldtype": "Check", - "label": "Document Share", - "show_days": 1, - "show_seconds": 1 + "label": "Document Share" }, { "default": "__user", @@ -92,23 +76,20 @@ "hidden": 1, "label": "User", "options": "User", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "fieldname": "seen", "fieldtype": "Check", "hidden": 1, - "label": "Seen", - "show_days": 1, - "show_seconds": 1 + "label": "Seen" } ], "in_create": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-05-31 22:16:40.798019", + "modified": "2020-11-04 12:54:57.989317", "modified_by": "Administrator", "module": "Desk", "name": "Notification Settings", diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 63b41b956e..6d1454a2cb 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -207,7 +207,7 @@ frappe.ui.form.on('Number Card', { frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields); if (!frm.field_options.numeric_fields.length) { - frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); + frappe.msgprint(__("Report has no numeric fields, please change the Report Name")); } } else { frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name')); diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index 26b2bd2835..aee7a8e52a 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -168,8 +168,8 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', """ if not (assigned_by and owner and doc_type and doc_name): return - # self assignment / closing - no message - if assigned_by==owner: + # return if self assigned or user disabled + if assigned_by == owner or not frappe.db.get_value('User', owner, 'enabled'): return # Search for email address in description -- i.e. assignee @@ -177,7 +177,7 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE', title = get_title(doc_type, doc_name) description_html = "
{0}
".format(description) if description else None - if action=='CLOSE': + if action == 'CLOSE': subject = _('Your assignment on {0} {1} has been removed by {2}')\ .format(frappe.bold(doc_type), get_title_html(title), frappe.bold(user_name)) else: diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index 646c31f7a1..5f91b376e8 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -42,7 +42,7 @@ class UserProfile { } make_user_profile() { - frappe.set_route('user-profile', this.user_id); + frappe.set_route('user-profile', this.user_id, { redirect: true }); this.user = frappe.user_info(this.user_id); this.page.set_title(this.user.fullname); this.setup_user_search(); @@ -360,11 +360,12 @@ class UserProfile { this.get_user_rank().then(() => { this.get_user_points().then(() => { - let html = $(__(`

${__('Energy Points: ')}{0}

-

${__('Review Points: ')}{1}

-

${__('Rank: ')}{2}

-

${__('Monthly Rank: ')}{3}

- `, [this.energy_points, this.review_points, this.rank, this.month_rank])); + let html = $(` +

${__('Energy Points:')} ${this.energy_points}

+

${__('Review Points:')} ${this.review_points}

+

${__('Rank:')} ${this.rank}

+

${__('Monthly Rank:')} ${this.month_rank}

+ `); $profile_details.append(html); }); diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py index 06928f9855..73df6d78cb 100644 --- a/frappe/desk/page/user_profile/user_profile.py +++ b/frappe/desk/page/user_profile/user_profile.py @@ -1,17 +1,23 @@ import frappe from datetime import datetime +from frappe.utils import getdate @frappe.whitelist() def get_energy_points_heatmap_data(user, date): + try: + date = getdate(date) + except Exception: + date = getdate() + return dict(frappe.db.sql("""select unix_timestamp(date(creation)), sum(points) from `tabEnergy Point Log` where date(creation) > subdate('{date}', interval 1 year) and date(creation) < subdate('{date}', interval -1 year) and - user = '{user}' and + user = %s and type != 'Review' group by date(creation) - order by creation asc""".format(user = user, date = date))) + order by creation asc""".format(date = date), user)) @frappe.whitelist() diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 5a9aae8435..3008cf0e61 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -12,6 +12,7 @@ from frappe.modules import scrub, get_module_path from frappe.utils import ( flt, cint, + cstr, get_html_format, get_url_to_form, gzip_decompress, @@ -74,23 +75,27 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) res = report.execute_script_report(filters) columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6) + columns = [get_column_as_dict(col) for col in columns] + report_column_names = [col["fieldname"] for col in columns] + + # convert to list of dicts + result = normalize_result(result, columns) if report.custom_columns: - # Original query columns, needed to reorder data as per custom columns - query_columns = columns - # Reordered columns + # saved columns (with custom columns / with different column order) columns = json.loads(report.custom_columns) - result = reorder_data_for_custom_columns(columns, query_columns, result) - - result = add_data_to_custom_columns(columns, result) - + # unsaved custom_columns if custom_columns: - result = add_data_to_custom_columns(custom_columns, result) - for custom_column in custom_columns: columns.insert(custom_column["insert_after_index"] + 1, custom_column) + # all columns which are not in original report + report_custom_columns = [column for column in columns if column["fieldname"] not in report_column_names] + + if report_custom_columns: + result = add_custom_column_data(report_custom_columns, result) + if result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -109,6 +114,20 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) or 0, } +def normalize_result(result, columns): + # Converts to list of dicts from list of lists/tuples + data = [] + column_names = [column["fieldname"] for column in columns] + if result and isinstance(result[0], (list, tuple)): + for row in result: + row_obj = {} + for idx, column_name in enumerate(column_names): + row_obj[column_name] = row[idx] + data.append(row_obj) + else: + data = result + + return data @frappe.whitelist() def background_enqueue_run(report_name, filters=None, user=None): @@ -177,14 +196,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run( - report_name, - filters=None, - user=None, - ignore_prepared_report=False, - custom_columns=None, -): - +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None): report = get_report_doc(report_name) if not user: user = frappe.session.user @@ -221,69 +233,20 @@ def run( return result -def add_data_to_custom_columns(columns, result): - custom_fields_data = get_data_for_custom_report(columns) +def add_custom_column_data(custom_columns, result): + custom_column_data = get_data_for_custom_report(custom_columns) - data = [] - for row in result: - row_obj = {} - if isinstance(row, tuple): - row = list(row) + for column in custom_columns: + key = (column.get('doctype'), column.get('fieldname')) + if key in custom_column_data: + for row in result: + row_reference = row.get(column.get('link_field')) + # possible if the row is empty + if not row_reference: + continue + row[column.get('fieldname')] = custom_column_data.get(key).get(row_reference) - if isinstance(row, list): - for idx, column in enumerate(columns): - if column.get("link_field"): - row_obj[column["fieldname"]] = None - row.insert(idx, None) - else: - row_obj[column["fieldname"]] = row[idx] - data.append(row_obj) - else: - data.append(row) - - for row in data: - for column in columns: - if column.get("link_field"): - fieldname = column["fieldname"] - key = (column["doctype"], fieldname) - link_field = column["link_field"] - row[fieldname] = custom_fields_data.get(key, {}).get( - row.get(link_field) - ) - - return data - - -def reorder_data_for_custom_columns(custom_columns, columns, result): - if not result: - return [] - - columns = [get_column_as_dict(col) for col in columns] - if isinstance(result[0], list) or isinstance(result[0], tuple): - # If the result is a list of lists - custom_column_names = [col["label"] for col in custom_columns] - original_column_names = [col["label"] for col in columns] - return get_columns_from_list(custom_column_names, original_column_names, result) - else: - # columns do not need to be reordered if result is a list of dicts - return result - - -def get_columns_from_list(columns, target_columns, result): - reordered_result = [] - - for res in result: - r = [] - for col_name in columns: - try: - idx = target_columns.index(col_name) - r.append(res[idx]) - except ValueError: - pass - - reordered_result.append(r) - - return reordered_result + return result def get_prepared_report_result(report, filters, dn="", user=None): @@ -343,31 +306,27 @@ def get_prepared_report_result(report, filters, dn="", user=None): @frappe.whitelist() def export_query(): """export from query reports""" - data = frappe._dict(frappe.local.form_dict) - - del data["cmd"] - if "csrf_token" in data: - del data["csrf_token"] + data.pop("cmd", None) + data.pop("csrf_token", None) if isinstance(data.get("filters"), string_types): filters = json.loads(data["filters"]) - if isinstance(data.get("report_name"), string_types): + + if data.get("report_name"): report_name = data["report_name"] frappe.permissions.can_export( frappe.get_cached_value("Report", report_name, "ref_doctype"), raise_exception=True, ) - if isinstance(data.get("file_format_type"), string_types): - file_format_type = data["file_format_type"] - custom_columns = frappe.parse_json(data["custom_columns"]) + file_format_type = data.get("file_format_type") + custom_columns = frappe.parse_json(data.get("custom_columns", "[]")) + include_indentation = data.get("include_indentation") + visible_idx = data.get("visible_idx") - include_indentation = data["include_indentation"] - if isinstance(data.get("visible_idx"), string_types): - visible_idx = json.loads(data.get("visible_idx")) - else: - visible_idx = None + if isinstance(visible_idx, string_types): + visible_idx = json.loads(visible_idx) if file_format_type == "Excel": data = run(report_name, filters, custom_columns=custom_columns) @@ -386,8 +345,8 @@ def export_query(): data["result"] = handle_duration_fieldtype_values( data.get("result"), data.get("columns") ) - xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation) - xlsx_file = make_xlsx(xlsx_data, "Query Report") + xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation) + xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) frappe.response["filename"] = report_name + ".xlsx" frappe.response["filecontent"] = xlsx_file.getvalue() @@ -421,34 +380,38 @@ def handle_duration_fieldtype_values(result, columns): def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] + column_widths = [] - # add column headings - for idx in range(len(data.columns)): - if not columns[idx].get("hidden"): - result[0].append(columns[idx]["label"]) + for column in data.columns: + if column.get("hidden"): + continue + result[0].append(column["label"]) + column_width = cint(column.get('width', 0)) + # to convert into scale accepted by openpyxl + column_width /= 10 + column_widths.append(column_width) # build table from result - for i, row in enumerate(data.result): + for row_idx, row in enumerate(data.result): # only pick up rows that are visible in the report - if i in visible_idx: + if row_idx in visible_idx: row_data = [] - - if isinstance(row, dict) and row: - for idx in range(len(data.columns)): - # check if column is not hidden - if not columns[idx].get("hidden"): - label = columns[idx]["label"] - fieldname = columns[idx]["fieldname"] - cell_value = row.get(fieldname, row.get(label, "")) - if cint(include_indentation) and "indent" in row and idx == 0: - cell_value = (" " * cint(row["indent"])) + cell_value - row_data.append(cell_value) - else: + if isinstance(row, dict): + for col_idx, column in enumerate(data.columns): + if column.get("hidden"): + continue + label = column.get("label") + fieldname = column.get("fieldname") + cell_value = row.get(fieldname, row.get(label, "")) + if cint(include_indentation) and "indent" in row and col_idx == 0: + cell_value = (" " * cint(row["indent"])) + cstr(cell_value) + row_data.append(cell_value) + elif row: row_data = row result.append(row_data) - return result + return result, column_widths def add_total_row(result, columns, meta=None): @@ -755,6 +718,8 @@ def get_column_as_dict(col): col_dict["fieldtype"], col_dict["options"] = col[1].split("/") else: col_dict["fieldtype"] = col[1] + if len(col) == 3: + col_dict["width"] = col[2] col_dict["label"] = col[0] col_dict["fieldname"] = frappe.scrub(col[0]) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 798e499bb9..f249c36746 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -141,7 +141,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, # 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("%", "")), doctype=doctype)) + _txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype)) # In order_by, `idx` gets second priority, because it stores link count diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 14a3cfd9f1..343141c66d 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -25,7 +25,11 @@ from frappe.core.doctype.communication.email import set_incoming_outgoing_accoun from frappe.utils.html_utils import clean_email_html from frappe.email.utils import get_port -class SentEmailInInbox(Exception): pass +class SentEmailInInbox(Exception): + pass + +class InvalidEmailCredentials(frappe.ValidationError): + pass class EmailAccount(Document): def autoname(self): @@ -148,7 +152,7 @@ class EmailAccount(Document): return None args = frappe._dict({ - "email_account":self.name, + "email_account": self.name, "host": self.email_server, "use_ssl": self.use_ssl, "username": getattr(self, "login_id", None) or self.email_id, @@ -166,21 +170,45 @@ class EmailAccount(Document): frappe.throw(_("{0} is required").format("Email Server")) email_server = EmailServer(frappe._dict(args)) + self.check_email_server_connection(email_server, in_receive) + + if not in_receive and self.use_imap: + email_server.imap.logout() + + # reset failed attempts count + self.set_failed_attempts_count(0) + + return email_server + + def check_email_server_connection(self, email_server, in_receive): + # tries to connect to email server and handles failure try: email_server.connect() except (error_proto, imaplib.IMAP4.error) as e: - e = cstr(e) - message = e.lower().replace(" ","") - if in_receive and any(map(lambda t: t in message, ['authenticationfailed', 'loginviayourwebbrowser', #abbreviated to work with both failure and failed - 'loginfailed', 'err[auth]', 'errtemporaryerror'])): #temporary error to deal with godaddy - # if called via self.receive and it leads to authentication error, disable incoming - # and send email to system manager - self.handle_incoming_connect_error( - description=_('Authentication failed while receiving emails from Email Account {0}. Message from server: {1}').format(self.name, e) - ) + message = cstr(e).lower().replace(" ","") + auth_error_codes = [ + 'authenticationfailed', + 'loginfailed', + ] + other_error_codes = [ + 'err[auth]', + 'errtemporaryerror', + 'loginviayourwebbrowser' + ] + + all_error_codes = auth_error_codes + other_error_codes + + if in_receive and any(map(lambda t: t in message, all_error_codes)): + # if called via self.receive and it leads to authentication error, + # disable incoming and send email to System Manager + error_message = _("Authentication failed while receiving emails from Email Account: {0}.").format(self.name) + error_message += "
" + _("Message from server: {0}").format(cstr(e)) + self.handle_incoming_connect_error(description=error_message) return None + elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): + self.throw_invalid_credentials_exception() else: frappe.throw(e) @@ -195,16 +223,16 @@ class EmailAccount(Document): else: frappe.cache().set_value("workers:no-internet", True) return None - else: raise - if not in_receive: - if self.use_imap: - email_server.imap.logout() - # reset failed attempts count - self.set_failed_attempts_count(0) - return email_server + @classmethod + def throw_invalid_credentials_exception(cls): + frappe.throw( + _("Incorrect email or password. Please check your login credentials."), + exc=InvalidEmailCredentials, + title=_("Invalid Credentials") + ) def handle_incoming_connect_error(self, description): if test_internet(): diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index 08583dc228..ce39523564 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -17,6 +17,8 @@ class EmailDomain(Document): def validate(self): """Validate email id and check POP3/IMAP and SMTP connections is enabled.""" + logger = frappe.logger() + if self.email_id: validate_email_address(self.email_id, True) @@ -26,19 +28,25 @@ class EmailDomain(Document): if not frappe.local.flags.in_install and not frappe.local.flags.in_patch: try: if self.use_imap: + logger.info('Checking incoming IMAP email server {host}:{port} ssl={ssl}...'.format( + host=self.email_server, port=get_port(self), ssl=self.use_ssl)) if self.use_ssl: test = imaplib.IMAP4_SSL(self.email_server, port=get_port(self)) else: test = imaplib.IMAP4(self.email_server, port=get_port(self)) else: + logger.info('Checking incoming POP3 email server {host}:{port} ssl={ssl}...'.format( + host=self.email_server, port=get_port(self), ssl=self.use_ssl)) if self.use_ssl: test = poplib.POP3_SSL(self.email_server, port=get_port(self)) else: test = poplib.POP3(self.email_server, port=get_port(self)) - except Exception: - frappe.throw(_("Incoming email account not correct")) + except Exception as e: + logger.warn('Incoming email account "{host}" not correct'.format(host=self.email_server), exc_info=e) + frappe.throw(title=_("Incoming email account not correct"), + msg='Error connecting IMAP/POP3 "{host}": {e}'.format(host=self.email_server, e=e)) finally: try: @@ -54,22 +62,28 @@ class EmailDomain(Document): if not self.get('smtp_port'): self.smtp_port = 465 + logger.info('Checking outgoing SMTPS email server {host}:{port}...'.format( + host=self.smtp_server, port=self.smtp_port)) sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'), cint(self.smtp_port) or None) else: if self.use_tls and not self.smtp_port: self.smtp_port = 587 + logger.info('Checking outgoing SMTP email server {host}:{port} STARTTLS={tls}...'.format( + host=self.smtp_server, port=self.get('smtp_port'), tls=self.use_tls)) sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None) sess.quit() - except Exception: - frappe.throw(_("Outgoing email account not correct")) + except Exception as e: + logger.warn('Outgoing email account "{host}" not correct'.format(host=self.smtp_server), exc_info=e) + frappe.throw(title=_("Outgoing email account not correct"), + msg='Error connecting SMTP "{host}": {e}'.format(host=self.smtp_server, e=e)) def on_update(self): """update all email accounts using this domain""" for email_account in frappe.get_all("Email Account", filters={"domain": self.name}): try: email_account = frappe.get_doc("Email Account", email_account.name) - for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]: + for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder", "incoming_port"]: email_account.set(attr, self.get(attr, default=0)) email_account.save() diff --git a/frappe/email/doctype/email_domain/test_email_domain.py b/frappe/email/doctype/email_domain/test_email_domain.py index 0050b3250a..1c5306e9c2 100644 --- a/frappe/email/doctype/email_domain/test_email_domain.py +++ b/frappe/email/doctype/email_domain/test_email_domain.py @@ -5,8 +5,35 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.test_runner import make_test_objects -# test_records = frappe.get_test_records('Domain') +test_records = frappe.get_test_records('Email Domain') class TestDomain(unittest.TestCase): - pass + + def setUp(self): + make_test_objects('Email Domain', reset=True) + + def tearDown(self): + frappe.delete_doc("Email Account", "Test") + frappe.delete_doc("Email Domain", "test.com") + + def test_on_update(self): + mail_domain = frappe.get_doc("Email Domain", "test.com") + mail_account = frappe.get_doc("Email Account", "Test") + + # Initially, incoming_port is different in domain and account + self.assertNotEqual(mail_account.incoming_port, mail_domain.incoming_port) + # Trigger update of accounts using this domain + mail_domain.on_update() + mail_account = frappe.get_doc("Email Account", "Test") + # After update, incoming_port in account should match the domain + self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port) + + # Also make sure that the other attributes match + self.assertEqual(mail_account.use_imap, mail_domain.use_imap) + self.assertEqual(mail_account.use_ssl, mail_domain.use_ssl) + self.assertEqual(mail_account.use_tls, mail_domain.use_tls) + self.assertEqual(mail_account.attachment_limit, mail_domain.attachment_limit) + self.assertEqual(mail_account.smtp_server, mail_domain.smtp_server) + self.assertEqual(mail_account.smtp_port, mail_domain.smtp_port) diff --git a/frappe/email/doctype/email_domain/test_records.json b/frappe/email/doctype/email_domain/test_records.json new file mode 100644 index 0000000000..32bc66e150 --- /dev/null +++ b/frappe/email/doctype/email_domain/test_records.json @@ -0,0 +1,30 @@ +[ + { + "doctype": "Email Domain", + "domain_name": "test.com", + "email_id": "_test@test.com", + "email_server": "imap.test.com", + "use_imap": "imap.test.com", + "use_ssl": 1, + "use_tls": 1, + "incoming_port": "993", + "attachment_limit": "1", + "smtp_server": "smtp.test.com", + "smtp_port": "587" + }, + { + "doctype": "Email Account", + "name": "_Test Email Account 1", + "enable_incoming": 1, + "email_id": "_test@test.com", + "domain": "test.com", + "email_server": "imap.test.com", + "use_imap": 1, + "use_ssl": 0, + "use_tls": 1, + "incoming_port": "143", + "attachment_limit": "1", + "smtp_server": "smtp.test.com", + "smtp_port": "587" + } +] diff --git a/frappe/email/doctype/email_group/email_group.js b/frappe/email/doctype/email_group/email_group.js index 63c3832b47..404600c97d 100644 --- a/frappe/email/doctype/email_group/email_group.js +++ b/frappe/email/doctype/email_group/email_group.js @@ -3,11 +3,6 @@ frappe.ui.form.on("Email Group", "refresh", function(frm) { if(!frm.is_new()) { - frm.add_custom_button(__("View Subscribers"), function() { - frappe.route_options = {"email_group": frm.doc.name}; - frappe.set_route("List", "Email Group Member"); - }, __("View")); - frm.add_custom_button(__("Import Subscribers"), function() { frappe.prompt({fieldtype:"Select", options: frm.doc.__onload.import_types, label:__("Import Email From"), fieldname:"doctype", reqd:1}, diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json index 0d784d409a..c49de841e6 100644 --- a/frappe/email/doctype/email_group/email_group.json +++ b/frappe/email/doctype/email_group/email_group.json @@ -5,6 +5,7 @@ "creation": "2015-03-18 06:08:32.729800", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "title", "total_subscribers", @@ -41,8 +42,15 @@ "options": "Email Template" } ], - "links": [], - "modified": "2020-02-21 14:12:48.884738", + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Members", + "link_doctype": "Email Group Member", + "link_fieldname": "email_group" + } + ], + "modified": "2020-09-24 16:41:55.286377", "modified_by": "Administrator", "module": "Email", "name": "Email Group", diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 5bb654abf3..8ac071fa61 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -198,12 +198,15 @@ class EMail: def set_message_id(self, message_id, is_notification=False): if message_id: - self.msg_root["Message-Id"] = '<' + message_id + '>' + message_id = '<' + message_id + '>' else: - self.msg_root["Message-Id"] = get_message_id() - self.msg_root["isnotification"] = '' + message_id = get_message_id() + self.set_header('isnotification', '') + if is_notification: - self.msg_root["isnotification"] = '' + self.set_header('isnotification', '') + + self.set_header('Message-Id', message_id) def set_in_reply_to(self, in_reply_to): """Used to send the Message-Id of a received email back as In-Reply-To""" diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 25b6f93f83..f780aebdc1 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -584,14 +584,15 @@ def prepare_message(email, recipient, recipients_list): return safe_encode(message.as_string()) -def clear_outbox(): - """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. - Called daily via scheduler. +def clear_outbox(days=None): + """Remove low priority older than 31 days in Outbox or configured in Log Settings. Note: Used separate query to avoid deadlock """ + if not days: + days=31 email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue` - WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '31' DAY)""") + WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days)) if email_queues: frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format( @@ -602,6 +603,11 @@ def clear_outbox(): ','.join(['%s']*len(email_queues) )), tuple(email_queues)) +def set_expiry_for_email_queue(): + ''' Mark emails as expire that has not sent for 7 days. + Called daily via scheduler. + ''' + frappe.db.sql(""" UPDATE `tabEmail Queue` SET `status`='Expired' diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 9ba080bfda..cf6c13ee76 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -59,10 +59,6 @@ class EmailServer: frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.')) raise - except Exception as e: - frappe.msgprint(_('Cannot connect: {0}').format(str(e))) - raise - def connect_pop(self): #this method return pop connection try: @@ -540,6 +536,8 @@ class Email: except MaxFileSizeReachedError: # WARNING: bypass max file size exception pass + except frappe.FileAlreadyAttachedException: + pass except frappe.DuplicateEntryError: # same file attached twice?? pass diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index aa025465e5..f53b835757 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -2,7 +2,6 @@ # MIT License. See license.txt from __future__ import unicode_literals -from six import reraise as raise_ import frappe import smtplib import email.utils @@ -242,16 +241,17 @@ class SMTPServer: return self._sess + except smtplib.SMTPAuthenticationError as e: + from frappe.email.doctype.email_account.email_account import EmailAccount + EmailAccount.throw_invalid_credentials_exception() + except _socket.error as e: # Invalid mail server -- due to refusing connection - frappe.msgprint(_('Invalid Outgoing Mail Server or Port')) - traceback = sys.exc_info()[2] - raise_(frappe.ValidationError, e, traceback) - - except smtplib.SMTPAuthenticationError as e: - frappe.msgprint(_("Invalid login or password")) - traceback = sys.exc_info()[2] - raise_(frappe.ValidationError, e, traceback) + frappe.throw( + _("Invalid Outgoing Mail Server or Port"), + exc=frappe.ValidationError, + title=_("Incorrect Configuration") + ) except smtplib.SMTPException: frappe.msgprint(_('Unable to send emails at this time')) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.json b/frappe/event_streaming/doctype/event_producer/event_producer.json index 8fafdc3bb2..d868f6c123 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.json +++ b/frappe/event_streaming/doctype/event_producer/event_producer.json @@ -13,7 +13,6 @@ "api_secret", "column_break_6", "user", - "last_update", "incoming_change" ], "fields": [ @@ -25,12 +24,6 @@ "reqd": 1, "unique": 1 }, - { - "fieldname": "last_update", - "fieldtype": "Data", - "label": "Last Update", - "read_only": 1 - }, { "description": "API Key of the user(Event Subscriber) on the producer site", "fieldname": "api_key", @@ -77,7 +70,7 @@ } ], "links": [], - "modified": "2020-09-08 18:50:57.687979", + "modified": "2020-10-26 13:00:15.361316", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Producer", diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index b0ec998ab9..d458f3c24b 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -79,10 +79,24 @@ class EventProducer(Document): ) if response: response = json.loads(response) - self.last_update = response['last_update'] + self.set_last_update(response['last_update']) else: frappe.throw(_('Failed to create an Event Consumer or an Event Consumer for the current site is already registered.')) + def set_last_update(self, last_update): + last_update_doc_name = frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name)) + if not last_update_doc_name: + frappe.get_doc(dict( + doctype = 'Event Producer Last Update', + event_producer = self.producer_url, + last_update = last_update + )).insert(ignore_permissions=True) + else: + frappe.db.set_value('Event Producer Last Update', last_update_doc_name, 'last_update', last_update) + + def get_last_update(self): + return frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name), 'last_update') + def get_request_data(self): consumer_doctypes = [] for entry in self.producer_doctypes: @@ -184,7 +198,7 @@ def pull_from_node(event_producer): """pull all updates after the last update timestamp from event producer site""" event_producer = frappe.get_doc('Event Producer', event_producer) producer_site = get_producer_site(event_producer.producer_url) - last_update = event_producer.last_update + last_update = event_producer.get_last_update() (doctypes, mapping_config, naming_config) = get_config(event_producer.producer_doctypes) @@ -239,7 +253,7 @@ def sync(update, producer_site, event_producer, in_retry=False): return 'Failed' log_event_sync(update, event_producer.name, 'Failed', frappe.get_traceback()) - event_producer.db_set('last_update', update.creation) + event_producer.set_last_update(update.creation) frappe.db.commit() diff --git a/frappe/custom/doctype/package_publish_target/__init__.py b/frappe/event_streaming/doctype/event_producer_last_update/__init__.py similarity index 100% rename from frappe/custom/doctype/package_publish_target/__init__.py rename to frappe/event_streaming/doctype/event_producer_last_update/__init__.py diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js new file mode 100644 index 0000000000..15730e4c5f --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Event Producer Last Update', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json similarity index 53% rename from frappe/custom/doctype/custom_link/custom_link.json rename to frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json index 350e6b1c2d..27f8ed2f81 100644 --- a/frappe/custom/doctype/custom_link/custom_link.json +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json @@ -1,36 +1,36 @@ { "actions": [], - "autoname": "field:document_type", - "creation": "2020-04-08 15:16:44.342509", + "autoname": "field:event_producer", + "creation": "2020-10-26 12:53:11.940177", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "document_type", - "links" + "event_producer", + "last_update" ], "fields": [ { - "fieldname": "document_type", - "fieldtype": "Link", + "fieldname": "event_producer", + "fieldtype": "Data", "in_list_view": 1, - "label": "Document Type", - "options": "DocType", + "label": "Event Producer", "reqd": 1, "unique": 1 }, { - "fieldname": "links", - "fieldtype": "Table", - "label": "Links", - "options": "DocType Link" + "fieldname": "last_update", + "fieldtype": "Data", + "label": "Last Update" } ], + "in_create": 1, + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-04-08 16:42:59.402671", + "modified": "2020-10-26 13:22:27.056599", "modified_by": "Administrator", - "module": "Custom", - "name": "Custom Link", + "module": "Event Streaming", + "name": "Event Producer Last Update", "owner": "Administrator", "permissions": [ { @@ -46,6 +46,7 @@ "write": 1 } ], + "read_only": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py similarity index 85% rename from frappe/custom/doctype/package_document_type/package_document_type.py rename to frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py index 6e166eecbd..02e297bdd5 100644 --- a/frappe/custom/doctype/package_document_type/package_document_type.py +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class PackageDocumentType(Document): +class EventProducerLastUpdate(Document): pass diff --git a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py new file mode 100644 index 0000000000..0311cb2df9 --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEventProducerLastUpdate(unittest.TestCase): + pass diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 88428b875c..60c17f6d5c 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -76,6 +76,7 @@ class UnknownDomainError(Exception): pass class MappingMismatchError(ValidationError): pass class InvalidStatusError(ValidationError): pass class MandatoryError(ValidationError): pass +class NonNegativeError(ValidationError): pass class InvalidSignatureError(ValidationError): pass class RateLimitExceededError(ValidationError): pass class CannotChangeConstantError(ValidationError): pass diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 6a56107333..e4c4e278b0 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -558,6 +558,8 @@ "code": "cn", "currency": "CNY", "currency_name": "Yuan Renminbi", + "currency_fraction_units": 100, + "smallest_currency_fraction_value": 0.01, "date_format": "yyyy-mm-dd", "number_format": "#,###.##", "timezones": [ @@ -1389,7 +1391,10 @@ "code": "la", "currency": "LAK", "currency_name": "Kip", - "number_format": "#,###.##" + "number_format": "#,###.##", + "timezones":[ + "Asia/Vientiane" + ] }, "Latvia": { "code": "lv", diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json index bb9abb7ce8..db3fa5a19f 100644 --- a/frappe/geo/doctype/currency/currency.json +++ b/frappe/geo/doctype/currency/currency.json @@ -1,345 +1,113 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:currency_name", - "beta": 0, "creation": "2013-01-28 10:06:02", - "custom": 0, "description": "**Currency** Master", - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, + "engine": "InnoDB", + "field_order": [ + "currency_name", + "enabled", + "fraction", + "fraction_units", + "smallest_currency_fraction_value", + "symbol", + "number_format" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "currency_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Currency Name", - "length": 0, - "no_copy": 0, "oldfieldname": "currency_name", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Sub-currency. For e.g. \"Cent\"", "fieldname": "fraction", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Fraction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fraction" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent", "fieldname": "fraction_units", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Fraction Units", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fraction Units" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01", "fieldname": "smallest_currency_fraction_value", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Smallest Currency Fraction Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "non_negative": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "A symbol for this currency. For e.g. $", "fieldname": "symbol", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Symbol", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Symbol" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "How should this currency be formatted? If not set, will use system defaults", "fieldname": "number_format", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Number Format", - "length": 0, - "no_copy": 0, - "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-bitcoin", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-29 06:37:19.908254", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-29 06:33:12.879978", "modified_by": "Administrator", "module": "Geo", "name": "Currency", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Accounts User" }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Sales User" }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Purchase User" } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index dd0c22f252..d8c8cd841c 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -140,9 +140,9 @@ doc_events = { "frappe.core.doctype.activity_log.feed.update_feed", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", "frappe.automation.doctype.assignment_rule.assignment_rule.apply", - "frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone", "frappe.core.doctype.file.file.attach_files_to_document", "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", + "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ @@ -155,7 +155,8 @@ doc_events = { "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" ], "on_change": [ - "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" + "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points", + "frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone" ] }, "Event": { @@ -205,7 +206,7 @@ scheduler_events = { "frappe.utils.password.delete_password_reset_cache" ], "daily": [ - "frappe.email.queue.clear_outbox", + "frappe.email.queue.set_expiry_for_email_queue", "frappe.desk.notifications.clear_notifications", "frappe.core.doctype.error_log.error_log.set_old_logs_as_seen", "frappe.desk.doctype.event.event.send_event_digest", @@ -214,7 +215,6 @@ scheduler_events = { "frappe.realtime.remove_old_task_logs", "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", - "frappe.core.doctype.activity_log.activity_log.clear_authentication_logs", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record", "frappe.desk.form.document_follow.send_daily_updates", "frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", @@ -222,7 +222,8 @@ scheduler_events = { "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry", "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", - "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports" + "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", + "frappe.core.doctype.log_settings.log_settings.run_log_clean_up" ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", diff --git a/frappe/installer.py b/frappe/installer.py index c6549f16ee..df767a3294 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -3,7 +3,7 @@ import json import os - +from frappe.defaults import _clear_cache import frappe @@ -111,8 +111,8 @@ def remove_from_installed_apps(app_name): installed_apps = frappe.get_installed_apps() if app_name in installed_apps: installed_apps.remove(app_name) - frappe.db.set_global("installed_apps", json.dumps(installed_apps)) - frappe.get_single("Installed Applications").update_versions() + frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps)) + _clear_cache("__global") frappe.db.commit() if frappe.flags.in_install: post_install() @@ -175,7 +175,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) for doctype in set(drop_doctypes): print("* dropping Table for '{0}'...".format(doctype)) - frappe.db.sql("drop table `tab{0}`".format(doctype)) + frappe.db.sql_ddl("drop table `tab{0}`".format(doctype)) frappe.db.commit() click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green") diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py index 2cb656459b..f1d59beb5a 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.py +++ b/frappe/integrations/doctype/integration_request/integration_request.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document import json +from six import string_types +from frappe.integrations.utils import json_handler class IntegrationRequest(Document): def autoname(self): @@ -20,3 +22,17 @@ class IntegrationRequest(Document): self.status = status self.save(ignore_permissions=True) frappe.db.commit() + + def handle_success(self, response): + """update the output field with the response along with the relevant status""" + if isinstance(response, string_types): + response = json.loads(response) + self.db_set("status", "Completed") + self.db_set("output", json.dumps(response, default=json_handler)) + + def handle_failure(self, response): + """update the error field with the response along with the relevant status""" + if isinstance(response, string_types): + response = json.loads(response) + self.db_set("status", "Failed") + self.db_set("error", json.dumps(response, default=json_handler)) \ No newline at end of file diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 808affe47a..1af9682073 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -49,16 +49,20 @@ def make_post_request(url, auth=None, headers=None, data=None): frappe.log_error() raise exc -def create_request_log(data, integration_type, service_name, name=None): +def create_request_log(data, integration_type, service_name, name=None, error=None): if isinstance(data, string_types): data = json.loads(data) + if isinstance(error, string_types): + error = json.loads(error) + integration_request = frappe.get_doc({ "doctype": "Integration Request", "integration_type": integration_type, "integration_request_service": service_name, "reference_doctype": data.get("reference_doctype"), "reference_docname": data.get("reference_docname"), + "error": json.dumps(error, default=json_handler), "data": json.dumps(data, default=json_handler) }) diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index fcf648e718..e0087a9e40 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -10,7 +10,7 @@ import copy import frappe import frappe.defaults from frappe.model import data_fieldtypes -from frappe.utils import nowdate, nowtime, now_datetime +from frappe.utils import nowdate, nowtime, now_datetime, cstr from frappe.core.doctype.user_permission.user_permission import get_user_permissions from frappe.permissions import filter_allowed_docs_for_doctype @@ -99,7 +99,7 @@ def get_static_default_value(df, doctype_user_permissions, allowed_records): elif df.default == "Today": return nowdate() - elif not df.default.startswith(":"): + elif not cstr(df.default).startswith(":"): # a simple default value is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions) or (df.default in allowed_records)) @@ -116,7 +116,7 @@ def set_dynamic_default_values(doc, parent_doc, parentfield): for df in frappe.get_meta(doc["doctype"]).get("fields"): if df.get("default"): - if df.default.startswith(":"): + if cstr(df.default).startswith(":"): default_value = get_default_based_on_another_field(df, user_permissions, parent_doc) if default_value is not None and not doc.get(df.fieldname): doc[df.fieldname] = default_value diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 4e40dc555d..ace9b04cec 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -38,7 +38,7 @@ class DatabaseQuery(object): join='left join', distinct=False, start=None, page_length=None, limit=None, ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, update=None, add_total_row=None, user_settings=None, reference_doctype=None, - return_query=False, strict=True, pluck=None): + return_query=False, strict=True, pluck=None, ignore_ddl=False): if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) raise frappe.PermissionError(self.doctype) @@ -86,6 +86,7 @@ class DatabaseQuery(object): self.user_settings_fields = copy.deepcopy(self.fields) self.return_query = return_query self.strict = strict + self.ignore_ddl = ignore_ddl # for contextual user permission check # to determine which user permission is applicable on link field of specific doctype @@ -94,6 +95,11 @@ class DatabaseQuery(object): if user_settings: self.user_settings = json.loads(user_settings) + self.columns = self.get_table_columns() + + # no table & ignore_ddl, return + if not self.columns: return [] + if query: result = self.run_custom_query(query) else: @@ -134,7 +140,8 @@ class DatabaseQuery(object): if self.return_query: return query else: - return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, update=self.update) + return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, + update=self.update, ignore_ddl=self.ignore_ddl) def prepare_args(self): self.parse_args() @@ -171,8 +178,16 @@ class DatabaseQuery(object): fields = [] + # Wrapping fields with grave quotes to allow support for sql keywords + # TODO: Add support for wrapping fields with sql functions and distinct keyword for field in self.fields: - if (field.strip().startswith(("`", "*")) or "(" in field): + stripped_field = field.strip().lower() + skip_wrapping = any([ + stripped_field.startswith(("`", "*", '"', "'")), + "(" in stripped_field, + "distinct" in stripped_field, + ]) + if skip_wrapping: fields.append(field) elif "as" in field.lower().split(" "): col, _, new = field.split() @@ -315,15 +330,22 @@ class DatabaseQuery(object): if '.' not in field and not _in_standard_sql_methods(field): self.fields[idx] = '{0}.{1}'.format(self.tables[0], field) + def get_table_columns(self): + try: + return get_table_columns(self.doctype) + except frappe.db.TableMissingError: + if self.ignore_ddl: + return None + else: + raise + def set_optional_columns(self): """Removes optional columns like `_user_tags`, `_comments` etc. if not in table""" - columns = get_table_columns(self.doctype) - # remove from fields to_remove = [] for fld in self.fields: for f in optional_fields: - if f in fld and not f in columns: + if f in fld and not f in self.columns: to_remove.append(fld) for fld in to_remove: @@ -336,7 +358,7 @@ class DatabaseQuery(object): each = [each] for element in each: - if element in optional_fields and element not in columns: + if element in optional_fields and element not in self.columns: to_remove.append(each) for each in to_remove: diff --git a/frappe/model/document.py b/frappe/model/document.py index 53fcd99f78..3789e20b19 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -493,6 +493,7 @@ class Document(BaseDocument): self._validate_mandatory() self._validate_data_fields() self._validate_selects() + self._validate_non_negative() self._validate_length() self._extract_images_from_text_editor() self._sanitize_content() @@ -503,6 +504,7 @@ class Document(BaseDocument): for d in children: d._validate_data_fields() d._validate_selects() + d._validate_non_negative() d._validate_length() d._extract_images_from_text_editor() d._sanitize_content() @@ -514,6 +516,21 @@ class Document(BaseDocument): else: self.validate_set_only_once() + def _validate_non_negative(self): + def get_msg(df): + if self.parentfield: + return "{} {} #{}: {} {}".format(frappe.bold(_(self.doctype)), + _("Row"), self.idx, _("Value cannot be negative for"), frappe.bold(_(df.label))) + else: + return _("Value cannot be negative for {0}: {1}").format(_(df.parent), frappe.bold(_(df.label))) + + for df in self.meta.get('fields', {'non_negative': ('=', 1), + 'fieldtype': ('in', ['Int', 'Float', 'Currency'])}): + + if flt(self.get(df.fieldname)) < 0: + msg = get_msg(df) + frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value")) + def validate_workflow(self): """Validate if the workflow transition is valid""" if frappe.flags.in_install == 'frappe': return diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index e5ce9102e2..7404ba407e 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -42,9 +42,12 @@ def get_dynamic_link_map(for_delete=False): # always check in Single DocTypes dynamic_link_map.setdefault(meta.name, []).append(df) else: - links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df)) - for doctype in links: - dynamic_link_map.setdefault(doctype, []).append(df) + try: + links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df)) + for doctype in links: + dynamic_link_map.setdefault(doctype, []).append(df) + except frappe.db.TableMissingError: # noqa: E722 + pass frappe.local.dynamic_link_map = dynamic_link_map return frappe.local.dynamic_link_map diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 1cc3abba5b..8c17a5b19b 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -19,7 +19,7 @@ from __future__ import unicode_literals, print_function from datetime import datetime from six.moves import range import frappe, json, os -from frappe.utils import cstr, cint +from frappe.utils import cstr, cint, cast_fieldtype from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields from frappe.model.document import Document from frappe.model.base_document import BaseDocument @@ -103,6 +103,7 @@ class Meta(Document): self.sort_fields() self.get_valid_columns() self.set_custom_permissions() + self.add_custom_links_and_actions() def as_dict(self, no_nulls = False): def serialize(doc): @@ -305,6 +306,11 @@ class Meta(Document): self.extend("fields", custom_fields) def apply_property_setters(self): + """ + Property Setters are set via Customize Form. They override standard properties + of the doctype or its child properties like fields, links etc. This method + applies the customized properties over the standard meta object + """ if not frappe.db.table_exists('Property Setter'): return @@ -313,26 +319,52 @@ class Meta(Document): if not property_setters: return - integer_docfield_properties = [d.fieldname for d in frappe.get_meta('DocField').fields - if d.fieldtype in ('Int', 'Check')] - for ps in property_setters: if ps.doctype_or_field=='DocType': - if ps.property_type in ('Int', 'Check'): - ps.value = cint(ps.value) + self.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) - self.set(ps.property, ps.value) - else: - docfield = self.get("fields", {"fieldname":ps.field_name}, limit=1) - if docfield: - docfield = docfield[0] - else: - continue + elif ps.doctype_or_field=='DocField': + for d in self.fields: + if d.fieldname == ps.field_name: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break - if ps.property in integer_docfield_properties: - ps.value = cint(ps.value) + elif ps.doctype_or_field=='DocType Link': + for d in self.links: + if d.name == ps.row_name: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break - docfield.set(ps.property, ps.value) + elif ps.doctype_or_field=='DocType Action': + for d in self.actions: + if d.name == ps.row_name: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break + + def add_custom_links_and_actions(self): + for doctype, fieldname in (('DocType Link', 'links'), ('DocType Action', 'actions')): + # ignore_ddl because the `custom` column was added later via a patch + for d in frappe.get_all(doctype, fields='*', filters=dict(parent=self.name, custom=1), ignore_ddl=True): + self.append(fieldname, d) + + # set the fields in order if specified + # order is saved as `links_order` + order = json.loads(self.get('{}_order'.format(fieldname)) or '[]') + if order: + name_map = {d.name:d for d in self.get(fieldname)} + new_list = [] + for name in order: + if name in name_map: + new_list.append(name_map[name]) + + # add the missing items that have not be added + # maybe these items were added to the standard product + # after the customization was done + for d in self.get(fieldname): + if d not in new_list: + new_list.append(d) + + self.set(fieldname, new_list) def sort_fields(self): """sort on basis of insert_after""" @@ -448,9 +480,6 @@ class Meta(Document): if hasattr(self, 'links') and self.links: dashboard_links.extend(self.links) - if frappe.get_all("Custom Link", {"document_type": self.name}): - dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links) - if not data.transactions: # init groups data.transactions = [] @@ -458,6 +487,9 @@ class Meta(Document): for link in dashboard_links: link.added = False + if link.hidden: + continue + for group in data.transactions: group = frappe._dict(group) # group found diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 9ea5fc0ca4..c2e074990e 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -93,15 +93,12 @@ def set_naming_from_document_naming_rule(doc): if doc.doctype in log_types: return - try: - for d in frappe.get_all('Document Naming Rule', - dict(document_type=doc.doctype, disabled=0), order_by='priority desc'): - frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc) - if doc.name: - break - except frappe.db.TableMissingError: # noqa: E722 - # not yet bootstrapped - pass + # ignore_ddl if naming is not yet bootstrapped + for d in frappe.get_all('Document Naming Rule', + dict(document_type=doc.doctype, disabled=0), order_by='priority desc', ignore_ddl=True): + frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc) + if doc.name: + break def set_name_by_naming_series(doc): """Sets name by the `naming_series` property""" diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index bba2f62856..b3debfc43c 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -90,10 +90,11 @@ def sync_customizations(app=None): folder = frappe.get_app_path(app_name, module_name, 'custom') if os.path.exists(folder): for fname in os.listdir(folder): - with open(os.path.join(folder, fname), 'r') as f: - data = json.loads(f.read()) - if data.get('sync_on_migrate'): - sync_customizations_for_doctype(data, folder) + if fname.endswith('.json'): + with open(os.path.join(folder, fname), 'r') as f: + data = json.loads(f.read()) + if data.get('sync_on_migrate'): + sync_customizations_for_doctype(data, folder) def sync_customizations_for_doctype(data, folder): diff --git a/frappe/patches.txt b/frappe/patches.txt index b701623b1c..0daf29e001 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -7,7 +7,7 @@ frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 -execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23 +execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17 execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 frappe.patches.v11_0.drop_column_apply_user_permissions @@ -207,7 +207,7 @@ frappe.patches.v9_1.resave_domain_settings frappe.patches.v9_1.revert_domain_settings frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) -frappe.patches.v10_0.reload_countries_and_currencies # 20-10-2020 +frappe.patches.v10_0.reload_countries_and_currencies # 14-10-2020 frappe.patches.v10_0.refactor_social_login_keys frappe.patches.v10_0.enable_chat_by_default_within_system_settings frappe.patches.v10_0.remove_custom_field_for_disabled_domain @@ -313,3 +313,6 @@ frappe.patches.v13_0.update_newsletter_content_type execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'}) frappe.patches.v13_0.delete_event_producer_and_consumer_keys frappe.patches.v13_0.web_template_set_module #2020-10-05 +frappe.patches.v13_0.remove_custom_link +execute:frappe.delete_doc("DocType", "Footer Item") +frappe.patches.v13_0.replace_field_target_with_open_in_new_tab diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py index 4315f06ebe..bcb47bec24 100644 --- a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py +++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py @@ -6,6 +6,7 @@ import frappe def execute(): + frappe.reload_doc("website", "doctype", "website_theme_ignore_app") themes = frappe.db.get_all( "Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")} ) diff --git a/frappe/patches/v13_0/remove_custom_link.py b/frappe/patches/v13_0/remove_custom_link.py new file mode 100644 index 0000000000..f38bb642f0 --- /dev/null +++ b/frappe/patches/v13_0/remove_custom_link.py @@ -0,0 +1,15 @@ +import frappe + +def execute(): + ''' + Remove the doctype "Custom Link" that was used to add Custom Links to the + Dashboard since this is now managed by Customize Form. + Update `parent` property to the DocType and delte the doctype + ''' + frappe.reload_doctype('DocType Link') + if frappe.db.has_table('Custom Link'): + for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']): + frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s', + (custom_link.document_type, custom_link.name)) + + frappe.delete_doc('DocType', 'Custom Link') \ No newline at end of file diff --git a/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py new file mode 100644 index 0000000000..21b2d8ef03 --- /dev/null +++ b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + doctype = "Top Bar Item" + if not frappe.db.table_exists(doctype) \ + or not frappe.db.has_column(doctype, "target"): + return + + frappe.reload_doc("website", "doctype", "top_bar_item") + frappe.db.set_value(doctype, {"target": 'target = "_blank"'}, 'open_in_new_tab', 1) diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py index 1b083dc5b8..df008557d8 100644 --- a/frappe/patches/v13_0/web_template_set_module.py +++ b/frappe/patches/v13_0/web_template_set_module.py @@ -6,8 +6,9 @@ import frappe def execute(): """Set default module for standard Web Template, if none.""" - frappe.reload_doctype('Web Template') - frappe.reload_doctype('Web Template Field') + frappe.reload_doc('website', 'doctype', 'Web Template Field') + frappe.reload_doc('website', 'doctype', 'web_template') + standard_templates = frappe.get_list('Web Template', {'standard': 1}) for template in standard_templates: doc = frappe.get_doc('Web Template', template.name) diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 74ee56cb54..63448ccc39 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -8,10 +8,11 @@ "field_order": [ "doc_type", "module", - "disabled", + "default_print_language", "column_break_3", "standard", "custom_format", + "disabled", "section_break_6", "print_format_type", "raw_printing", @@ -22,7 +23,6 @@ "show_section_headings", "line_breaks", "column_break_11", - "default_print_language", "font", "css_section", "css", @@ -202,7 +202,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-29 11:44:59.082797", + "modified": "2020-10-27 18:27:58.307070", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css index 7f922ffd1d..1cf8bb011a 100644 --- a/frappe/public/css/mobile.css +++ b/frappe/public/css/mobile.css @@ -10,6 +10,7 @@ body { html, body { overflow-x: hidden; + overflow-y: overlay; } @media (max-width: 991px) { .intro-area, diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 97d5239a72..c8ed29fb76 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -139,6 +139,26 @@ frappe.Application = Class.extend({ } }); }, 300000); // check every 5 minutes + + if(frappe.user.has_role("System Manager")){ + setInterval(function() { + frappe.call({ + method: 'frappe.core.doctype.log_settings.log_settings.has_unseen_error_log', + args: { + user: frappe.session.user + }, + callback: function(r) { + console.log(r); + if(r.message.show_alert){ + frappe.show_alert({ + indicator: 'red', + message: r.message.message + }); + } + } + }); + }, 600000); // check every 10 minutes + } } this.fetch_tags(); @@ -179,6 +199,7 @@ frappe.Application = Class.extend({ 'reqd': 1 }, { + "fieldname": "submit", "fieldtype": "Button", "label": __("Submit") } diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index f3f04ec4d8..2f051a4701 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -133,18 +133,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ me.parse_validate_and_set_in_model(me.get_input_value(), e); }); }, - bind_focusout: function() { - // on touchscreen devices, scroll to top - // so that static navbar and page head don't overlap the input - if (frappe.dom.is_touchscreen()) { - var me = this; - this.$input && this.$input.on("focusout", function() { - if (frappe.dom.is_touchscreen()) { - frappe.utils.scroll_to(me.$wrapper); - } - }); - } - }, set_label: function(label) { if(label) this.df.label = label; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index bbf9a89072..4db2553bd1 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -21,7 +21,6 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.input = this.$input.get(0); this.has_input = true; this.bind_change_event(); - this.bind_focusout(); this.setup_autoname_check(); if (this.df.options == 'Phone') { this.setup_phone(); diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 02e970091e..00bb02a5fc 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -1,14 +1,12 @@ frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ get_options: function() { let options = ''; - if(this.df.get_options) { + if (this.df.get_options) { options = this.df.get_options(); - } - else if (this.docname==null && cur_dialog) { + } else if (this.docname==null && cur_dialog) { //for dialog box options = cur_dialog.get_value(this.df.options); - } - else if (!cur_frm) { + } else if (!cur_frm) { const selector = `input[data-fieldname="${this.df.options}"]`; let input = null; if (cur_list) { @@ -21,13 +19,12 @@ frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ if (input) { options = input.val(); } - } - else { + } else { options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); } if (frappe.model.is_single(options)) { - frappe.throw(__(`${options.bold()} is not a valid DocType for Dynamic Link`)); + frappe.throw(__("{0} is not a valid DocType for Dynamic Link", [options.bold()])); } return options; diff --git a/frappe/public/js/frappe/form/controls/int.js b/frappe/public/js/frappe/form/controls/int.js index 91dee45838..9b59444fd3 100644 --- a/frappe/public/js/frappe/form/controls/int.js +++ b/frappe/public/js/frappe/form/controls/int.js @@ -1,17 +1,17 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ - make: function() { + make: function () { this._super(); // $(this.label_area).addClass('pull-right'); // $(this.disp_area).addClass('text-right'); }, - make_input: function() { + make_input: function () { var me = this; this._super(); this.$input // .addClass("text-right") - .on("focus", function() { - setTimeout(function() { - if(!document.activeElement) return; + .on("focus", function () { + setTimeout(function () { + if (!document.activeElement) return; document.activeElement.value = me.validate(document.activeElement.value); document.activeElement.select(); @@ -19,7 +19,10 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ return false; }); }, - eval_expression: function(value) { + validate: function (value) { + return this.parse(value); + }, + eval_expression: function (value) { if (typeof value === 'string') { if (value.match(/^[0-9+\-/* ]+$/)) { // If it is a string containing operators @@ -33,7 +36,7 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ } return value; }, - parse: function(value) { + parse: function (value) { return cint(this.eval_expression(value), null); } }); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 9e6d3f0bdb..bb9e8c22d1 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -261,7 +261,7 @@ frappe.ui.form.Form = class FrappeForm { cur_frm = this; if(this.docname) { // document to show - + this.save_disabled = false; // set the doc this.doc = frappe.get_doc(this.doctype, this.docname); @@ -1265,17 +1265,17 @@ frappe.ui.form.Form = class FrappeForm { set_df_property(fieldname, property, value, docname, table_field) { var df; - if (!docname && !table_field) { + if (!docname || !table_field) { df = this.get_docfield(fieldname); } else { - var grid = this.fields_dict[table_field].grid, - fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': fieldname}); + var grid = this.fields_dict[fieldname].grid, + fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field}); if (fname && fname.length) - df = frappe.meta.get_docfield(fname[0].parent, fieldname, docname); + df = frappe.meta.get_docfield(fname[0].parent, table_field, docname); } if (df && df[property] != value) { df[property] = value; - refresh_field(fieldname, table_field); + this.refresh_field(fieldname); } } @@ -1517,11 +1517,12 @@ frappe.ui.form.Form = class FrappeForm { const escaped_name = encodeURIComponent(value); - return repl('%(label)s', { + return repl('%(label)s', { color: get_color(doc || {}), doctype: df.options, - name: escaped_name, - label: label + escaped_name: escaped_name, + label: label, + name: value }); } else { return ''; @@ -1658,6 +1659,21 @@ frappe.ui.form.Form = class FrappeForm { frappe.route.on('change', () => driver.reset()); driver.start(); } + + // Filters fields from the reference doctype and sets them as options for a Select field + set_fields_as_options(fieldname, reference_doctype, filter_function, default_options=[], table_fieldname) { + if (!reference_doctype) return; + let options = default_options; + return new Promise(resolve => { + frappe.model.with_doctype(reference_doctype, () => { + frappe.get_meta(reference_doctype).fields.map(df => { + filter_function(df) && options.push({ label: df.label, value: df.fieldname }); + }); + options && this.set_df_property(fieldname, 'options', options, this.doc.name, table_fieldname); + resolve(options); + }); + }); + } }; frappe.validated = 0; diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 39541757a5..3f422d0a9b 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -106,7 +106,7 @@ frappe.form.formatters = { if(frappe.form.link_formatters[doctype]) { // don't apply formatters in case of composite (parent field of same type) if (doc && doctype !== doc.doctype) { - value = frappe.form.link_formatters[doctype](value, doc); + value = frappe.form.link_formatters[doctype](value, doc, docfield); } } @@ -305,7 +305,7 @@ frappe.format = function(value, df, options, doc) { formatted = frappe.dom.remove_script_and_style(formatted); return formatted; -} +}; frappe.get_format_helper = function(doc) { var helper = { @@ -317,4 +317,9 @@ frappe.get_format_helper = function(doc) { }; $.extend(helper, doc); return helper; -} +}; + +frappe.form.link_formatters['User'] = function(value, doc, docfield) { + let full_name = doc && (doc.full_name || (docfield && doc[`${docfield.fieldname}_full_name`])); + return full_name || value; +}; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 401dac4a8a..9c916ccc4a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -51,10 +51,12 @@ export default class Grid { make() { - let template = `
+ let template = `
+
+
@@ -112,6 +114,7 @@ export default class Grid { this.custom_buttons = {}; this.grid_buttons = this.wrapper.find('.grid-buttons'); + this.grid_custom_buttons = this.wrapper.find('.grid-custom-buttons'); this.remove_rows_button = this.grid_buttons.find('.grid-remove-rows'); this.remove_all_rows_button = this.grid_buttons.find('.grid-remove-all-rows'); @@ -763,6 +766,13 @@ export default class Grid { // download this.setup_download(); + const value_formatter_map = { + "Date": val => val ? frappe.datetime.user_to_str(val) : val, + "Int": val => cint(val), + "Check": val => cint(val), + "Float": val => flt(val), + }; + // upload frappe.flags.no_socketio = true; $(this.wrapper).find(".grid-upload").removeClass('hidden').on("click", () => { @@ -790,16 +800,9 @@ export default class Grid { var fieldname = fieldnames[ci]; var df = frappe.meta.get_docfield(me.df.options, fieldname); - // convert date formatting - if (df.fieldtype==="Date" && value) { - value = frappe.datetime.user_to_str(value); - } - - if (df.fieldtype==="Int" || df.fieldtype==="Check") { - value = cint(value); - } - - d[fieldnames[ci]] = value; + d[fieldnames[ci]] = value_formatter_map[df.fieldtype] + ? value_formatter_map[df.fieldtype](value) + : value; }); } } @@ -861,18 +864,19 @@ export default class Grid { }); } - add_custom_button(label, click) { + add_custom_button(label, click, position='bottom') { // add / unhide a custom button - var btn = this.custom_buttons[label]; - if (!btn) { - btn = $('') - .css('margin-right', '4px') - .prependTo(this.grid_buttons) + const $wrapper = position === 'top' ? this.grid_custom_buttons : this.grid_buttons; + let $btn = this.custom_buttons[label]; + if (!$btn) { + $btn = $(``) + .prependTo($wrapper) .on('click', click); - this.custom_buttons[label] = btn; + this.custom_buttons[label] = $btn; } else { - btn.removeClass('hidden'); + $btn.removeClass('hidden'); } + return $btn; } clear_custom_buttons() { diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 1348203bc4..6ea21e6e63 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -113,7 +113,7 @@ frappe.ui.form.Layout = Class.extend({ label: __('Dashboard'), cssClass: 'form-dashboard', collapsible: 1, - //hidden: 1 + hidden: 1 }); }, @@ -498,6 +498,7 @@ frappe.ui.form.Layout = Class.extend({ }, set_dependant_property: function(condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); + let value = set_property ? 1 : 0; let form_obj; if (this.frm) { @@ -506,10 +507,10 @@ frappe.ui.form.Layout = Class.extend({ form_obj = this; } if (form_obj) { - if (set_property) { - form_obj.set_df_property(fieldname, property, 1); + if (this.doc && this.doc.parent) { + form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); } else { - form_obj.set_df_property(fieldname, property, 0); + form_obj.set_df_property(fieldname, property, value); } } }, diff --git a/frappe/public/js/frappe/form/sidebar/review.js b/frappe/public/js/frappe/form/sidebar/review.js index 2cf2980bf7..b321e3d3d5 100644 --- a/frappe/public/js/frappe/form/sidebar/review.js +++ b/frappe/public/js/frappe/form/sidebar/review.js @@ -78,7 +78,6 @@ frappe.ui.form.Review = class Review { } show_review_dialog() { const user_options = this.get_involved_users(); - const doc_owner = this.frm.doc.owner; const review_dialog = new frappe.ui.Dialog({ 'title': __('Add Review'), 'fields': [{ @@ -106,7 +105,7 @@ frappe.ui.form.Review = class Review { fieldtype: 'Int', label: __('Points'), reqd: 1, - description: __(`Currently you have ${this.points.review_points} review points`) + description: __("Currently you have {0} review points", [this.points.review_points]) }, { fieldtype: 'Small Text', fieldname: 'reason', @@ -181,7 +180,7 @@ frappe.ui.form.Review = class Review { trigger: 'hover', delay: 500, placement: 'top', - template:` + template: `
diff --git a/frappe/public/js/frappe/form/success_action.js b/frappe/public/js/frappe/form/success_action.js index 9d81be20e7..312d83ef0f 100644 --- a/frappe/public/js/frappe/form/success_action.js +++ b/frappe/public/js/frappe/form/success_action.js @@ -28,8 +28,9 @@ frappe.ui.form.SuccessAction = class SuccessAction { } show_alert() { - frappe.db.count(this.form.doctype) - .then(count => { + frappe.db.get_list(this.form.doctype, {limit: 2}) + .then(result => { + const count = result.length; const setting = this.setting; let message = count === 1 ? setting.first_success_message : diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index bbe2fa2f95..bdc7dc0827 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -308,7 +308,6 @@ frappe.views.BaseList = class BaseList { this.$paging_area.on('click', '.btn-paging, .btn-more', e => { const $this = $(e.currentTarget); - if ($this.is('.btn-paging')) { // set active button this.$paging_area.find('.btn-paging').removeClass('btn-info'); @@ -316,11 +315,10 @@ frappe.views.BaseList = class BaseList { this.start = 0; this.page_length = $this.data().value; - this.refresh(); } else if ($this.is('.btn-more')) { this.start = this.start + this.page_length; - this.refresh(); } + this.refresh(); }); } diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 4a6ac56294..2a25e64bf3 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -13,7 +13,6 @@ frappe.views.ListSidebar = class ListSidebar { constructor(opts) { $.extend(this, opts); this.make(); - this.cat_tags = []; } make() { @@ -242,10 +241,6 @@ frappe.views.ListSidebar = class ListSidebar { }); } - get_cat_tags() { - return this.cat_tags; - } - get_stats() { var me = this; frappe.call({ @@ -326,33 +321,6 @@ frappe.views.ListSidebar = class ListSidebar { .appendTo(this.sidebar.find(".list-stats-dropdown")); } - set_fieldtype(df) { - - // scrub - if (df.fieldname == "docstatus") { - df.fieldtype = "Select", - df.options = [ - { value: 0, label: "Draft" }, - { value: 1, label: "Submitted" }, - { value: 2, label: "Cancelled" }, - ]; - } else if (df.fieldtype == 'Check') { - df.fieldtype = 'Select'; - df.options = [{ value: 0, label: 'No' }, - { value: 1, label: 'Yes' } - ]; - } else if (['Text', 'Small Text', 'Text Editor', 'Code', 'Tag', 'Comments', - 'Dynamic Link', 'Read Only', 'Assign' - ].indexOf(df.fieldtype) != -1) { - df.fieldtype = 'Data'; - } else if (df.fieldtype == 'Link' && this.$w.find('.condition').val() != "=") { - df.fieldtype = 'Data'; - } - if (df.fieldtype === "Data" && (df.options || "").toLowerCase() === "email") { - df.options = null; - } - } - reload_stats() { this.sidebar.find(".stat-link").remove(); this.sidebar.find(".stat-no-records").remove(); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 4d8121ebd6..5275b21cda 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -33,7 +33,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (!this.has_permissions()) { frappe.set_route(''); - frappe.msgprint(__(`Not permitted to view ${this.doctype}`)); + frappe.msgprint(__("Not permitted to view {0}", [this.doctype])); return; } @@ -88,6 +88,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { }); } + if (this.view_name == 'List') this.toggle_paging = true; + this.patch_refresh_and_load_lib(); return this.get_list_view_settings(); } @@ -457,6 +459,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { sort_by: this.sort_selector.sort_by, sort_order: this.sort_selector.sort_order }); + this.toggle_paging && this.$paging_area.toggle(false); } after_render() { @@ -467,6 +470,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { `); this.setup_new_doc_event(); this.list_sidebar.reload_stats(); + this.toggle_paging && this.$paging_area.toggle(true); } render() { @@ -781,7 +785,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return this.settings.get_form_link(doc); } - const docname = doc.name.match(/[%'"]/) + const docname = doc.name.match(/[%'"\s]/) ? encodeURIComponent(doc.name) : doc.name; diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index d77de00970..d233a47893 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -82,6 +82,9 @@ frappe.render_template = function(name, data) { if(data===undefined) { data = {}; } + if (!template) { + frappe.throw(`Template ${name} not found.`); + } return frappe.render(template, data, name); } frappe.render_grid = function(opts) { @@ -160,4 +163,4 @@ frappe.render_pdf = function(html, opts = {}) { } }; xhr.send(formData); -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index f3f3285245..3c729f4af2 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -168,7 +168,14 @@ frappe.set_route = function() { } }).join('/'); - window.location.hash = route; + // Perform a redirect when redirect is set in route_options + if (frappe.route_options && frappe.route_options.redirect) { + const url = new URL(window.location); + url.hash = route; + window.location.replace(url); + } else { + window.location.hash = route; + } // Set favicon (app.js) frappe.provide('frappe.app'); diff --git a/frappe/public/js/frappe/ui/capture.js b/frappe/public/js/frappe/ui/capture.js index 2161970bf7..f08257d394 100644 --- a/frappe/public/js/frappe/ui/capture.js +++ b/frappe/public/js/frappe/ui/capture.js @@ -3,155 +3,139 @@ /** * @description Converts a canvas, image or a video to a data URL string. - * + * * @param {HTMLElement} element - canvas, img or video. * @returns {string} - The data URL string. - * + * * @example * frappe._.get_data_uri(video) * // returns "data:image/pngbase64,..." */ -frappe._.get_data_uri = element => -{ - const $element = $(element) - const width = $element.width() - const height = $element.height() +frappe._.get_data_uri = element => { + const $element = $(element); + const width = $element.width(); + const height = $element.height(); - const $canvas = $('') - $canvas[0].width = width - $canvas[0].height = height + const $canvas = $(''); + $canvas[0].width = width; + $canvas[0].height = height; - const context = $canvas[0].getContext('2d') - context.drawImage($element[0], 0, 0, width, height) - - const data_uri = $canvas[0].toDataURL('image/png') + const context = $canvas[0].getContext('2d'); + context.drawImage($element[0], 0, 0, width, height); - return data_uri -} + const data_uri = $canvas[0].toDataURL('image/png'); + + return data_uri; +}; /** * @description Frappe's Capture object. - * + * * @example * const capture = frappe.ui.Capture() * capture.show() - * + * * capture.click((data_uri) => { * // do stuff * }) - * + * * @see https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos */ -frappe.ui.Capture = class -{ - constructor (options = { }) - { - this.options = frappe.ui.Capture.OPTIONS - this.set_options(options) +frappe.ui.Capture = class { + constructor(options = {}) { + this.options = frappe.ui.Capture.OPTIONS; + this.set_options(options); } - - set_options (options) - { - this.options = { ...frappe.ui.Capture.OPTIONS, ...options } - - return this + + set_options(options) { + this.options = { ...frappe.ui.Capture.OPTIONS, ...options }; + + return this; } - - render ( ) - { - return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => - { - this.dialog = new frappe.ui.Dialog({ - title: this.options.title, + + render() { + return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => { + this.dialog = new frappe.ui.Dialog({ + title: this.options.title, animate: this.options.animate, - action: - { - secondary: - { - label: "×" + action: { + secondary: { + label: '×' } } - }) - - const $e = $(frappe.ui.Capture.TEMPLATE) - - const video = $e.find('video')[0] - video.srcObject = stream - video.play() - - const $container = $(this.dialog.body) - $container.html($e) - - $e.find('.fc-btf').hide() + }); - $e.find('.fc-bcp').click(() => - { - const data_url = frappe._.get_data_uri(video) - $e.find('.fc-p').attr('src', data_url) + const $e = $(frappe.ui.Capture.TEMPLATE); - $e.find('.fc-s').hide() - $e.find('.fc-p').show() + const video = $e.find('video')[0]; + video.srcObject = stream; + video.play(); - $e.find('.fc-btu').hide() - $e.find('.fc-btf').show() - }) + const $container = $(this.dialog.body); + $container.html($e); - $e.find('.fc-br').click(() => - { - $e.find('.fc-p').hide() - $e.find('.fc-s').show() + $e.find('.fc-btf').hide(); - $e.find('.fc-btf').hide() - $e.find('.fc-btu').show() - }) + $e.find('.fc-bcp').click(() => { + const data_url = frappe._.get_data_uri(video); + $e.find('.fc-p').attr('src', data_url); - $e.find('.fc-bs').click(() => - { - const data_url = frappe._.get_data_uri(video) - this.hide() - - if (this.callback) - this.callback(data_url) - }) - }) + $e.find('.fc-s').hide(); + $e.find('.fc-p').show(); + + $e.find('.fc-btu').hide(); + $e.find('.fc-btf').show(); + }); + + $e.find('.fc-br').click(() => { + $e.find('.fc-p').hide(); + $e.find('.fc-s').show(); + + $e.find('.fc-btf').hide(); + $e.find('.fc-btu').show(); + }); + + $e.find('.fc-bs').click(() => { + const data_url = frappe._.get_data_uri(video); + this.hide(); + + if (this.callback) this.callback(data_url); + }); + }); } - show ( ) - { - this.render().then(() => - { - this.dialog.show() - }).catch(err => { - if ( this.options.error ) - { - const alert = ` ${frappe.ui.Capture.ERR_MESSAGE}` - frappe.show_alert(alert, 3) - } + show() { + this.render() + .then(() => { + this.dialog.show(); + }) + .catch(err => { + if (this.options.error) { + const alert = ` ${ + frappe.ui.Capture.ERR_MESSAGE + }`; + frappe.show_alert(alert, 3); + } - throw err - }) + throw err; + }); } - hide ( ) - { - if ( this.dialog ) - this.dialog.hide() + hide() { + if (this.dialog) this.dialog.hide(); } - submit (fn) - { - this.callback = fn + submit(fn) { + this.callback = fn; } -} -frappe.ui.Capture.OPTIONS = -{ - title: __(`Camera`), +}; +frappe.ui.Capture.OPTIONS = { + title: __("Camera"), animate: false, - error: false, -} -frappe.ui.Capture.ERR_MESSAGE = __("Unable to load camera.") -frappe.ui.Capture.TEMPLATE = -` + error: false +}; +frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.'); +frappe.ui.Capture.TEMPLATE = `
@@ -181,14 +165,7 @@ frappe.ui.Capture.TEMPLATE =
- ${ - '' - //
- // - //
- } + ${''}
@@ -201,4 +178,4 @@ frappe.ui.Capture.TEMPLATE =
-` \ No newline at end of file +`; diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 4dedfb32fe..da19ce7eb0 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -54,7 +54,7 @@ frappe.ui.Filter = class { this.filters_config = frappe.boot.additional_filters_config; for (let key of Object.keys(this.filters_config)) { const filter = this.filters_config[key]; - this.conditions.push([key, __(`{0}`, [filter.label])]); + this.conditions.push([key, __(filter.label)]); for (let fieldtype of Object.keys(this.invalid_condition_map)) { if (!filter.valid_for_fieldtypes.includes(fieldtype)) { this.invalid_condition_map[fieldtype].push(key); @@ -542,13 +542,13 @@ frappe.ui.filter_utils = { if (period_map[period]) { period_map[period].forEach((p) => { options.push({ - label: __(`{0} {1}`, [period, p]), + label: `${period} ${p}`, value: `${period.toLowerCase()} ${p.toLowerCase()}`, }); }); } else { options.push({ - label: __(`{0}`, [period]), + label: __(period), value: `${period.toLowerCase()}`, }); } diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 6c577aa0bc..e269c35ca5 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -63,12 +63,15 @@ frappe.ui.FilterGroup = class { } validate_args(doctype, fieldname) { - - if(doctype && fieldname + if (doctype && fieldname && !frappe.meta.has_field(doctype, fieldname) && !frappe.model.std_fields_list.includes(fieldname)) { - frappe.throw(__(`Invalid filter: "${[fieldname.bold()]}"`)); + frappe.msgprint({ + message: __('Invalid filter: {0}', [fieldname.bold()]), + indicator: 'red', + }); + return false; } return true; diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index c37cc41650..9f40c59819 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -128,6 +128,19 @@ frappe.msgprint = function(msg, title, is_minimizable) { data.indicator = 'blue'; } + if (data.as_list) { + const list_rows = data.message.map(m => `
  • ${m}
  • `).join(''); + data.message = `
      ${list_rows}
    `; + } + + if (data.as_table) { + const rows = data.message.map(row => { + const cols = row.map(col => `${col}`).join(''); + return `${cols}`; + }).join(''); + data.message = `${rows}
    `; + } + if(data.message instanceof Array) { data.message.forEach(function(m) { frappe.msgprint(m); diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index fca26d6ece..d69ec1ddde 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -182,7 +182,7 @@ frappe.dashboard_utils = { try { f[3] = eval(f[3]); } catch (e) { - frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`)); + frappe.throw(__("Invalid expression set in filter {0} ({1})", [f[1], f[0]])); } }); filters = [...filters, ...dynamic_filters]; @@ -192,7 +192,7 @@ frappe.dashboard_utils = { const val = eval(dynamic_filters[key]); dynamic_filters[key] = val; } catch (e) { - frappe.throw(__(`Invalid expression set in filter ${key}`)); + frappe.throw(__("Invalid expression set in filter {0}", [key])); } } Object.assign(filters, dynamic_filters); @@ -238,7 +238,7 @@ frappe.dashboard_utils = { let dashboard_route_html = `${values.dashboard}`; let message = - __(`${doctype} ${values.name} added to Dashboard ` + dashboard_route_html); + __("{0} {1} added to Dashboard {2}", [doctype, values.name, dashboard_route_html]); frappe.msgprint(message); }); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index b8eeefb046..10a8aa40f0 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -122,33 +122,38 @@ Object.assign(frappe.utils, {

    '); return content.html(); }, - scroll_to: function(element, animate, additional_offset, element_to_be_scrolled) { + scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) { element_to_be_scrolled = element_to_be_scrolled || $("html, body"); - - var y = 0; - if (element && typeof element==="number") { - y = element; - } else if(element) { - var header_offset = $(".navbar").height() + $(".page-head").height(); - var y = $(element).offset().top - header_offset - cint(additional_offset); + let scroll_top = 0; + if (element) { + // If a number is passed, just subtract the offset, + // otherwise calculate scroll position from element + scroll_top = typeof element == "number" + ? element - cint(additional_offset) + : this.get_scroll_position(element, additional_offset); } - if(y < 0) { - y = 0; + if (scroll_top < 0) { + scroll_top = 0; } // already there - if (y == element_to_be_scrolled.scrollTop()) { + if (scroll_top == element_to_be_scrolled.scrollTop()) { return; } - if (animate !== false) { - element_to_be_scrolled.animate({ scrollTop: y }); + if (animate) { + element_to_be_scrolled.animate({ scrollTop: scroll_top }); } else { - element_to_be_scrolled.scrollTop(y); + element_to_be_scrolled.scrollTop(scroll_top); } }, + get_scroll_position: function(element, additional_offset) { + let header_offset = $(".navbar").height() + $(".page-head").height(); + let scroll_top = $(element).offset().top - header_offset - cint(additional_offset); + return scroll_top; + }, filter_dict: function(dict, filters) { var ret = []; if(typeof filters=='string') { diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 6f6279fd08..92e5a76f9d 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -249,7 +249,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { show_add_chart_dialog() { let fields = this.get_field_options(); const dialog = new frappe.ui.Dialog({ - title: __(`Add a ${this.doctype} Chart`), + title: __("Add a {0} Chart", [this.doctype]), fields: [ { fieldname: 'new_or_existing', diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 0974a6c9f5..b51dd27936 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -70,9 +70,9 @@ export default class Desktop { } make_sidebar() { - const get_sidebar_item = function(item) { + const get_sidebar_item = function (item) { return $(` @@ -87,7 +87,7 @@ export default class Desktop { } let $item = get_sidebar_item(item); let $mobile_item = $item.clone(); - + $item.appendTo(this.sidebar); this.sidebar_items[item.name] = $item; @@ -128,7 +128,7 @@ export default class Desktop { if (this.sidebar_items && this.sidebar_items[this.current_page]) { this.sidebar_items[this.current_page].removeClass("selected"); this.mobile_sidebar_items[this.current_page].removeClass("selected"); - + this.sidebar_items[page].addClass("selected"); this.mobile_sidebar_items[page].addClass("selected"); } @@ -140,9 +140,13 @@ export default class Desktop { } get_page_to_show() { - const default_page = this.sidebar_configuration - ? this.sidebar_configuration["Modules"][0].name - : frappe.boot.allowed_workspaces[0].name; + let default_page; + + if (this.sidebar_configuration && this.sidebar_configuration["Modules"]) { + default_page = this.sidebar_configuration["Modules"][0].name; + } else { + default_page = frappe.boot.allowed_workspaces[0].name; + } let page = frappe.get_route()[1] || @@ -282,6 +286,7 @@ class DesktopPage { } save_customization() { + frappe.dom.freeze(); const config = {}; if (this.sections.charts) config.charts = this.sections.charts.get_widget_config(); @@ -292,19 +297,20 @@ class DesktopPage { page: this.page_name, config: config }).then(res => { + frappe.dom.unfreeze(); if (res.message) { - frappe.msgprint({ message: __("Customizations Saved Successfully"), title: __("Success")}); + frappe.msgprint({ message: __("Customizations Saved Successfully"), title: __("Success") }); this.reload(); } else { - frappe.throw({message: __("Something went wrong while saving customizations"), title: __("Failed")}); + frappe.throw({ message: __("Something went wrong while saving customizations"), title: __("Failed") }); this.reload(); } - }); + }) } make_onboarding() { this.onboarding_widget = frappe.widget.make_widget({ - label: this.data.onboarding.label || __(`Let's Get Started`), + label: this.data.onboarding.label || __("Let's Get Started"), subtitle: this.data.onboarding.subtitle, steps: this.data.onboarding.items, success: this.data.onboarding.success, @@ -324,7 +330,7 @@ class DesktopPage { make_charts() { return frappe.dashboard_utils.get_dashboard_settings().then(settings => { - let chart_config = settings.chart_config ? JSON.parse(settings.chart_config): {}; + let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {}; if (this.data.charts.items) { this.data.charts.items.map(chart => { chart.chart_settings = chart_config[chart.chart_name] || {}; @@ -369,7 +375,7 @@ class DesktopPage { make_cards() { let cards = new frappe.widget.WidgetGroup({ - title: this.data.cards.label || __(`Reports & Masters`), + title: this.data.cards.label || __("Reports & Masters"), container: this.page, type: "links", columns: 3, diff --git a/frappe/public/js/frappe/views/gantt/gantt_view.js b/frappe/public/js/frappe/views/gantt/gantt_view.js index 6fb4688a48..6ab05be96f 100644 --- a/frappe/public/js/frappe/views/gantt/gantt_view.js +++ b/frappe/public/js/frappe/views/gantt/gantt_view.js @@ -10,7 +10,12 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView { .then(() => { this.page_title = this.page_title + ' ' + __('Gantt'); this.calendar_settings = frappe.views.calendar[this.doctype] || {}; - if(this.calendar_settings.order_by) { + + if (typeof this.calendar_settings.gantt == 'object') { + Object.assign(this.calendar_settings, this.calendar_settings.gantt); + } + + if (this.calendar_settings.order_by) { this.sort_by = this.calendar_settings.order_by; this.sort_order = 'asc'; } else { diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js index 4acde8042c..e4d0659965 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_view.js +++ b/frappe/public/js/frappe/views/kanban/kanban_view.js @@ -39,7 +39,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { this.save_kanban_board_filters(); } }); - + this.toggle_paging = true; return this.get_board(); }); } @@ -71,6 +71,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { this.save_view_user_settings({ last_kanban_board: this.board_name }); + this.toggle_paging && this.$paging_area.toggle(false); } render_list() { diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 95e0a966e5..f8bd1cdea8 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -322,12 +322,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { let message; if (dashboard_name) { let dashboard_route_html = `${dashboard_name}`; - message = __(`New {0} {1} added to Dashboard ` + dashboard_route_html, [doctype, name]); + message = __("New {0} {1} added to Dashboard {2}", [doctype, name, dashboard_route_html]); } else { - message = __(`New {0} {1} created`, [doctype, name]); + message = __("New {0} {1} created", [doctype, name]); } - frappe.msgprint(message, __(`New {0} Created`, [doctype])); + frappe.msgprint(message, __("New {0} Created", [doctype])); }); } @@ -433,7 +433,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { try { out = eval(expression.substr(5)); } catch (e) { - frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`)); + frappe.throw(__('Invalid "depends_on" expression set in filter {0}', [filter_label])); } } else { var value = doc[expression]; @@ -738,14 +738,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { get_queued_prepared_reports_warning_message(reports) { const route = `#List/Prepared Report/List?status=Queued&report_name=${this.report_name}`; + const report_link_html = reports.length == 1 + ? `${__('1 Report')}` + : `${__("{0} Reports", [reports.length])}`; + const no_of_reports_html = reports.length == 1 - ? `${__('There is ')}${__('1 Report')}` - : `${__('There are ')}${__(`{} Reports`, [reports.length])}`; + ? `${__('There is {0} with the same filters already in the queue:', [report_link_html])}` + : `${__('There are {0} with the same filters already in the queue:', [report_link_html])}`; let warning_message = `

    - ${__(`Are you sure you want to generate a new report? - {} with the same filters already in the queue:`, [no_of_reports_html])} + ${__("Are you sure you want to generate a new report?")} + ${no_of_reports_html}

    `; let get_item_html = item => `${item.name}`; @@ -1013,7 +1017,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { field.value == values.x_field )[0].label; - options.title = __(`${this.report_name}: ${x_field_label} vs ${y_field_label}`); + options.title = __("{0}: {1} vs {2}", [this.report_name, x_field_label, y_field_label]); this.render_chart(options); this.add_chart_buttons_to_toolbar(true); @@ -1039,7 +1043,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { // applied to Float, Currency fields, needed only for currency formatting. // make first data column have value 'Total' let index = 1; - if (this.datatable && this.datatable.options.checkboxColumn) index = 2; + + if (this.report_settings.get_datatable_options) { + let datatable = this.report_settings.get_datatable_options({}); + if (datatable && datatable.checkboxColumn) index = 2; + } if (column.colIndex === index && !value) { value = "Total"; @@ -1484,7 +1492,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { insert_after_index: insert_after_index, link_field: this.doctype_field_map[values.doctype], doctype: values.doctype, - options: df.fieldtype === "Link" ? df.options : undefined, + options: df.options, width: 100 }); diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 05e8e75eeb..026e120c50 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -651,6 +651,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { } set_fields() { + // default fields + ['name', 'docstatus'].map((f) => this._add_field(f)); + if (this.report_name && this.report_doc.json.fields) { let fields = this.report_doc.json.fields.slice(); fields.forEach(f => this._add_field(f[0], f[1])); @@ -667,12 +670,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { set_default_fields() { // get fields from meta - this.fields = []; + this.fields = this.fields || []; const add_field = f => this._add_field(f); // default fields [ - 'name', 'docstatus', this.meta.title_field, this.meta.image_field ].map(add_field); @@ -1056,17 +1058,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { name: __('Totals Row'), content: totals[col.id], format: value => { - return frappe.format(value, col.docfield, { always_show_decimals: true }, data[0]); + let formatted_value = frappe.format(value, col.docfield, { + always_show_decimals: true + }, data[0]); + if (i === 0) { + return this.format_total_cell(formatted_value, col); + } + return formatted_value; } - } - }) + }; + }); - totals_row[0].content = __('Totals').bold(); out.push(totals_row); } return out; } + format_total_cell(formatted_value, df) { + let cell_value = __('Totals').bold(); + if (frappe.model.is_numeric_field(df.docfield)) { + cell_value = ` + ${cell_value} ${$(formatted_value).text()} + `; + } + return cell_value; + } + build_row(d) { return this.columns.map(col => { if (col.docfield.parent !== this.doctype) { @@ -1247,8 +1264,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { label: __('Show Totals'), action: () => { this.add_totals_row = !this.add_totals_row; - this.save_view_user_settings( - { add_totals_row: this.add_totals_row }); + this.save_view_user_settings({ + add_totals_row: this.add_totals_row + }); this.datatable.refresh(this.get_data(this.data)); } }, diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index ccec5b3ef4..8509eac7a4 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -387,7 +387,7 @@ export default class ChartWidget extends Widget { setup_filter_dialog(fields) { let me = this; let dialog = new frappe.ui.Dialog({ - title: __(`Set Filters for ${this.chart_doc.chart_name}`), + title: __("Set Filters for {0}", [this.chart_doc.chart_name]), fields: fields, primary_action: function() { let values = this.get_values(); @@ -547,6 +547,7 @@ export default class ChartWidget extends Widget { type: chart_type_map[this.chart_doc.type], colors: colors, height: this.height, + maxSlices: ['Pie', 'Donut'].includes(this.chart_doc.type) ? 6 : 9, axisOptions: { xIsSeries: this.chart_doc.timeseries, shortenYAxisNumbers: 1 diff --git a/frappe/public/js/frappe/widgets/new_widget.js b/frappe/public/js/frappe/widgets/new_widget.js index e0cfd51ab4..892fae59f4 100644 --- a/frappe/public/js/frappe/widgets/new_widget.js +++ b/frappe/public/js/frappe/widgets/new_widget.js @@ -19,7 +19,8 @@ export default class NewWidget { get_title() { // DO NOT REMOVE: Comment to load translation // __("New Chart") __("New Shortcut") __("New Number Card") - return __(`New ${frappe.model.unscrub(this.type)}`); + let title = `New ${frappe.model.unscrub(this.type)}`; + return __(title); } make_widget() { diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 6b38412ebd..3708e78d3a 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -268,7 +268,7 @@ export default class NumberCardWidget extends Widget { result: this.number }).then(res => { if (res !== undefined) { - this.percentage_stat = +res.toFixed(2); + this.percentage_stat = shorten_number(res); } }); } diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index 8c1d2cbb5b..a630b8925a 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -37,7 +37,7 @@ export default class OnboardingWidget extends Widget { // Add skip button if (!step.is_mandatory && !step.is_complete) { let skip_html = $( - `Skip` + `${__('Skip')}` ); skip_html.appendTo($step); @@ -443,7 +443,7 @@ export default class OnboardingWidget extends Widget { set_actions() { this.action_area.empty(); const dismiss = $( - `
    Dismiss
    ` + `
    ${__('Dismiss')}
    ` ); dismiss.on("click", () => { let dismissed = JSON.parse( diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 9a255d0776..d95c7baf4f 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -154,7 +154,7 @@ function shorten_number(number, country) { return (number/map.divisor).toFixed(2) + ' ' + map.symbol; } } - return number.toFixed(); + return number.toFixed(2); } function get_number_system(country) { diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index d5b6df6021..fce2a35539 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -35,7 +35,8 @@ class WidgetDialog { // __("New Chart") __("New Shortcut") __("Edit Chart") __("Edit Shortcut") let action = this.editing ? "Edit" : "Add"; - return __(`${action} ${frappe.model.unscrub(this.type)}`); + let label = action = action + " " + frappe.model.unscrub(this.type); + return __(label); } get_fields() { @@ -69,7 +70,7 @@ class WidgetDialog { } let $loading = this.dialog.get_field("filter_area_loading").$wrapper; - $(`Loading Filters...`).appendTo($loading); + $(`${__('Loading Filters...')}`).appendTo($loading); this.filters = []; @@ -149,7 +150,12 @@ class ShortcutDialog extends WidgetDialog { onchange: () => { if (this.dialog.get_value("type") == "DocType") { this.dialog.fields_dict.link_to.get_query = () => { - return { filters: { istable: false } }; + return { + query: "frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes", + filters: { + user: frappe.session.user + } + }; }; } else { this.dialog.fields_dict.link_to.get_query = null; @@ -197,7 +203,7 @@ class ShortcutDialog extends WidgetDialog { fieldname: "doc_view", label: "DocType View", options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar", - description: "Which view of the associated DocType should this shortcut take you to?", + description: __("Which view of the associated DocType should this shortcut take you to?"), default: "List", depends_on: (state) => { if (this.dialog) { @@ -212,7 +218,7 @@ class ShortcutDialog extends WidgetDialog { { fieldtype: "Section Break", fieldname: "filters_section_break", - label: "Count Filter", + label: __("Count Filter"), hidden: 1, }, { @@ -227,13 +233,13 @@ class ShortcutDialog extends WidgetDialog { { fieldtype: "Section Break", fieldname: "count_section_break", - label: "Count Customizations", + label: __("Count Customizations"), hidden: 1, }, { fieldtype: "Color", fieldname: "color", - label: "Color", + label: __("Color"), }, { fieldtype: "Column Break", @@ -242,8 +248,8 @@ class ShortcutDialog extends WidgetDialog { { fieldtype: "Data", fieldname: "format", - label: "Format", - description: "For Example: {} Open", + label: __("Format"), + description: __("For Example: {} Open"), }, ]; } @@ -287,14 +293,14 @@ class NumberCardDialog extends WidgetDialog { fields = [ { fieldtype: 'Select', - label: 'Choose Existing Card or create New Card', + label: __('Choose Existing Card or create New Card'), fieldname: 'new_or_existing', options: ['New Card', 'Existing Card'] }, { fieldtype: 'Link', fieldname: 'card', - label: 'Number Cards', + label: __('Number Cards'), options: 'Number Card', get_query: () => { return { @@ -312,13 +318,13 @@ class NumberCardDialog extends WidgetDialog { depends_on: 'eval: doc.new_or_existing == "New Card"' }, { - label: 'Label', + label: __('Label'), fieldname: 'label', fieldtype: 'Data', mandatory_depends_on: 'eval: doc.new_or_existing == "New Card"' }, { - label: 'Doctype', + label: __('Doctype'), fieldname: 'document_type', fieldtype: 'Link', options: 'DocType', @@ -330,7 +336,7 @@ class NumberCardDialog extends WidgetDialog { hidden: 1 }, { - label: 'Color', + label: __('Color'), fieldname: 'color', fieldtype: 'Color' }, @@ -339,14 +345,14 @@ class NumberCardDialog extends WidgetDialog { fieldname: "cb_1", }, { - label: 'Function', + label: __('Function'), fieldname: 'function', fieldtype: 'Select', options: ['Count', 'Sum', 'Average', 'Minimum', 'Maximum'], mandatory_depends_on: 'eval: doc.new_or_existing == "New Card"' }, { - label: 'Function Based On', + label: __('Function Based On'), fieldname: 'aggregate_function_based_on', fieldtype: 'Select', depends_on: "eval: doc.function !== 'Count'", @@ -355,7 +361,7 @@ class NumberCardDialog extends WidgetDialog { { fieldtype: "Section Break", fieldname: "sb_1", - label: 'Add Filters', + label: __('Add Filters'), depends_on: 'eval: doc.new_or_existing == "New Card"' }, { diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less index a738679cc8..dc0ff458e9 100644 --- a/frappe/public/less/desktop.less +++ b/frappe/public/less/desktop.less @@ -147,6 +147,12 @@ .desk-body { padding-left: 0px; padding-right: calc(20rem + 15px); + + .desk-page.allow-customization { + .customize-options { + text-align: left; + } + } } } diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less index 3c9e096444..17e612caf8 100644 --- a/frappe/public/less/form_grid.less +++ b/frappe/public/less/form_grid.less @@ -1,6 +1,10 @@ @import "variables.less"; @import "mixins.less"; +.grid-field { + position: relative; +} + .form-grid { border: 1px solid @border-color; border-radius: 3px; @@ -294,6 +298,16 @@ html.chrome .grid-row .grid-row-check { display: flow-root; } +.grid-custom-buttons { + position: absolute; + right: 0; + top: -5px; +} + +.btn-custom:not(:last-child) { + margin-right: 5px; +} + .grid-buttons { display: inline-flex; } diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index 4c7d04406d..7e57d23fdc 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -52,7 +52,7 @@ body.no-list-sidebar { [data-page-route^="List/"] { - @media (min-width: @screen-md) { + @media (min-width: @screen-sm) { .layout-side-section { display: none; } diff --git a/frappe/public/less/mobile.less b/frappe/public/less/mobile.less index 6b587c5026..55347c7d47 100644 --- a/frappe/public/less/mobile.less +++ b/frappe/public/less/mobile.less @@ -15,6 +15,7 @@ body { html, body { overflow-x: hidden; //Prevent scroll on narrow devices + overflow-y: overlay; } @media(max-width: 991px) { diff --git a/frappe/public/less/navbar.less b/frappe/public/less/navbar.less index 5cb050b10f..ba9c0e242a 100644 --- a/frappe/public/less/navbar.less +++ b/frappe/public/less/navbar.less @@ -266,6 +266,8 @@ .navbar-brand > img { display: inline-block; + height: 24px; + max-width: 150px; } .toggle-sidebar { diff --git a/frappe/recorder.py b/frappe/recorder.py index 388efcbf6e..e3eef94809 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -53,16 +53,18 @@ def sql(*args, **kwargs): def get_current_stack_frames(): - current = inspect.currentframe() - frames = inspect.getouterframes(current, context=10) - for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]: - if "/apps/" in filename: - yield { - "filename": re.sub(".*/apps/", "", filename), - "lineno": lineno, - "function": function, - } - + try: + current = inspect.currentframe() + frames = inspect.getouterframes(current, context=10) + for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]: + if "/apps/" in filename: + yield { + "filename": re.sub(".*/apps/", "", filename), + "lineno": lineno, + "function": function, + } + except Exception: + pass def record(): if __debug__: diff --git a/frappe/templates/includes/app_analytics/google_analytics.html b/frappe/templates/includes/app_analytics/google_analytics.html index 65199548b9..7c3b165a8f 100644 --- a/frappe/templates/includes/app_analytics/google_analytics.html +++ b/frappe/templates/includes/app_analytics/google_analytics.html @@ -6,6 +6,9 @@ })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', '{{ google_analytics_id }}', 'auto'); + {% if google_analytics_anonymize_ip %} + ga('set', 'anonymizeIp', true); + {% endif %} $(document).on("mousedown", function(event) { if(!frappe && !frappe.get_route) return; diff --git a/frappe/templates/includes/footer/footer_grouped_links.html b/frappe/templates/includes/footer/footer_grouped_links.html index 0383409090..350f20af81 100644 --- a/frappe/templates/includes/footer/footer_grouped_links.html +++ b/frappe/templates/includes/footer/footer_grouped_links.html @@ -15,7 +15,8 @@