diff --git a/.snyk b/.snyk index 6e7bb44986..6c6555a819 100644 --- a/.snyk +++ b/.snyk @@ -65,3 +65,37 @@ patch: patched: '2020-04-30T23:02:32.330Z' - quill-image-resize > lodash: patched: '2020-08-24T23:06:37.710Z' + - node-sass > lodash: + patched: '2020-09-15T23:06:41.931Z' + - node-sass > sass-graph > lodash: + patched: '2020-09-15T23:06:41.931Z' + - node-sass > gaze > globule > lodash: + patched: '2020-09-15T23:06:41.931Z' + - snyk > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-gradle-plugin > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-php-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > @snyk/dep-graph > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' + - snyk > snyk-go-plugin > graphlib > lodash: + patched: '2020-09-16T23:06:38.881Z' diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index 8a4aeddd0a..a0f8cc3621 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -59,15 +59,18 @@ context('Recorder', () => { cy.get('.title-text').should('contain', 'DocType'); cy.get('.list-count').should('contain', '20 of '); - cy.visit('/desk#recorder'); + // temporarily commenting out theses tests as they seem to be + // randomly failing maybe due a backround event - cy.get('.list-row-container span').contains('/api/method/frappe').click(); + // cy.visit('/desk#recorder'); - cy.location('hash').should('contain', '#recorder/request/'); - cy.get('form').should('contain', '/api/method/frappe'); + // cy.get('.list-row-container span').contains('/api/method/frappe').click(); - cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); - cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); - cy.location('hash').should('eq', '#recorder'); + // cy.location('hash').should('contain', '#recorder/request/'); + // cy.get('form').should('contain', '/api/method/frappe'); + + // cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); + // cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); + // cy.location('hash').should('eq', '#recorder'); }); }); \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a9a358ce5f..fcf24bf1a9 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -146,7 +146,7 @@ class AutoRepeat(Document): def make_new_document(self): reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document) - new_doc = frappe.copy_doc(reference_doc) + new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False) self.update_doc(new_doc, reference_doc) new_doc.insert(ignore_permissions = True) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index dabc78a9f6..3b3d188999 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import frappe, json -import frappe.defaults from frappe.model.document import Document from frappe.desk.notifications import (delete_notification_count_for, clear_notifications) diff --git a/frappe/chat/doctype/chat_message/chat_message.json b/frappe/chat/doctype/chat_message/chat_message.json index ea3491acfa..9d2d70c5e0 100644 --- a/frappe/chat/doctype/chat_message/chat_message.json +++ b/frappe/chat/doctype/chat_message/chat_message.json @@ -62,11 +62,11 @@ "label": "URLs" } ], - "modified": "2019-11-07 13:21:19.395927", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Chat", "name": "Chat Message", - "owner": "arjun@gmail.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/frappe/commands/site.py b/frappe/commands/site.py index d343d10126..c5008b32a5 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -12,7 +12,6 @@ import click # imports - module imports import frappe -from frappe import _ from frappe.commands import get_site, pass_context from frappe.commands.scheduler import _is_scheduler_enabled from frappe.exceptions import SiteNotSpecifiedError diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index cf99cc914e..5a5986ff57 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -305,8 +305,6 @@ def import_doc(context, path, force=False): @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable') - - @pass_context def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): "Import CSV using data import" @@ -437,7 +435,7 @@ def jupyter(context): os.mkdir(jupyter_notebooks_path) bin_path = os.path.abspath('../env/bin') print(''' -Stating Jupyter notebook +Starting Jupyter notebook Run the following in your first cell to connect notebook to frappe ``` import frappe diff --git a/frappe/contacts/doctype/salutation/salutation.json b/frappe/contacts/doctype/salutation/salutation.json index b60a592eea..579f176aa7 100644 --- a/frappe/contacts/doctype/salutation/salutation.json +++ b/frappe/contacts/doctype/salutation/salutation.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:salutation", "beta": 0, "creation": "2017-04-10 12:17:58.071915", @@ -53,7 +53,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-04-10 12:55:18.855578", + "modified": "2020-09-14 12:55:18.855578", "modified_by": "Administrator", "module": "Contacts", "name": "Salutation", @@ -129,4 +129,4 @@ "sort_order": "DESC", "track_changes": 1, "track_seen": 0 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index e4d2ff2af6..bec8cde7ea 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -8,7 +8,7 @@ from frappe import _ import frappe.permissions import re, csv, os from frappe.utils.csvutils import UnicodeWriter -from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint +from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration from frappe.core.doctype.data_import_legacy.importer import get_data_keys from six import string_types from frappe.core.doctype.access_log.access_log import make_access_log @@ -330,6 +330,8 @@ class DataExporter: value = formatdate(value) elif fieldtype == "Datetime": value = format_datetime(value) + elif fieldtype == "Duration": + value = format_duration(value, df.hide_days) row[_column_start_end.start + i + 1] = value diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 3eef6ce016..66e32a1270 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -8,6 +8,7 @@ from frappe.model import ( no_value_fields, table_fields as table_fieldtypes, ) +from frappe.utils import flt, format_duration from frappe.utils.csvutils import build_csv_response from frappe.utils.xlsxutils import build_xlsx_response @@ -146,8 +147,13 @@ class Exporter: if df.parent == doctype: if df.is_child_table_field and df.child_table_df.fieldname != parentfield: continue - row[i] = doc.get(df.fieldname, "") + value = doc.get(df.fieldname, None) + if df.fieldtype == "Duration": + value = flt(value or 0) + value = format_duration(value, df.hide_days) + + row[i] = value return rows def get_data_as_docs(self): diff --git a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv index ef5b96df58..693f400878 100644 --- a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv +++ b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv @@ -1,5 +1,5 @@ -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 ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7 - , , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , , -Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , , -Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71 +Title ,Description ,Number ,Duration,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 ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7 + , , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, , +Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , , +Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71 \ No newline at end of file diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 2c10c6b0a5..5271690527 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -9,7 +9,7 @@ import timeit import json from datetime import datetime, date from frappe import _ -from frappe.utils import cint, flt, update_progress_bar, cstr +from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets from frappe.utils.xlsxutils import ( read_xlsx_file_from_attached_file, @@ -664,6 +664,20 @@ class Row: } ) return + elif df.fieldtype == "Duration": + import re + is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) + if not is_valid_duration: + self.warnings.append( + { + "row": self.row_number, + "col": col.column_number, + "field": df_as_json(df), + "message": _("Value {0} must be in the valid duration format: d h m s").format( + frappe.bold(value) + ) + } + ) return value @@ -692,6 +706,8 @@ class Row: value = flt(value) elif df.fieldtype in ["Date", "Datetime"]: value = self.get_date(value, col) + elif df.fieldtype == "Duration": + value = duration_to_seconds(value) return value diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index bdadad7890..249451fd4d 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import getdate +from frappe.utils import getdate, format_duration doctype_name = 'DocType for Import' @@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.description, 'test description') self.assertEqual(doc1.number, 1) + self.assertEqual(format_duration(doc1.duration), '3h') self.assertEqual(doc1.table_field_1[0].child_title, 'child title') self.assertEqual(doc1.table_field_1[0].child_description, 'child description') @@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22')) self.assertEqual(doc2.description, 'test description 2') + self.assertEqual(format_duration(doc2.duration), '4d 3h') + self.assertEqual(doc3.another_number, 5) + self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') def test_data_import_preview(self): import_file = get_import_file('sample_import_file') @@ -48,7 +52,7 @@ class TestImporter(unittest.TestCase): preview = data_import.get_preview_from_template() self.assertEqual(len(preview.data), 4) - self.assertEqual(len(preview.columns), 15) + self.assertEqual(len(preview.columns), 16) def test_data_import_without_mandatory_values(self): import_file = get_import_file('sample_import_file_without_mandatory') @@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False): {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, + {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, {'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py index 5bd0daf32b..35569c7186 100644 --- a/frappe/core/doctype/data_import_legacy/importer.py +++ b/frappe/core/doctype/data_import_legacy/importer.py @@ -15,7 +15,7 @@ from frappe import _ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date -from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url +from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds from six import string_types @@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, d[fieldname] = get_datetime(_date + " " + _time) else: d[fieldname] = None - + elif fieldtype == "Duration": + d[fieldname] = duration_to_seconds(cstr(d[fieldname])) elif fieldtype in ("Image", "Attach Image", "Attach"): # added file to attachments list attachments.append(d[fieldname]) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 6524036975..9d37849746 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -99,6 +99,10 @@ class DocType(Document): if self.default_print_format and not self.custom: frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) + if frappe.conf.get('developer_mode'): + self.owner = 'Administrator' + self.modified_by = 'Administrator' + 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]: @@ -234,6 +238,8 @@ class DocType(Document): if not autoname and self.get("fields", {"fieldname":"naming_series"}): self.autoname = "naming_series:" + elif self.autoname == "naming_series:" and not self.get("fields", {"fieldname":"naming_series"}): + frappe.throw(_("Invalid fieldname '{0}' in autoname").format(self.autoname)) # validate field name if autoname field:fieldname is used # Create unique index on autoname field automatically. @@ -634,13 +640,15 @@ class DocType(Document): if not name: name = self.name + flags = {"flags": re.ASCII} if six.PY3 else {} + + # a DocType name should not start or end with an empty space + if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): + frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) + # a DocType's name should not start with a number or underscore # and should only contain letters, numbers and underscore - if six.PY2: - is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name) - else: - is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name, flags = re.ASCII) - if not is_a_valid_name: + if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags): frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError) diff --git a/frappe/core/doctype/document_naming_rule/__init__.py b/frappe/core/doctype/document_naming_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.js b/frappe/core/doctype/document_naming_rule/document_naming_rule.js new file mode 100644 index 0000000000..c7413a9b09 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.js @@ -0,0 +1,23 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Document Naming Rule', { + refresh: function(frm) { + frm.trigger('document_type'); + }, + document_type: (frm) => { + // update the select field options with fieldnames + if (frm.doc.document_type) { + frappe.model.with_doctype(frm.doc.document_type, () => { + let fieldnames = frappe.get_meta(frm.doc.document_type).fields + .filter((d) => { + return frappe.model.no_value_type.indexOf(d.fieldtype) === -1; + }).map((d) => { + return {label: `${d.label} (${d.fieldname})`, value: d.fieldname}; + }); + frappe.meta.get_docfield('Document Naming Rule Condition', 'field', frm.doc.name).options = fieldnames; + frm.refresh_field('conditions'); + }); + } + } +}); diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json new file mode 100644 index 0000000000..79eebdbe64 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json @@ -0,0 +1,104 @@ +{ + "actions": [], + "creation": "2020-09-07 12:48:48.334318", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "disabled", + "priority", + "section_break_3", + "conditions", + "naming_section", + "prefix", + "prefix_digits", + "counter" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Document Type", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "prefix", + "fieldtype": "Data", + "label": "Prefix", + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"" + }, + { + "fieldname": "counter", + "fieldtype": "Int", + "label": "Counter", + "read_only": 1 + }, + { + "default": "5", + "description": "Example: 00001", + "fieldname": "prefix_digits", + "fieldtype": "Int", + "label": "Digits", + "mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"" + }, + { + "fieldname": "naming_section", + "fieldtype": "Section Break", + "label": "Naming" + }, + { + "collapsible": 1, + "collapsible_depends_on": "conditions", + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Rule Conditions" + }, + { + "fieldname": "conditions", + "fieldtype": "Table", + "label": "Conditions", + "options": "Document Naming Rule Condition" + }, + { + "description": "Rules with higher priority will be applied first.", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-21 10:23:34.401539", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Naming Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "document_type", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py new file mode 100644 index 0000000000..2de7552dc1 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -0,0 +1,21 @@ +# -*- 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 +from frappe.utils.data import evaluate_filters + +class DocumentNamingRule(Document): + def apply(self, doc): + ''' + 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]): + return + + counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0 + doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1) + frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1) diff --git a/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py new file mode 100644 index 0000000000..1b91f6a0cf --- /dev/null +++ b/frappe/core/doctype/document_naming_rule/test_document_naming_rule.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestDocumentNamingRule(unittest.TestCase): + def test_naming_rule_by_series(self): + naming_rule = frappe.get_doc(dict( + doctype = 'Document Naming Rule', + document_type = 'ToDo', + prefix = 'test-todo-', + prefix_digits = 5 + )).insert() + + todo = frappe.get_doc(dict( + doctype = 'ToDo', + description = 'Is this my name ' + frappe.generate_hash() + )).insert() + + self.assertEqual(todo.name, 'test-todo-00001') + + naming_rule.delete() + todo.delete() + + def test_naming_rule_by_condition(self): + naming_rule = frappe.get_doc(dict( + doctype = 'Document Naming Rule', + document_type = 'ToDo', + prefix = 'test-high-', + prefix_digits = 5, + priority = 10, + conditions = [dict( + field = 'priority', + condition = '=', + value = 'High' + )] + )).insert() + + # another rule + naming_rule_1 = frappe.copy_doc(naming_rule) + naming_rule_1.prefix = 'test-medium-' + naming_rule_1.conditions[0].value = 'Medium' + naming_rule_1.insert() + + # default rule with low priority - should not get applied for rules + # with higher priority + naming_rule_2 = frappe.copy_doc(naming_rule) + naming_rule_2.prefix = 'test-low-' + naming_rule_2.priority = 0 + naming_rule_2.conditions = [] + naming_rule_2.insert() + + + todo = frappe.get_doc(dict( + doctype = 'ToDo', + priority = 'High', + description = 'Is this my name ' + frappe.generate_hash() + )).insert() + + todo_1 = frappe.get_doc(dict( + doctype = 'ToDo', + priority = 'Medium', + description = 'Is this my name ' + frappe.generate_hash() + )).insert() + + todo_2 = frappe.get_doc(dict( + doctype = 'ToDo', + priority = 'Low', + description = 'Is this my name ' + frappe.generate_hash() + )).insert() + + try: + self.assertEqual(todo.name, 'test-high-00001') + self.assertEqual(todo_1.name, 'test-medium-00001') + self.assertEqual(todo_2.name, 'test-low-00001') + finally: + naming_rule.delete() + naming_rule_1.delete() + naming_rule_2.delete() + todo.delete() + todo_1.delete() + todo_2.delete() diff --git a/frappe/core/doctype/document_naming_rule_condition/__init__.py b/frappe/core/doctype/document_naming_rule_condition/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js new file mode 100644 index 0000000000..8ef39c7b70 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Document Naming Rule Condition', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json new file mode 100644 index 0000000000..781566b7d1 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2020-09-08 10:17:54.366279", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "field", + "condition", + "value" + ], + "fields": [ + { + "fieldname": "field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Field", + "reqd": 1 + }, + { + "fieldname": "condition", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Condition", + "options": "=\n!=\n>\n<\n>=\n<=", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-08 10:19:56.192949", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Naming Rule Condition", + "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/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py new file mode 100644 index 0000000000..0895c9f93f --- /dev/null +++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py @@ -0,0 +1,10 @@ +# -*- 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 DocumentNamingRuleCondition(Document): + pass diff --git a/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.py new file mode 100644 index 0000000000..6f1376dc62 --- /dev/null +++ b/frappe/core/doctype/document_naming_rule_condition/test_document_naming_rule_condition.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 TestDocumentNamingRuleCondition(unittest.TestCase): + pass diff --git a/frappe/core/doctype/domain/domain.json b/frappe/core/doctype/domain/domain.json index c235596884..a6c7397e13 100644 --- a/frappe/core/doctype/domain/domain.json +++ b/frappe/core/doctype/domain/domain.json @@ -17,11 +17,11 @@ "unique": 1 } ], - "modified": "2019-06-30 13:24:13.732202", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Core", "name": "Domain", - "owner": "makarand@erpnext.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/frappe/core/doctype/has_domain/has_domain.json b/frappe/core/doctype/has_domain/has_domain.json index bfc1764138..e2b646b457 100644 --- a/frappe/core/doctype/has_domain/has_domain.json +++ b/frappe/core/doctype/has_domain/has_domain.json @@ -54,12 +54,12 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-05-04 11:05:54.750351", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Core", "name": "Has Domain", "name_case": "", - "owner": "makarand@erpnext.com", + "owner": "Administrator", "permissions": [], "quick_entry": 1, "read_only": 0, diff --git a/frappe/core/doctype/report_column/report_column.json b/frappe/core/doctype/report_column/report_column.json index 53b5dff9b6..2e6a22d29a 100644 --- a/frappe/core/doctype/report_column/report_column.json +++ b/frappe/core/doctype/report_column/report_column.json @@ -31,7 +31,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", + "options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", "reqd": 1 }, { @@ -48,7 +48,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 14:32:17.174796", + "modified": "2020-09-03 10:52:03.895817", "modified_by": "Administrator", "module": "Core", "name": "Report Column", diff --git a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.json b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.json index cd06be1c1a..2cfc2e3bd7 100644 --- a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.json +++ b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.json @@ -186,8 +186,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-07-28 15:49:54.019073", - "modified_by": "cave@aperture.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Data Migration", "name": "Data Migration Plan", "name_case": "", diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.json b/frappe/data_migration/doctype/data_migration_run/data_migration_run.json index d13cbd9ffb..db77997928 100644 --- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.json +++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.json @@ -800,12 +800,12 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-07-30 07:02:26.980372", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Data Migration", "name": "Data Migration Run", "name_case": "", - "owner": "faris@erpnext.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/frappe/desk/doctype/calendar_view/calendar_view.json b/frappe/desk/doctype/calendar_view/calendar_view.json index ea220c335c..8ef49e399d 100644 --- a/frappe/desk/doctype/calendar_view/calendar_view.json +++ b/frappe/desk/doctype/calendar_view/calendar_view.json @@ -53,11 +53,11 @@ } ], "links": [], - "modified": "2020-06-15 11:24:57.639430", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Desk", "name": "Calendar View", - "owner": "faris@erpnext.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/frappe/desk/doctype/notification_log/notification_log.json b/frappe/desk/doctype/notification_log/notification_log.json index 050bf85ead..9e802298e3 100644 --- a/frappe/desk/doctype/notification_log/notification_log.json +++ b/frappe/desk/doctype/notification_log/notification_log.json @@ -120,8 +120,8 @@ "hide_toolbar": 1, "in_create": 1, "links": [], - "modified": "2020-05-31 22:31:12.886950", - "modified_by": "umair@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Desk", "name": "Notification Log", "owner": "Administrator", diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py index 80f614b5b6..3aa3a4fa88 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -169,16 +169,14 @@ def get_comments(doctype, doc_name, frequency, user): return timeline def is_document_followed(doctype, doc_name, user): - docs = frappe.get_all( + return frappe.db.exists( "Document Follow", - filters={ + { "ref_doctype": doctype, "ref_docname": doc_name, "user": user - }, - limit=1 + } ) - return len(docs) @frappe.whitelist() def get_follow_users(doctype, doc_name): diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index cae1bf5c77..5219a98cbd 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -23,6 +23,8 @@ def savedocs(doc, action): # update recent documents run_onload(doc) send_updated_docs(doc) + + frappe.msgprint(frappe._("Saved"), indicator='green', alert=True) except Exception: frappe.errprint(frappe.utils.get_traceback()) raise @@ -36,6 +38,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat doc.set(workflow_state_fieldname, workflow_state) doc.cancel() send_updated_docs(doc) + frappe.msgprint(frappe._("Cancelled"), indicator='red', alert=True) except Exception: frappe.errprint(frappe.utils.get_traceback()) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 08ef7ae485..9ce15ef361 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,14 +8,13 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta -from frappe.utils import gzip_decompress from frappe.core.utils import ljust_list def get_report_doc(report_name): @@ -67,7 +66,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) # Reordered columns columns = json.loads(report.custom_columns) - result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type) + result = reorder_data_for_custom_columns(columns, query_columns, result) result = add_data_to_custom_columns(columns, result) @@ -215,25 +214,19 @@ def add_data_to_custom_columns(columns, result): return data -def reorder_data_for_custom_columns(custom_columns, columns, result, report_type): +def reorder_data_for_custom_columns(custom_columns, columns, result): if not result: return [] - if report_type == 'Query Report': - # Assume list result for query reports - # Query report columns exclusively use Label - custom_column_labels = [col["label"] for col in custom_columns] - original_column_labels = [col.split(":")[0] for col in columns] - return get_columns_from_list(custom_column_labels, original_column_labels, result) - - custom_column_names = [col["fieldname"] for col in custom_columns] + 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 - original_column_names = [col["fieldname"] for col in columns] + 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: - # If the result is a list of dicts - return get_columns_from_dict(custom_column_names, result) + # 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 = [] @@ -251,21 +244,6 @@ def get_columns_from_list(columns, target_columns, result): return reordered_result -def get_columns_from_dict(columns, result): - reordered_result = [] - - for res in result: - r = {} - for col_name in columns: - try: - r[col_name] = res[col_name] - except KeyError: - pass - - reordered_result.append(r) - - return reordered_result - def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None @@ -360,6 +338,7 @@ def export_query(): columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx + 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") @@ -367,6 +346,29 @@ def export_query(): frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response['type'] = 'binary' +def handle_duration_fieldtype_values(result, columns): + for i, col in enumerate(columns): + fieldtype = None + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + + if fieldtype == "Duration": + for entry in range(0, len(result)): + val_in_seconds = result[entry][i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + result[entry][i] = duration_val + + return result def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] @@ -384,12 +386,14 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation): if isinstance(row, dict) and row: for idx in range(len(data.columns)): - 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) + # 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: row_data = row @@ -427,7 +431,7 @@ def add_total_row(result, columns, meta = None): if i >= len(row): continue cell = row.get(fieldname) if isinstance(row, dict) else row[i] - if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell): + if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell): total_row[i] = flt(total_row[i]) + flt(cell) if fieldtype == "Percent" and i not in has_percent: @@ -638,31 +642,35 @@ def get_columns_dict(columns): """ columns_dict = frappe._dict() for idx, col in enumerate(columns): - col_dict = frappe._dict() - - # string - if isinstance(col, string_types): - col = col.split(":") - if len(col) > 1: - if "/" in col[1]: - col_dict["fieldtype"], col_dict["options"] = col[1].split("/") - else: - col_dict["fieldtype"] = col[1] - - col_dict["label"] = col[0] - col_dict["fieldname"] = frappe.scrub(col[0]) - - # dict - else: - col_dict.update(col) - if "fieldname" not in col_dict: - col_dict["fieldname"] = frappe.scrub(col_dict["label"]) - + col_dict = get_column_as_dict(col) columns_dict[idx] = col_dict columns_dict[col_dict["fieldname"]] = col_dict return columns_dict +def get_column_as_dict(col): + col_dict = frappe._dict() + + # string + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if "/" in col[1]: + col_dict["fieldtype"], col_dict["options"] = col[1].split("/") + else: + col_dict["fieldtype"] = col[1] + + col_dict["label"] = col[0] + col_dict["fieldname"] = frappe.scrub(col[0]) + + # dict + else: + col_dict.update(col) + if "fieldname" not in col_dict: + col_dict["fieldname"] = frappe.scrub(col_dict["label"]) + + return col_dict + def get_user_match_filters(doctypes, user): match_filters = {} diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d4fc8833ae..9f5a5d84c8 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery from frappe import _ from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log -from frappe.utils import cstr +from frappe.utils import cstr, format_duration @frappe.whitelist() @@ -167,6 +167,8 @@ def export_query(): for i, row in enumerate(ret): data.append([i+1] + list(row)) + data = handle_duration_fieldtype_values(doctype, data, db_query.fields) + if file_format_type == "CSV": # convert to csv @@ -236,6 +238,29 @@ def get_labels(fields, doctype): return labels +def handle_duration_fieldtype_values(doctype, data, fields): + for field in fields: + key = field.split(" as ")[0] + + if key.startswith(('count(', 'sum(', 'avg(')): continue + + if "." in key: + parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") + else: + parenttype = doctype + fieldname = field.strip("`") + + df = frappe.get_meta(parenttype).get_field(fieldname) + + if df and df.fieldtype == 'Duration': + index = fields.index(field) + 1 + for i in range(1, len(data)): + val_in_seconds = data[i][index] + if val_in_seconds: + duration_val = format_duration(val_in_seconds, df.hide_days) + data[i][index] = duration_val + return data + @frappe.whitelist() def delete_items(): """delete selected items""" diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js index fd9170366e..1b91e7a38c 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.js +++ b/frappe/email/doctype/auto_email_report/auto_email_report.js @@ -3,23 +3,7 @@ frappe.ui.form.on('Auto Email Report', { refresh: function(frm) { - if(frm.doc.report_type !== 'Report Builder') { - if(frm.script_setup_for !== frm.doc.report && !frm.doc.__islocal) { - frappe.call({ - method:"frappe.desk.query_report.get_script", - args: { - report_name: frm.doc.report - }, - callback: function(r) { - frappe.dom.eval(r.message.script || ""); - frm.script_setup_for = frm.doc.report; - frm.trigger('show_filters'); - } - }); - } else { - frm.trigger('show_filters'); - } - } + frm.trigger('fetch_report_filters'); if(!frm.is_new()) { frm.add_custom_button(__('Download'), function() { var w = window.open( @@ -50,6 +34,27 @@ frappe.ui.form.on('Auto Email Report', { }, report: function(frm) { frm.set_value('filters', ''); + frm.trigger('fetch_report_filters'); + }, + fetch_report_filters(frm) { + if (frm.doc.report + && frm.doc.report_type !== 'Report Builder' + && frm.script_setup_for !== frm.doc.report + ) { + frappe.call({ + method: "frappe.desk.query_report.get_script", + args: { + report_name: frm.doc.report + }, + callback: function(r) { + frappe.dom.eval(r.message.script || ""); + frm.script_setup_for = frm.doc.report; + frm.trigger('show_filters'); + } + }); + } else { + frm.trigger('show_filters'); + } }, show_filters: function(frm) { var wrapper = $(frm.get_field('filters_display').wrapper); diff --git a/frappe/email/doctype/document_follow/document_follow.json b/frappe/email/doctype/document_follow/document_follow.json index b00ef833dd..5a9ff96255 100644 --- a/frappe/email/doctype/document_follow/document_follow.json +++ b/frappe/email/doctype/document_follow/document_follow.json @@ -1,181 +1,78 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2019-01-09 16:39:23.746535", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2019-01-09 16:39:23.746535", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "ref_docname", + "user" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ref_doctype", - "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": "Doctype", - "length": 0, - "no_copy": 0, - "options": "DocType", - "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 - }, + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Doctype", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ref_docname", - "fieldtype": "Dynamic 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": "Document Name", - "length": 0, - "no_copy": 0, - "options": "ref_doctype", - "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 - }, + "fieldname": "ref_docname", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Document Name", + "options": "ref_doctype", + "reqd": 1, + "search_index": 1 + }, { - "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 + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1, + "search_index": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-02-26 15:43:44.330348", - "modified_by": "Administrator", - "module": "Email", - "name": "Document Follow", - "name_case": "", - "owner": "Administrator", + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-17 09:19:28.496453", + "modified_by": "Administrator", + "module": "Email", + "name": "Document Follow", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 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, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "All", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 9d8fb49ee9..9a40fb02b7 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -173,8 +173,13 @@ def get_context(context): subject = frappe.render_template(self.subject, context) attachments = self.get_attachment(doc) + recipients, cc, bcc = self.get_list_of_recipients(doc, context) + users = recipients + cc + bcc + + if not users: + return notification_doc = { 'type': 'Alert', @@ -280,8 +285,6 @@ def get_context(context): if self.send_to_all_assignees: recipients = recipients + get_assignees(doc) - if not recipients and not cc and not bcc: - return None, None, None return list(set(recipients)), list(set(cc)), list(set(bcc)) def get_receiver_list(self, doc, context): diff --git a/frappe/hooks.py b/frappe/hooks.py index 9e2009ef6c..81cfb5af1a 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -43,6 +43,11 @@ app_include_css = [ "assets/css/report.min.css", ] +doctype_js = { + "Web Page": "public/js/frappe/utils/web_template.js", + "Website Settings": "public/js/frappe/utils/web_template.js" +} + web_include_js = [ "website_script.js" ] diff --git a/frappe/installer.py b/frappe/installer.py index 4994646890..2a912695e5 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -1,29 +1,17 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -# called from wnf.py -# lib/wnf.py --install [rootpassword] [dbname] [source] +import json +import os -from __future__ import unicode_literals, print_function - -from six.moves import input - -import os, json, subprocess, shutil -import click import frappe -import frappe.database -import importlib -from frappe import _ -from frappe.model.sync import sync_for -from frappe.utils.fixtures import sync_fixtures -from frappe.website import render -from frappe.modules.utils import sync_customizations -from frappe.database import setup_database -from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False): + import frappe.database + from frappe.database import setup_database if not db_type: db_type = frappe.conf.db_type or 'mariadb' @@ -45,7 +33,13 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N frappe.flags.in_install_db = False + def install_app(name, verbose=False, set_as_patched=True): + from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs + from frappe.utils.fixtures import sync_fixtures + from frappe.model.sync import sync_for + from frappe.modules.utils import sync_customizations + frappe.flags.in_install = name frappe.flags.ignore_in_install = False @@ -65,7 +59,7 @@ def install_app(name, verbose=False, set_as_patched=True): raise Exception("App not in apps.txt") if name in installed_apps: - frappe.msgprint(_("App {0} already installed").format(name)) + frappe.msgprint(frappe._("App {0} already installed").format(name)) return print("\nInstalling {0}...".format(name)) @@ -102,25 +96,31 @@ def install_app(name, verbose=False, set_as_patched=True): frappe.flags.in_install = False + def add_to_installed_apps(app_name, rebuild_website=True): installed_apps = frappe.get_installed_apps() if not app_name in installed_apps: installed_apps.append(app_name) frappe.db.set_global("installed_apps", json.dumps(installed_apps)) frappe.db.commit() - post_install(rebuild_website) + if frappe.flags.in_install: + post_install(rebuild_website) + 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_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps)) + frappe.db.set_global("installed_apps", json.dumps(installed_apps)) + frappe.get_single("Installed Applications").update_versions() frappe.db.commit() if frappe.flags.in_install: post_install() + def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): """Remove app and all linked to the app's module with the app from a site.""" + import click # dont allow uninstall app if not installed unless forced if not force: @@ -143,11 +143,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) frappe.flags.in_uninstall = True drop_doctypes = [] - # remove modules, doctypes, roles - for module_name in frappe.get_module_list(app_name): - for doctype in frappe.get_list("DocType", filters={"module": module_name}, - fields=["name", "issingle"]): - print("removing DocType {0}...".format(doctype.name)) + modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name})) + for module_name in modules: + print("Deleting Module '{0}'".format(module_name)) + + for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]): + print("* removing DocType '{0}'...".format(doctype.name)) if not dry_run: frappe.delete_doc("DocType", doctype.name) @@ -155,35 +156,36 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) if not doctype.issingle: drop_doctypes.append(doctype.name) - linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent']) ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes] for doctype in doctypes_with_linked_modules: for record in frappe.get_list(doctype, filters={"module": module_name}): - print("removing {0} {1}...".format(doctype, record.name)) + print("* removing {0} '{1}'...".format(doctype, record.name)) if not dry_run: frappe.delete_doc(doctype, record.name) - print("removing Module {0}...".format(module_name)) + print("* removing Module Def '{0}'...".format(module_name)) if not dry_run: frappe.delete_doc("Module Def", module_name) - remove_from_installed_apps(app_name) - if not dry_run: - # drop tables after a commit - frappe.db.commit() + remove_from_installed_apps(app_name) for doctype in set(drop_doctypes): + print("* dropping Table for '{0}'...".format(doctype)) frappe.db.sql("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") frappe.flags.in_uninstall = False + def post_install(rebuild_website=False): + from frappe.website import render + if rebuild_website: render.clear_cache() @@ -191,6 +193,7 @@ def post_install(rebuild_website=False): frappe.db.commit() frappe.clear_cache() + def set_all_patches_as_completed(app): patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt") if os.path.exists(patch_path): @@ -201,6 +204,7 @@ def set_all_patches_as_completed(app): }).insert(ignore_permissions=True) frappe.db.commit() + def init_singles(): singles = [single['name'] for single in frappe.get_all("DocType", filters={'issingle': True})] for single in singles: @@ -210,6 +214,7 @@ def init_singles(): doc.flags.ignore_validate=True doc.save() + def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): site = frappe.local.site make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port) @@ -217,6 +222,7 @@ def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db frappe.destroy() frappe.init(site, sites_path=sites_path) + def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None): frappe.create_folder(os.path.join(frappe.local.site_path)) site_file = get_site_config_path() @@ -237,6 +243,7 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N with open(site_file, "w") as f: f.write(json.dumps(site_config, indent=1, sort_keys=True)) + def update_site_config(key, value, validate=True, site_config_path=None): """Update a value in site_config""" if not site_config_path: @@ -266,9 +273,11 @@ def update_site_config(key, value, validate=True, site_config_path=None): if hasattr(frappe.local, "conf"): frappe.local.conf[key] = value + def get_site_config_path(): return os.path.join(frappe.local.site_path, "site_config.json") + def get_conf_params(db_name=None, db_password=None): if not db_name: db_name = input("Database Name: ") @@ -281,6 +290,7 @@ def get_conf_params(db_name=None, db_password=None): return {"db_name": db_name, "db_password": db_password} + def make_site_dirs(): site_public_path = os.path.join(frappe.local.site_path, 'public') site_private_path = os.path.join(frappe.local.site_path, 'private') @@ -296,6 +306,7 @@ def make_site_dirs(): if not os.path.exists(locks_dir): os.makedirs(locks_dir) + def add_module_defs(app): modules = frappe.get_module_list(app) for module in modules: @@ -304,7 +315,10 @@ def add_module_defs(app): d.module_name = module d.save(ignore_permissions=True) + def remove_missing_apps(): + import importlib + apps = ('frappe_subscription', 'shopping_cart') installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]") for app in apps: @@ -316,7 +330,10 @@ def remove_missing_apps(): installed_apps.remove(app) frappe.db.set_global("installed_apps", json.dumps(installed_apps)) + def extract_sql_gzip(sql_gz_path): + import subprocess + try: # dvf - decompress, verbose, force original_file = sql_gz_path @@ -328,7 +345,11 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file + def extract_tar_files(site_name, file_path, folder_name): + import subprocess + import shutil + # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) abs_site_path = os.path.abspath(frappe.get_site_path()) @@ -349,6 +370,7 @@ def extract_tar_files(site_name, file_path, folder_name): return tar_path + def is_downgrade(sql_file_path, verbose=False): """checks if input db backup will get downgraded on current bench""" from semantic_version import Version diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 864720174f..6b95a3f5bf 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -97,7 +97,7 @@ def backup_to_dropbox(upload_db_backup=True): if frappe.flags.create_new_backup: backup = new_backup(ignore_files=True) filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) - site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path)) + site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf)) else: filename, site_config = get_latest_backup_file() diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index 1089c6b635..76781fe47f 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -97,8 +97,8 @@ "label": "Push to Google Contacts" } ], - "modified": "2019-09-13 15:53:19.569924", - "modified_by": "himanshu@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Integrations", "name": "Google Contacts", "owner": "Administrator", diff --git a/frappe/integrations/doctype/google_drive/google_drive.json b/frappe/integrations/doctype/google_drive/google_drive.json index 7ea26dadc8..6742d9ee5d 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.json +++ b/frappe/integrations/doctype/google_drive/google_drive.json @@ -100,8 +100,8 @@ } ], "issingle": 1, - "modified": "2019-08-21 17:33:28.516614", - "modified_by": "qwe@qwe.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Integrations", "name": "Google Drive", "owner": "Administrator", diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 58f28f882a..c1c73d7726 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -191,7 +191,7 @@ def upload_system_backup_to_google_drive(): backup = new_backup() file_urls = [] file_urls.append(backup.backup_path_db) - file_urls.append(backup.site_config_backup_path) + file_urls.append(backup.backup_path_conf) if account.file_backup: file_urls.append(backup.backup_path_files) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index c59b0ddd5b..7c90d37f82 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -118,7 +118,7 @@ def backup_to_s3(): backup = new_backup(ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=True) db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) - site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path)) + site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf)) if backup_files: files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) diff --git a/frappe/migrate.py b/frappe/migrate.py index 6d64799fdd..196aa530e1 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -23,7 +23,7 @@ from frappe.search.website_search import build_index_for_all_routes def migrate(verbose=True, rebuild_website=False, skip_failing=False, skip_search_index=False): - '''Migrate all apps to the latest version, will: + '''Migrate all apps to the current version, will: - run before migrate hooks - run patches - sync doctypes (schema) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index db2b4eff85..eca2af7aff 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -335,6 +335,9 @@ class BaseDocument(object): if frappe.db.is_primary_key_violation(e): if self.meta.autoname=="hash": # hash collision? try again + frappe.flags.retry_count = (frappe.flags.retry_count or 0) + 1 + if frappe.flags.retry_count > 5: + raise self.name = None self.db_insert() return diff --git a/frappe/model/naming.py b/frappe/model/naming.py index f2c918113b..9ea5fc0ca4 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -7,6 +7,7 @@ from frappe import _ from frappe.utils import now_datetime, cint, cstr import re from six import string_types +from frappe.model import log_types def set_new_name(doc): @@ -35,7 +36,13 @@ def set_new_name(doc): elif getattr(doc.meta, "issingle", False): doc.name = doc.doctype - else: + elif getattr(doc.meta, "istable", False): + doc.name = make_autoname("hash", doc.doctype) + + if not doc.name: + set_naming_from_document_naming_rule(doc) + + if not doc.name: doc.run_method("autoname") if not doc.name and autoname: @@ -43,12 +50,15 @@ def set_new_name(doc): # if the autoname option is 'field:' and no name was derived, we need to # notify - if autoname.startswith("field:") and not doc.name: + if not doc.name and autoname.startswith("field:"): fieldname = autoname[6:] frappe.throw(_("{0} is required").format(doc.meta.get_label(fieldname))) # at this point, we fall back to name generation with the hash option - if not doc.name or autoname == "hash": + if not doc.name and autoname == "hash": + doc.name = make_autoname("hash", doc.doctype) + + if not doc.name: doc.name = make_autoname("hash", doc.doctype) doc.name = validate_name( @@ -76,6 +86,23 @@ def set_name_from_naming_options(autoname, doc): elif "#" in autoname: doc.name = make_autoname(autoname, doc=doc) +def set_naming_from_document_naming_rule(doc): + ''' + Evaluate rules based on "Document Naming Series" doctype + ''' + 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 + def set_name_by_naming_series(doc): """Sets name by the `naming_series` property""" if not doc.naming_series: diff --git a/frappe/patches.txt b/frappe/patches.txt index b2dc3fa391..acc322b164 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -306,7 +306,10 @@ frappe.patches.v13_0.add_toggle_width_in_navbar_settings frappe.patches.v13_0.rename_notification_fields frappe.patches.v13_0.remove_duplicate_navbar_items frappe.patches.v12_0.set_default_password_reset_limit +execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) frappe.patches.v13_0.set_route_for_blog_category frappe.patches.v13_0.enable_custom_script frappe.patches.v13_0.update_newsletter_content_type -frappe.patches.v13_0.delete_event_producer_and_consumer_keys \ No newline at end of file +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 diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py new file mode 100644 index 0000000000..b4ccb21ef2 --- /dev/null +++ b/frappe/patches/v13_0/web_template_set_module.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + """Set default module for standard Web Template, if none.""" + 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) + if not doc.module: + doc.module = 'Website' + doc.save() diff --git a/frappe/public/build.json b/frappe/public/build.json index 844e436e43..242cf0160a 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -243,6 +243,7 @@ "public/js/frappe/utils/energy_point_utils.js", "public/js/frappe/utils/dashboard_utils.js", "public/js/frappe/ui/chart.js", + "public/js/frappe/ui/datatable.js", "public/js/frappe/ui/driver.js", "public/js/frappe/barcode_scanner/index.js" ], diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 7b59f9da08..6281ff7f76 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -34,7 +34,7 @@ frappe.dom = { }, remove_script_and_style: function(txt) { const evil_tags = ["script", "style", "noscript", "title", "meta", "base", "head"]; - const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|')); + const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|'), 's'); if (!regex.test(txt)) { // no evil tags found, skip the DOM method entirely! return txt; diff --git a/frappe/public/js/frappe/form/controls/markdown_editor.js b/frappe/public/js/frappe/form/controls/markdown_editor.js index 81e47a0924..b134b44e9e 100644 --- a/frappe/public/js/frappe/form/controls/markdown_editor.js +++ b/frappe/public/js/frappe/form/controls/markdown_editor.js @@ -44,5 +44,9 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({ .then(() => { this.update_preview(); }); + }, + + set_disp_area(value) { + this.disp_area && $(this.disp_area).text(value); } }); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index f453b7dea3..9e6d3f0bdb 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1265,7 +1265,7 @@ 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, @@ -1273,7 +1273,7 @@ frappe.ui.form.Form = class FrappeForm { if (fname && fname.length) df = frappe.meta.get_docfield(fname[0].parent, fieldname, docname); } - if(df && df[property] != value) { + if (df && df[property] != value) { df[property] = value; refresh_field(fieldname, table_field); } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 401dac4a8a..2f86d54ab8 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -770,6 +770,10 @@ export default class Grid { as_dataurl: true, allow_multiple: false, on_success(file) { + if (file.file_obj.type !== "text/csv") { + let msg = __(`Your file could not be processed. It should be a standard CSV file.`); + frappe.throw(msg); + } var data = frappe.utils.csv_to_array(frappe.utils.get_decoded_string(file.dataurl)); // row #2 contains fieldnames; var fieldnames = data[2]; diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 733c1bea5f..827fbfdee6 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -393,11 +393,16 @@ export default class GridRow { // sync get_query field.get_query = this.grid.get_field(df.fieldname).get_query; - var field_on_change_function = field.df.onchange; - field.df.onchange = function(e) { - field_on_change_function && field_on_change_function(e); - me.grid.grid_rows[this.doc.idx - 1].refresh_field(field.df.fieldname); - }; + if (!field.df.onchange_modified) { + var field_on_change_function = field.df.onchange; + field.df.onchange = function(e) { + field_on_change_function && field_on_change_function(e); + me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname); + }; + + field.df.onchange_modified = true; + } + field.refresh(); if(field.$input) { field.$input diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 8386cb6c7e..6e3a404821 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -215,10 +215,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) { $(btn).prop("disabled", false); frappe.ui.form.is_saving = false; - if (!r.exc) { - frappe.show_alert({message: __('Saved'), indicator: 'green'}); - } - if (r) { var doc = r.docs && r.docs[0]; if (doc) { diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index ab62723161..c7fb69a2b5 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -186,7 +186,7 @@ frappe.ui.form.Toolbar = Class.extend({ }, set_indicator: function() { var indicator = frappe.get_indicator(this.frm.doc); - if (this.frm.save_disabled && [__('Saved'), __('Not Saved')].includes(indicator[0])) { + if (this.frm.save_disabled && indicator && [__('Saved'), __('Not Saved')].includes(indicator[0])) { return; } if(indicator) { @@ -272,12 +272,12 @@ frappe.ui.form.Toolbar = Class.extend({ }); } - if (frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) { + if (frappe.user_roles.includes("System Manager")) { let is_doctype_form = me.frm.doctype === 'DocType'; let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype; let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false; - if (doctype != 'DocType' && !is_doctype_custom) { + if (doctype != 'DocType' && !is_doctype_custom && me.frm.meta.issingle === 0) { this.page.add_menu_item(__("Customize"), function() { if (me.frm.meta && me.frm.meta.custom) { frappe.set_route('Form', 'DocType', doctype); diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 663850d08c..308d9bd5f8 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -31,7 +31,7 @@ $.extend(frappe.model, { {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, ], - numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], + numeric_fieldtypes: ["Int", "Float", "Currency", "Percent", "Duration"], std_fields_table: [ {fieldname:'parent', fieldtype:'Data', label:__('Parent')}, diff --git a/frappe/public/js/frappe/router_history.js b/frappe/public/js/frappe/router_history.js index fb3d09fe0b..61fc4d6b13 100644 --- a/frappe/public/js/frappe/router_history.js +++ b/frappe/public/js/frappe/router_history.js @@ -1,6 +1,6 @@ frappe.provide('frappe.route'); frappe.route_history_queue = []; -const routes_to_skip = ['Form', 'social', 'setup-wizard']; +const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder']; const save_routes = frappe.utils.debounce(() => { const routes = frappe.route_history_queue; @@ -30,7 +30,6 @@ function is_route_useful(route) { if (!route[1]) { return false; } else if ((route[0] === 'List' && !route[2]) || routes_to_skip.includes(route[0])) { - return false; } else { return true; diff --git a/frappe/public/js/frappe/ui/datatable.js b/frappe/public/js/frappe/ui/datatable.js new file mode 100644 index 0000000000..c71c285f3c --- /dev/null +++ b/frappe/public/js/frappe/ui/datatable.js @@ -0,0 +1,3 @@ +import DataTable from "frappe-datatable"; + +frappe.DataTable = DataTable; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 38c22c9c9f..b8eeefb046 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -824,8 +824,14 @@ Object.assign(frappe.utils, { }; }, - get_formatted_duration(value, duration_options) { + get_formatted_duration(value, duration_options=null) { let duration = ''; + if (!duration_options) { + duration_options = { + hide_days: 0, + hide_seconds: 0 + }; + } if (value) { let total_duration = frappe.utils.seconds_to_duration(value, duration_options); diff --git a/frappe/public/js/frappe/utils/web_template.js b/frappe/public/js/frappe/utils/web_template.js new file mode 100644 index 0000000000..64cf17b2d7 --- /dev/null +++ b/frappe/public/js/frappe/utils/web_template.js @@ -0,0 +1,71 @@ +function open_web_template_values_editor(template, current_values = {}) { + return new Promise(resolve => { + frappe.model.with_doc("Web Template", template).then((doc) => { + let d = new frappe.ui.Dialog({ + title: __("Edit Values"), + fields: get_fields(doc), + primary_action(values) { + d.hide(); + resolve(values); + }, + }); + d.set_values(current_values); + d.show(); + + d.sections.forEach((sect) => { + let fields_with_value = sect.fields_list.filter( + (field) => current_values[field.df.fieldname] + ); + + if (fields_with_value.length) { + sect.collapse(false); + } + }); + }); + }); + + function get_fields(doc) { + let normal_fields = []; + let table_fields = []; + + let current_table = null; + for (let df of doc.fields) { + if (current_table) { + current_table.fields = current_table.fields || []; + + if (df.fieldtype != 'Table Break') { + current_table.fields.push(df); + } else { + table_fields.push(df); + current_table = df; + } + } else if (df.fieldtype != 'Table Break') { + normal_fields.push(df); + } else { + table_fields.push(df); + current_table = df; + } + } + + let fields = [ + ...normal_fields, + ...table_fields.map(tf => { + let data = current_values[tf.fieldname] || []; + return { + label: tf.label, + fieldname: tf.fieldname, + fieldtype: 'Table', + fields: tf.fields.map((df, i) => ({ + ...df, + in_list_view: i <= 1, + columns: tf.fields.length == 1 ? 10 : 5 + })), + data, + get_data: () => data + }; + }) + ]; + + return fields; + } +} diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1514fcb070..1c0cd9e3b2 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1327,6 +1327,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return row .slice(standard_column_count) .map((cell, i) => { + if (cell.column.fieldtype === "Duration") { + cell.content = frappe.utils.get_formatted_duration(cell.content); + } if (include_indentation && i===0) { cell.content = ' '.repeat(row.meta.indent) + (cell.content || ''); } diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index a8eead8a3d..7174ba0d2a 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -89,11 +89,10 @@ export default class ShortcutWidget extends Widget { const label = get_label(); const buttons = $(`
{{ blogger_info.bio }}
{% endif %}
- {%- for child in page.child_items -%} --
-
- {%- if child.icon -%}
-
+
+
+ {% for group in footer_items if group.child_items %}
+ {# 2 columns to every 5 links, so 5 links get 2 columns, 5-10 links get 4 columns, and so on #}
+ {%- set cols = frappe.utils.ceil((group.child_items | len) / 5) * 2 -%}
+
+
+
+
+ {% endfor %}
-{% endfor %}
diff --git a/frappe/templates/includes/footer/footer_info.html b/frappe/templates/includes/footer/footer_info.html
new file mode 100644
index 0000000000..a186247c9a
--- /dev/null
+++ b/frappe/templates/includes/footer/footer_info.html
@@ -0,0 +1,19 @@
+
+
+
+ {% if copyright %}
+ © {{ copyright }}
+ {% endif %}
+ {% if footer_address %}
+ {% if copyright %}
+ {# powered #}
+
+ {% block powered %}
+ {% include "templates/includes/footer/footer_powered.html" %}
+ {% endblock %}
+
+
+
diff --git a/frappe/templates/includes/footer/footer_items.html b/frappe/templates/includes/footer/footer_items.html
deleted file mode 100644
index 352bde6e27..0000000000
--- a/frappe/templates/includes/footer/footer_items.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% for page in footer_items %}
-{% if not page.parent_label %}
-
- {% if not page.parent_label -%}
-
-
- {%- if page.child_items -%}
- {{ page.label }}
-
- {{ page.label }}
-
- {%- endif -%}
-
- {%- endif -%}
-
- {% endif %}
-{% endfor %}
diff --git a/frappe/templates/includes/footer/footer_links.html b/frappe/templates/includes/footer/footer_links.html
index e8bfdadb7f..a5939a9635 100644
--- a/frappe/templates/includes/footer/footer_links.html
+++ b/frappe/templates/includes/footer/footer_links.html
@@ -10,15 +10,15 @@
- {% for item in footer_items if item.label and not (item.parent_label or item.child_items) and not item.right %}
+ {%- for item in footer_items if item.label and not (item.parent_label or item.child_items) and not item.right %}
{{ footer_link(item) }}
- {% endfor %}
+ {% endfor -%}
- {% for item in footer_items if item.label and not (item.parent_label or item.child_items) and item.right %}
+ {%- for item in footer_items if item.label and not (item.parent_label or item.child_items) and item.right %}
{{ footer_link(item) }}
- {% endfor %}
+ {% endfor -%}
diff --git a/frappe/templates/includes/footer/footer_logo_extension.html b/frappe/templates/includes/footer/footer_logo_extension.html
new file mode 100644
index 0000000000..17f3218c45
--- /dev/null
+++ b/frappe/templates/includes/footer/footer_logo_extension.html
@@ -0,0 +1,16 @@
+
+
+
+ {%- if footer_logo -%}
+
+
+
+ {%- endif -%}
+
+
+ {% block extension %}
+ {% include "templates/includes/footer/footer_extension.html" %}
+ {% endblock %}
+
+
+
diff --git a/frappe/templates/includes/navbar/navbar_items.html b/frappe/templates/includes/navbar/navbar_items.html
index deffe54dbd..4e22060581 100644
--- a/frappe/templates/includes/navbar/navbar_items.html
+++ b/frappe/templates/includes/navbar/navbar_items.html
@@ -90,7 +90,7 @@
{%- if call_to_action -%} - + {{ call_to_action }} {%- endif -%} diff --git a/frappe/templates/includes/search_result.html b/frappe/templates/includes/search_result.html index a34dd84607..21a0c33374 100644 --- a/frappe/templates/includes/search_result.html +++ b/frappe/templates/includes/search_result.html @@ -1,5 +1,5 @@ {% for d in results %} -+ {%- if group.icon -%} +
{%- else -%}
- {{ child.label }}
+ {{ group.label }}
{%- endif -%}
-
-
- {%- endfor -%}
-
+
++ {%- for child in group.child_items -%} +-
+
+ {%- if child.icon -%}
+
+ {%- else -%}
+ {{ child.label }}
+ {%- endif -%}
+
+
+ {%- endfor -%}
+
+{% endif %} + {{ footer_address }} + {% endif %} +
- {%- for child in page.child_items -%} --
- {{ child.label }}
-
- {%- endfor -%}
-
- {%- else -%} -{{ d.preview }}
{{ title }}
+{{ title }}
{% endif %} {% include "templates/includes/search_result.html" %} @@ -15,7 +15,7 @@