diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 81a7bc9705..6a922618cb 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', { }, update_primary_action(frm) { + if (frm.is_dirty()) { + frm.enable_save(); + return; + } frm.disable_save(); if (frm.doc.status !== 'Success') { if (!frm.is_new() && (frm.has_import_file())) { @@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', { }, download_template(frm) { - if ( - frm.data_exporter && - frm.data_exporter.doctype === frm.doc.reference_doctype - ) { - frm.data_exporter.exporting_for = frm.doc.import_type; - frm.data_exporter.dialog.show(); - } else { - frappe.require('/assets/js/data_import_tools.min.js', () => { - frm.data_exporter = new frappe.data_import.DataExporter( - frm.doc.reference_doctype, - frm.doc.import_type - ); - }); - } + frappe.require('/assets/js/data_import_tools.min.js', () => { + frm.data_exporter = new frappe.data_import.DataExporter( + frm.doc.reference_doctype, + frm.doc.import_type + ); + }); }, reference_doctype(frm) { @@ -301,8 +297,8 @@ frappe.ui.form.on('Data Import', { events: { remap_column(changed_map) { let template_options = JSON.parse(frm.doc.template_options || '{}'); - template_options.remap_column = template_options.remap_column || {}; - Object.assign(template_options.remap_column, changed_map); + template_options.column_to_field_map = template_options.column_to_field_map || {}; + Object.assign(template_options.column_to_field_map, changed_map); frm.set_value('template_options', JSON.stringify(template_options)); frm.save().then(() => frm.trigger('import_file')); } @@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', { .join(''); let id = frappe.dom.get_unique_id(); html = `${messages} - -
+
${log.exception}
diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index 177252ea22..8b1b6c4e07 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -119,7 +119,7 @@ { "fieldname": "import_warnings_section", "fieldtype": "Section Break", - "label": "Warnings" + "label": "Import File Errors and Warnings" }, { "fieldname": "import_warnings", @@ -127,7 +127,7 @@ "label": "Import Warnings" }, { - "depends_on": "reference_doctype", + "depends_on": "eval:!doc.__islocal", "fieldname": "download_template", "fieldtype": "Button", "label": "Download Template" @@ -159,7 +159,7 @@ "label": "Import from Google Sheets" }, { - "depends_on": "eval:doc.google_sheets_url", + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved", "fieldname": "refresh_google_sheet", "fieldtype": "Button", "label": "Refresh Google Sheet" @@ -167,7 +167,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2020-06-18 16:05:54.211034", + "modified": "2020-06-24 14:33:03.173876", "modified_by": "Administrator", "module": "Core", "name": "Data Import", diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 4761652c70..ec3cccc1b1 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ( read_xls_file_from_attached_file, ) from frappe.model import no_value_fields, table_fields as table_fieldtypes +from frappe.core.doctype.version.version import get_diff INVALID_VALUES = ("", None) MAX_ROWS_IN_PREVIEW = 10 @@ -216,14 +217,22 @@ class Importer: def update_record(self, doc): id_field = get_id_field(self.doctype) existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) - existing_doc.flags.updater_reference = { - "doctype": self.data_import.doctype, - "docname": self.data_import.name, - "label": _("via Data Import"), - } - existing_doc.update(doc) - existing_doc.save() - return existing_doc + + updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) + updated_doc.update(doc) + + if get_diff(existing_doc, updated_doc): + # update doc if there are changes + updated_doc.flags.updater_reference = { + "doctype": self.data_import.doctype, + "docname": self.data_import.name, + "label": _("via Data Import"), + } + updated_doc.save() + return updated_doc + else: + # throw if no changes + frappe.throw('No changes to update') def get_eta(self, current, total, processing_time): self.last_eta = getattr(self, "last_eta", 0) @@ -306,8 +315,9 @@ class ImportFile: ) self.column_to_field_map = self.template_options.column_to_field_map self.import_type = import_type + self.warnings = [] - self.file_doc = self.file_path = None + self.file_doc = self.file_path = self.google_sheets_url = None if isinstance(file, frappe.string_types): if frappe.db.exists("File", {"file_url": file}): self.file_doc = frappe.get_doc("File", {"file_url": file}) @@ -462,38 +472,46 @@ class ImportFile: parent_doc[table_df.fieldname].append(child_doc) doc = parent_doc - # check if there is atleast one row for mandatory table fields - meta = frappe.get_meta(self.doctype) - mandatory_table_fields = [ - df - for df in meta.fields - if df.fieldtype in table_fieldtypes - and df.reqd - and len(doc.get(df.fieldname, [])) == 0 - ] - if len(mandatory_table_fields) == 1: - self.warnings.append( - { - "row": first_row.row_number, - "message": _("There should be atleast one row for {0} table").format( - mandatory_table_fields[0].label - ), - } - ) - elif mandatory_table_fields: - fields_string = ", ".join([df.label for df in mandatory_table_fields]) - message = _("There should be atleast one row for the following tables: {0}").format( - fields_string - ) - self.warnings.append({"row": first_row.row_number, "message": message}) + + if self.import_type == INSERT: + # check if there is atleast one row for mandatory table fields + meta = frappe.get_meta(self.doctype) + mandatory_table_fields = [ + df + for df in meta.fields + if df.fieldtype in table_fieldtypes + and df.reqd + and len(doc.get(df.fieldname, [])) == 0 + ] + if len(mandatory_table_fields) == 1: + self.warnings.append( + { + "row": first_row.row_number, + "message": _("There should be atleast one row for {0} table").format( + frappe.bold(mandatory_table_fields[0].label) + ), + } + ) + elif mandatory_table_fields: + fields_string = ", ".join([df.label for df in mandatory_table_fields]) + message = _("There should be atleast one row for the following tables: {0}").format( + fields_string + ) + self.warnings.append({"row": first_row.row_number, "message": message}) return doc, rows, data[len(rows) :] def get_warnings(self): warnings = [] + + # ImportFile warnings + warnings += self.warnings + + # Column warnings for col in self.header.columns: warnings += col.warnings + # Row warnings for row in self.data: warnings += row.warnings @@ -607,7 +625,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -622,7 +640,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -635,7 +653,7 @@ class Row: { "row": self.row_number, "col": col.column_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": _("Value {0} must in {1} format").format( frappe.bold(value), frappe.bold(get_user_format(col.date_format)) ), @@ -646,7 +664,7 @@ class Row: return value def link_exists(self, value, df): - key = df.options + "::" + value + key = df.options + "::" + cstr(value) if Row.link_values_exist_map.get(key) is None: Row.link_values_exist_map[key] = frappe.db.exists(df.options, value) return Row.link_values_exist_map.get(key) @@ -755,19 +773,21 @@ class Row: class Header(Row): - def __init__(self, index, row, doctype, raw_data, column_to_field_map): + def __init__(self, index, row, doctype, raw_data, column_to_field_map=None): self.index = index self.row_number = index + 1 self.data = row self.doctype = doctype + column_to_field_map = column_to_field_map or frappe._dict() self.seen = [] self.columns = [] for j, header in enumerate(row): column_values = [get_item_at_index(r, j) for r in raw_data] + map_to_field = column_to_field_map.get(str(j)) column = Column( - j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen + j, header, self.doctype, column_values, map_to_field, self.seen ) self.seen.append(header) self.columns.append(column) @@ -824,7 +844,7 @@ class Column: self.meta = frappe.get_meta(doctype) self.parse() - self.parse_date_format() + self.validate_values() def parse(self): header_title = self.header_title @@ -897,10 +917,6 @@ class Column: self.df = df self.skip_import = skip_import - def parse_date_format(self): - if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"): - self.date_format = self.guess_date_format_for_column() - def guess_date_format_for_column(self): """ Guesses date format for a column by parsing all the values in the column, getting the date format and then returning the one which has the maximum frequency @@ -935,6 +951,26 @@ class Column: return max_occurred_date_format + def validate_values(self): + if not self.df: + return + + if self.df.fieldtype == 'Link': + # find all values that dont exist + values = list(set([v for v in self.column_values[1:] if v])) + exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})] + not_exists = list(set(values) - set(exists)) + if not_exists: + missing_values = ', '.join(not_exists) + self.warnings.append({ + 'col': self.column_number, + 'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values), + 'type': 'warning' + }) + elif self.df.fieldtype in ("Date", "Time", "Datetime"): + # guess date format + self.date_format = self.guess_date_format_for_column() + def as_dict(self): d = frappe._dict() d.index = self.index @@ -944,6 +980,9 @@ class Column: d.map_to_field = self.map_to_field d.date_format = self.date_format d.df = self.df + if hasattr(self.df, 'is_child_table_field'): + d.is_child_table_field = self.df.is_child_table_field + d.child_table_df = self.df.child_table_df d.skip_import = self.skip_import d.warnings = self.warnings return d @@ -1113,3 +1152,13 @@ def get_user_format(date_format): .replace("%m", "mm") .replace("%d", "dd") ) + +def df_as_json(df): + return { + 'fieldname': df.fieldname, + 'fieldtype': df.fieldtype, + 'label': df.label, + 'options': df.options, + 'parent': df.parent, + 'default': df.default + } diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index a5c5504db2..4ad6943e0b 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -260,9 +260,7 @@ def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [] - if timegrain == 'Daily': - result.append([start_date, 0.0]) + result = [[start_date, 0.0]] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) @@ -280,11 +278,8 @@ def get_result(data, timegrain, from_date, to_date): def get_next_expected_date(date, timegrain): next_date = None - if timegrain=='Daily': - next_date = add_to_date(date, days=1) - else: - # given date is always assumed to be the period ending date - next_date = get_period_ending(add_to_date(date, days=1), timegrain) + # given date is always assumed to be the period ending date + next_date = get_period_ending(add_to_date(date, days=1), timegrain) return getdate(next_date) def get_period_ending(date, timegrain): diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 1a300e471a..5e39998e62 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -4,13 +4,12 @@ from __future__ import unicode_literals import unittest, frappe -from frappe.utils import getdate, formatdate +from frappe.utils import getdate, formatdate, get_last_day from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get, get_period_ending) from datetime import datetime from dateutil.relativedelta import relativedelta -import calendar class TestDashboardChart(unittest.TestCase): def test_period_ending(self): @@ -53,16 +52,18 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name='Test Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) - # self.assertEqual(result.get('datasets')[0].get('values')[:-1], - # [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13]) - frappe.db.rollback() def test_empty_dashboard_chart(self): @@ -79,15 +80,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name ='Test Empty Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -111,15 +117,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -141,7 +152,7 @@ class TestDashboardChart(unittest.TestCase): chart_type = 'Group By', document_type = 'ToDo', group_by_based_on = 'status', - filters_json = '{}', + filters_json = '[]', )).insert() result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) @@ -168,7 +179,7 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Daily', from_date = datetime(2019, 1, 6), to_date = datetime(2019, 1, 11), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -200,22 +211,24 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Weekly', from_date = datetime(2018, 12, 30), to_date = datetime(2019, 1, 15), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0]) - self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) + self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) frappe.db.rollback() def insert_test_records(): - create_new_communication(datetime(2019, 1, 10), 100) + create_new_communication(datetime(2018, 12, 30), 50) + create_new_communication(datetime(2019, 1, 4), 100) create_new_communication(datetime(2019, 1, 6), 200) create_new_communication(datetime(2019, 1, 7), 400) create_new_communication(datetime(2019, 1, 8), 300) + create_new_communication(datetime(2019, 1, 10), 100) def create_new_communication(date, rating): communication = { diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index f24f33df07..cacbd3c633 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None): "shared": frappe.share.get_users(doc.doctype, doc.name), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), + "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), "tags": get_tags(doc.doctype, doc.name), @@ -277,3 +278,14 @@ def get_document_email(doctype, name): def get_automatic_email_link(): return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") + +def get_additional_timeline_content(doctype, docname): + contents = [] + hooks = frappe.get_hooks().get('additional_timeline_content', {}) + methods_for_all_doctype = hooks.get('*', []) + methods_for_current_doctype = hooks.get(doctype, []) + + for method in methods_for_all_doctype + methods_for_current_doctype: + contents.extend(frappe.get_attr(method)(doctype, docname) or []) + + return contents \ No newline at end of file diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ce512de276..8bffc108b9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -347,7 +347,7 @@ def flush(from_test=False): if not smtpserver: smtpserver = SMTPServer() smtpserver_dict[email.sender] = smtpserver - + if from_test: send_one(email.name, smtpserver, auto_commit) else: @@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): where name=%s for update''', email, as_dict=True) - + if len(email): email = email[0] else: return - + recipients_list = frappe.db.sql('''select name, recipient, status from `tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1) @@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): if email.communication: frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) + email_sent_to_any_recipient = None + try: message = None diff --git a/frappe/hooks.py b/frappe/hooks.py index c207e61de5..1f209f00a2 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe" app_license = "MIT" app_logo_url = '/assets/frappe/images/frappe-framework-logo.png' -develop_version = '12.x.x-develop' +develop_version = '13.x.x-develop' app_email = "info@frappe.io" 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 6cfd3646b2..c8b007ba7b 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -19,6 +19,9 @@ from botocore.exceptions import ClientError class S3BackupSettings(Document): def validate(self): + if not self.enabled: + return + if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' conn = boto3.client( diff --git a/frappe/model/document.py b/frappe/model/document.py index 30d3442954..ea693167f8 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -830,7 +830,7 @@ class Document(BaseDocument): def run_notifications(self, method): """Run notifications for this method""" - if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: return if self.flags.notifications_executed==None: diff --git a/frappe/patches.txt b/frappe/patches.txt index a03d31918b..f883d2a7bb 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -289,4 +289,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field") execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options -frappe.patches.v13_0.replace_old_data_import +frappe.patches.v13_0.replace_old_data_import # 2020-06-24 diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py index 1c00ae5f34..f3eed6253c 100644 --- a/frappe/patches/v13_0/replace_old_data_import.py +++ b/frappe/patches/v13_0/replace_old_data_import.py @@ -6,9 +6,11 @@ import frappe def execute(): + if not frappe.db.exists("DocType", "Data Import Beta"): + return + + frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`") frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy') frappe.db.commit() frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`") - frappe.reload_doc("core", "doctype", "data_import") - frappe.get_doc("DocType", "Data Import").on_update() - frappe.delete_doc_if_exists("DocType", "Data Import Beta") + frappe.rename_doc('DocType', 'Data Import Beta', 'Data Import') diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index 735237189d..f6af338235 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter { this.dialog = new frappe.ui.Dialog({ title: __('Export Data'), fields: [ - { - fieldtype: 'Select', - fieldname: 'exporting_for', - label: __('Exporting For'), - options: [ - { - label: __('Insert New Records'), - value: 'Insert New Records' - }, - { - label: __('Update Existing Records'), - value: 'Update Existing Records' - } - ], - change: () => { - let exporting_for = this.dialog.get_value('exporting_for'); - this.dialog.set_value( - 'export_records', - exporting_for === 'Insert New Records' ? 'blank_template' : 'all' - ); - - // Force ID field to be exported when updating existing records - let id_field = this.dialog.get_field(this.doctype).options[0]; - if (id_field.value === 'name' && id_field.$checkbox) { - id_field.$checkbox - .find('input') - .prop('disabled', exporting_for === 'Update Existing Records'); - } - } - }, { fieldtype: 'Select', fieldname: 'export_records', @@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter { value: 'blank_template' } ], - default: 'blank_template', + default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all', change: () => { this.update_record_count_message(); } @@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter { on_page_show: () => this.select_mandatory() }); - if (this.exporting_for) { - this.dialog.set_value('exporting_for', this.exporting_for); - } - this.make_filter_area(); this.make_select_all_buttons(); this.update_record_count_message(); @@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter { } make_select_all_buttons() { + let for_insert = this.exporting_for === 'Insert New Records'; + let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update'); let $select_all_buttons = $(`
-
${__('Select fields to export')}
+
${section_title}
- + `: ''} @@ -285,11 +253,9 @@ frappe.data_import.DataExporter = class DataExporter { } get_filters() { - return this.filter_group.get_filters().reduce((acc, filter) => { - return Object.assign(acc, { - [filter[1]]: [filter[2], filter[3]] - }); - }, {}); + return this.filter_group.get_filters().map(filter => { + return filter.slice(0, 4); + }); } get_multicheck_options(doctype, child_fieldname = null) { @@ -308,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter { ? this.column_map[child_fieldname] : this.column_map[doctype]; + let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname) + || (df.reqd && this.exporting_for == 'Insert New Records'); + return fields .filter(df => { if (autoname_field && df.fieldname === autoname_field.fieldname) { @@ -323,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter { return { label, value: df.fieldname, - danger: df.reqd, + danger: is_field_mandatory(df), checked: false, description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}` }; diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 7cf8431456..4edcb87aeb 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview { let fieldname; if (!df) { fieldname = null; + } else if (col.map_to_field) { + fieldname = col.map_to_field; + } else if (col.is_child_table_field) { + fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`; } else { - fieldname = - df.parent === this.doctype - ? df.fieldname - : `${df.parent}:${df.fieldname}`; + fieldname = df.fieldname; } return [ { @@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview { label: __("Don't Import"), value: "Don't Import" } - ].concat(column_picker_fields.get_fields_as_options()), + ].concat(get_fields_as_options(this.doctype, column_picker_fields)), default: fieldname || "Don't Import", change() { changed.push(i); @@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview { }); } }; + +function get_fields_as_options(doctype, column_map) { + let keys = [doctype]; + frappe.meta.get_table_fields(doctype).forEach(df => { + keys.push(df.fieldname); + }); + // flatten array + return [].concat( + ...keys.map(key => { + return column_map[key].map(df => { + let label = df.label; + let value = df.fieldname; + if (doctype !== key) { + let table_field = frappe.meta.get_docfield(doctype, key); + label = `${df.label} (${table_field.label})`; + value = `${table_field.fieldname}.${df.fieldname}`; + } + return { + label, + value, + description: value + }; + }); + }) + ); +} \ No newline at end of file diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 1b6fb0e438..cf716c67e5 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -91,12 +91,26 @@ frappe.db = { }); }, count: function(doctype, args={}) { - return new Promise(resolve => { - frappe.call({ - method: 'frappe.client.get_count', - type: 'GET', - args: Object.assign(args, { doctype }) - }).then(r => resolve(r.message)); + let filters = args.filters || {}; + const with_child_table_filter = Array.isArray(filters) && filters.some(filter => { + return filter[0] !== doctype; + }); + + const fields = [ + // cannot break this line as it adds extra \n's and \t's which breaks the query + `count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count` + ]; + + return frappe.call({ + type: 'GET', + method: 'frappe.desk.reportview.get', + args: { + doctype, + filters, + fields, + } + }).then(r => { + return r.message.values[0][0]; }); }, get_link_options(doctype, txt = '', filters={}) { diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 7821a04c50..84f34d4757 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -120,9 +120,11 @@ frappe.ui.form.Timeline = class Timeline { display_automatic_link_email() { let docinfo = this.frm.get_docinfo(); - if (docinfo.document_email){ + if (docinfo.document_email) { let link = __("Send an email to {0} to link it here", [`${docinfo.document_email}`]); - $('.timeline-email-import').html(link); + const email_link = $('.timeline-email-import'); + email_link.removeClass('hide'); + email_link.html(link); } } @@ -180,12 +182,15 @@ frappe.ui.form.Timeline = class Timeline { // append energy point logs timeline = timeline.concat(this.get_energy_point_logs()); + // custom contents + timeline = timeline.concat(this.get_additional_timeline_content()); + // append milestones timeline = timeline.concat(this.get_milestones()); // sort timeline - .filter(a => a.content) + .filter(a => a.content || a.template) .sort((b, c) => me.compare_dates(b, c)) .forEach(d => { d.frm = me.frm; @@ -407,7 +412,10 @@ frappe.ui.form.Timeline = class Timeline { c.original_content = c.content; c.content = frappe.utils.toggle_blockquote(c.content); } - if(!frappe.utils.is_html(c.content)) { + + if (c.template) { + c.content_html = frappe.render_template(c.template, c.template_data); + } else if (!frappe.utils.is_html(c.content)) { c.content_html = frappe.markdown(__(c.content)); } else { c.content_html = c.content; @@ -529,6 +537,10 @@ frappe.ui.form.Timeline = class Timeline { return energy_point_logs; } + get_additional_timeline_content() { + return this.frm.get_docinfo().additional_timeline_content || []; + } + get_milestones() { let milestones = this.frm.get_docinfo().milestones; milestones.map(log => { diff --git a/frappe/public/js/frappe/form/templates/timeline.html b/frappe/public/js/frappe/form/templates/timeline.html index 5600f9384f..0a8a631142 100644 --- a/frappe/public/js/frappe/form/templates/timeline.html +++ b/frappe/public/js/frappe/form/templates/timeline.html @@ -17,9 +17,7 @@ {% } %} {% } %}
-
- -
+
diff --git a/frappe/public/js/frappe/form/templates/timeline_item.html b/frappe/public/js/frappe/form/templates/timeline_item.html index 1442374e24..9cb8499771 100755 --- a/frappe/public/js/frappe/form/templates/timeline_item.html +++ b/frappe/public/js/frappe/form/templates/timeline_item.html @@ -1,4 +1,6 @@ -
{% if (data.user_content) { %} - {% } else if (data.comment_type == "Energy Points") { %} + {% } else if (data.comment_type == "Energy Points" || data.template) { %} {{ data.content_html }} {% } else { %} {%= data.fullname %} @@ -200,8 +202,11 @@ {% } %} {% } %} - - – {%= data.comment_on %} + {% if (!data.template) { %} + + – {%= data.comment_on %} + + {% } %}
{% } %}
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 9e1ba1b9bd..8da73b0dec 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -760,26 +760,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let current_count = this.data.length; let count_without_children = this.data.uniqBy(d => d.name).length; - const filters = this.get_filters_for_args(); - const with_child_table_filter = filters.some(filter => { - return filter[0] !== this.doctype; - }); - - const fields = [ - // cannot break this line as it adds extra \n's and \t's which breaks the query - `count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count` - ]; - - return frappe.call({ - type: 'GET', - method: this.method, - args: { - doctype: this.doctype, - filters, - fields, - } - }).then(r => { - this.total_count = r.message.values[0][0] || current_count; + return frappe.db.count(this.doctype, { + filters: this.get_filters_for_args() + }).then(total_count => { + this.total_count = total_count || current_count; let str = __('{0} of {1}', [current_count, this.total_count]); if (count_without_children !== current_count) { str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]); diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html index 3908c63fa1..f6618a2107 100644 --- a/frappe/public/js/frappe/ui/filters/edit_filter.html +++ b/frappe/public/js/frappe/ui/filters/edit_filter.html @@ -10,6 +10,7 @@
+
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 37eab50957..5e41ed645e 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -13,26 +13,26 @@ frappe.ui.Filter = class { set_conditions() { this.conditions = [ - ["=", __("Equals")], - ["!=", __("Not Equals")], - ["like", __("Like")], - ["not like", __("Not Like")], - ["in", __("In")], - ["not in", __("Not In")], - ["is", __("Is")], - [">", ">"], - ["<", "<"], - [">=", ">="], - ["<=", "<="], - ["Between", __("Between")], - ["Timespan", __("Timespan")], + ['=', __('Equals')], + ['!=', __('Not Equals')], + ['like', __('Like')], + ['not like', __('Not Like')], + ['in', __('In')], + ['not in', __('Not In')], + ['is', __('Is')], + ['>', '>'], + ['<', '<'], + ['>=', '>='], + ['<=', '<='], + ['Between', __('Between')], + ['Timespan', __('Timespan')], ]; this.nested_set_conditions = [ - ["descendants of", __("Descendants Of")], - ["not descendants of", __("Not Descendants Of")], - ["ancestors of", __("Ancestors Of")], - ["not ancestors of", __("Not Ancestors Of")], + ['descendants of', __('Descendants Of')], + ['not descendants of', __('Not Descendants Of')], + ['ancestors of', __('Ancestors Of')], + ['not ancestors of', __('Not Ancestors Of')], ]; this.conditions.push(...this.nested_set_conditions); @@ -42,10 +42,10 @@ frappe.ui.Filter = class { Datetime: ['like', 'not like'], Data: ['Between', 'Timespan'], Select: ['like', 'not like', 'Between', 'Timespan'], - Link: ["Between", 'Timespan', '>', '<', '>=', '<='], - Currency: ["Between", 'Timespan'], - Color: ["Between", 'Timespan'], - Check: this.conditions.map(c => c[0]).filter(c => c !== '=') + Link: ['Between', 'Timespan', '>', '<', '>=', '<='], + Currency: ['Between', 'Timespan'], + Color: ['Between', 'Timespan'], + Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='), }; } @@ -65,10 +65,11 @@ frappe.ui.Filter = class { } make() { - this.filter_edit_area = $(frappe.render_template("edit_filter", { - conditions: this.conditions - })) - .appendTo(this.parent.find('.filter-edit-area')); + this.filter_edit_area = $( + frappe.render_template('edit_filter', { + conditions: this.conditions, + }) + ).appendTo(this.parent.find('.filter-edit-area')); this.make_select(); this.set_events(); @@ -82,41 +83,51 @@ frappe.ui.Filter = class { filter_fields: this.filter_fields, select: (doctype, fieldname) => { this.set_field(doctype, fieldname); - } + }, }); - if(this.fieldname) { + if (this.fieldname) { this.fieldselect.set_value(this.doctype, this.fieldname); } } set_events() { - this.filter_edit_area.find("a.remove-filter").on("click", () => { + this.filter_edit_area.find('a.remove-filter').on('click', () => { this.remove(); }); - this.filter_edit_area.find(".set-filter-and-run").on("click", () => { - this.filter_edit_area.removeClass("new-filter"); + this.filter_edit_area.find('.set-filter-and-run').on('click', () => { + this.filter_edit_area.removeClass('new-filter'); this.on_change(); this.update_filter_tag(); }); this.filter_edit_area.find('.condition').change(() => { - if(!this.field) return; + if (!this.field) return; let condition = this.get_condition(); let fieldtype = null; - if(["in", "like", "not in", "not like"].includes(condition)) { + if (['in', 'like', 'not in', 'not like'].includes(condition)) { fieldtype = 'Data'; this.add_condition_help(condition); + } else { + this.filter_edit_area.find('.filter-description').empty(); } - if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) { + if ( + ['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && + ['in', 'not in'].includes(condition) + ) { fieldtype = 'MultiSelect'; } - this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition); + this.set_field( + this.field.df.parent, + this.field.df.fieldname, + fieldtype, + condition + ); }); } @@ -129,12 +140,12 @@ frappe.ui.Filter = class { setup_state(is_new) { let promise = Promise.resolve(); if (is_new) { - this.filter_edit_area.addClass("new-filter"); + this.filter_edit_area.addClass('new-filter'); } else { promise = this.update_filter_tag(); } - if(this.hidden) { + if (this.hidden) { promise.then(() => this.$filter_tag.hide()); } } @@ -164,13 +175,13 @@ frappe.ui.Filter = class { set_values(doctype, fieldname, condition, value) { // presents given (could be via tags!) if (this.set_field(doctype, fieldname) === false) { - return + return; } - if(this.field.df.original_type==='Check') { - value = (value==1) ? 'Yes' : 'No'; + if (this.field.df.original_type === 'Check') { + value = value == 1 ? 'Yes' : 'No'; } - if(condition) this.set_condition(condition, true); + if (condition) this.set_condition(condition, true); // set value can be asynchronous, so update_filter_tag should happen after field is set this._filter_value_set = Promise.resolve(); @@ -190,11 +201,13 @@ frappe.ui.Filter = class { set_field(doctype, fieldname, fieldtype, condition) { // set in fieldname (again) let cur = {}; - if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k]; + if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k]; - let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname]; + let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[ + fieldname + ]; - if(!original_docfield) { + if (!original_docfield) { console.warn(`Field ${fieldname} is not selectable.`); this.remove(); return false; @@ -214,8 +227,13 @@ frappe.ui.Filter = class { // called when condition is changed, // don't change if all is well - if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && - df.parent == cur.parent && df.options == cur.options) { + if ( + this.field && + cur.fieldname == fieldname && + df.fieldtype == cur.fieldtype && + df.parent == cur.parent && + df.options == cur.options + ) { return; } @@ -223,20 +241,25 @@ frappe.ui.Filter = class { this.fieldselect.selected_doctype = doctype; this.fieldselect.selected_fieldname = fieldname; - if (this.filters_config && this.filters_config[condition] - && this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) { + if ( + this.filters_config && + this.filters_config[condition] && + this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype) + ) { let args = {}; if (this.filters_config[condition].depends_on) { const field_name = this.filters_config[condition].depends_on; const filter_value = this.base_list.get_filter_value(field_name); args[field_name] = filter_value; } - frappe.xcall(this.filters_config[condition].get_field, args).then(field => { - df.fieldtype = field.fieldtype; - df.options = field.options; - df.fieldname = fieldname; - this.make_field(df, cur.fieldtype); - }); + frappe + .xcall(this.filters_config[condition].get_field, args) + .then(field => { + df.fieldtype = field.fieldtype; + df.options = field.options; + df.fieldname = fieldname; + this.make_field(df, cur.fieldtype); + }); } else { this.make_field(df, cur.fieldtype); } @@ -255,16 +278,18 @@ frappe.ui.Filter = class { f.refresh(); this.field = f; - if(old_text && f.fieldtype===old_fieldtype) { + if (old_text && f.fieldtype === old_fieldtype) { this.field.set_value(old_text); } // run on enter - $(this.field.wrapper).find(':input').keydown(e => { - if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') { - this.on_change(); - } - }); + $(this.field.wrapper) + .find(':input') + .keydown(e => { + if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') { + this.on_change(); + } + }); } get_value() { @@ -273,7 +298,7 @@ frappe.ui.Filter = class { this.field.df.fieldname, this.get_condition(), this.get_selected_value(), - this.hidden + this.hidden, ]; } get_selected_value() { @@ -284,90 +309,101 @@ frappe.ui.Filter = class { return this.filter_edit_area.find('.condition').val(); } - set_condition(condition, trigger_change=false) { + set_condition(condition, trigger_change = false) { let $condition_field = this.filter_edit_area.find('.condition'); $condition_field.val(condition); - if(trigger_change) $condition_field.change(); - + if (trigger_change) $condition_field.change(); } make_tag() { if (!this.field) return; - this.$filter_tag = this.get_filter_tag_element() - .insertAfter(this.parent.find(".active-tag-filters .clear-filters")); + this.$filter_tag = this.get_filter_tag_element().insertAfter( + this.parent.find('.active-tag-filters .clear-filters') + ); this.set_filter_button_text(); this.bind_tag(); } bind_tag() { - this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this)); + this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this)); - let filter_button = this.$filter_tag.find(".toggle-filter"); - filter_button.on("click", () => { - filter_button.closest('.tag-filters-area').find('.filter-edit-area').show(); + let filter_button = this.$filter_tag.find('.toggle-filter'); + filter_button.on('click', () => { + filter_button + .closest('.tag-filters-area') + .find('.filter-edit-area') + .show(); this.filter_edit_area.toggle(); }); } set_filter_button_text() { - this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text()); + this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text()); } get_filter_button_text() { - let value = this.utils.get_formatted_value(this.field, this.get_selected_value()); - return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`; + let value = this.utils.get_formatted_value( + this.field, + this.get_selected_value() + ); + return `${__(this.field.df.label)} ${__(this.get_condition())} ${__( + value + )}`; } get_filter_tag_element() { return $(`
`); } add_condition_help(condition) { - let $desc = this.field.desc_area; - if(!$desc) { - $desc = $('
').appendTo(this.field.wrapper); - } - // set description - $desc.html((in_list(["in", "not in"], condition)==="in" - ? __("values separated by commas") - : __("use % as wildcard"))+'
'); + const description = ['in', 'not in'].includes(condition) + ? __('values separated by commas') + : __('use % as wildcard'); + + this.filter_edit_area.find('.filter-description').html(description); } hide_invalid_conditions(fieldtype, original_type) { - let invalid_conditions = this.invalid_condition_map[original_type] - || this.invalid_condition_map[fieldtype] || []; + let invalid_conditions = + this.invalid_condition_map[original_type] || + this.invalid_condition_map[fieldtype] || + []; for (let condition of this.conditions) { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle( - !invalid_conditions.includes(condition[0]) - ); + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(!invalid_conditions.includes(condition[0])); } } toggle_nested_set_conditions(df) { - let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options); - this.nested_set_conditions.forEach(condition => { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition); + let show_condition = + df.fieldtype === 'Link' && + frappe.boot.nested_set_doctypes.includes(df.options); + this.nested_set_conditions.forEach((condition) => { + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(show_condition); }); } }; frappe.ui.filter_utils = { get_formatted_value(field, value) { - if(field.df.fieldname==="docstatus") { - value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; - } else if(field.df.original_type==="Check") { - value = {0:"No", 1:"Yes"}[cint(value)]; + if (field.df.fieldname === 'docstatus') { + value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value; + } else if (field.df.original_type === 'Check') { + value = { 0: 'No', 1: 'Yes' }[cint(value)]; } - return frappe.format(value, field.df, {only_value: 1}); + return frappe.format(value, field.df, { only_value: 1 }); }, get_selected_value(field, condition) { @@ -382,7 +418,7 @@ frappe.ui.filter_utils = { } if (field.df.original_type == 'Check') { - val = (val=='Yes' ? 1 :0); + val = val == 'Yes' ? 1 : 0; } if (condition.indexOf('like', 'not like') !== -1) { @@ -390,12 +426,13 @@ frappe.ui.filter_utils = { if (val && !(val.startsWith('%') || val.endsWith('%'))) { val = '%' + val + '%'; } - } else if (in_list(["in", "not in"], condition)) { + } else if (in_list(['in', 'not in'], condition)) { if (val) { - val = val.split(',').map(v => strip(v)); + val = val.split(',').map((v) => strip(v)); } - } if (val === '%') { - val = ""; + } + if (val === '%') { + val = ''; } return val; @@ -404,7 +441,7 @@ frappe.ui.filter_utils = { get_default_condition(df) { if (df.fieldtype == 'Data') { return 'like'; - } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ + } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') { return 'Between'; } else { return '='; @@ -413,44 +450,73 @@ frappe.ui.filter_utils = { set_fieldtype(df, fieldtype, condition) { // reset - if(df.original_type) - df.fieldtype = df.original_type; - else - df.original_type = df.fieldtype; + if (df.original_type) df.fieldtype = df.original_type; + else df.original_type = df.fieldtype; - df.description = ''; df.reqd = 0; + df.description = ''; + df.reqd = 0; df.ignore_link_validation = true; // given - if(fieldtype) { + if (fieldtype) { df.fieldtype = fieldtype; return; } // scrub - if(df.fieldname=="docstatus") { - df.fieldtype="Select", - df.options=[ - {value:0, label:__("Draft")}, - {value:1, label:__("Submitted")}, - {value:2, label:__("Cancelled")} + 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='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', - 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { + } else if (df.fieldtype == 'Check') { + df.fieldtype = 'Select'; + df.options = 'No\nYes'; + } 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' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) { + } else if ( + df.fieldtype == 'Link' && + [ + '=', + '!=', + 'descendants of', + 'ancestors of', + 'not descendants of', + 'not ancestors of', + ].indexOf(condition) == -1 + ) { df.fieldtype = 'Data'; } - if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { + if ( + df.fieldtype === 'Data' && + (df.options || '').toLowerCase() === 'email' + ) { df.options = null; } - if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ + if ( + condition == 'Between' && + (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') + ) { df.fieldtype = 'DateRange'; } - if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) { + if ( + condition == 'Timespan' && + ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) + ) { df.fieldtype = 'Select'; df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']); } @@ -466,15 +532,15 @@ frappe.ui.filter_utils = { get_timespan_options(periods) { const period_map = { - 'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'], - 'Today': null, - 'This': ['Week', 'Month', 'Quarter', 'Year'], - 'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year'] + Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], + Today: null, + This: ['Week', 'Month', 'Quarter', 'Year'], + Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], }; let options = []; - periods.forEach(period => { + periods.forEach((period) => { if (period_map[period]) { - period_map[period].forEach(p => { + period_map[period].forEach((p) => { options.push({ label: __(`{0} {1}`, [period, p]), value: `${period.toLowerCase()} ${p.toLowerCase()}`, @@ -488,5 +554,5 @@ frappe.ui.filter_utils = { } }); return options; - } + }, }; diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index cd391c1f10..8d01cd6dd7 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -349,6 +349,9 @@ h6.uppercase, .h6.uppercase { .form-section { padding: 15px 7px; } + .hide-border { + padding-top: 0; + } } .help ol { @@ -573,7 +576,13 @@ h6.uppercase, .h6.uppercase { margin-left: 5px; } - .media-body:after, .media-body:before { + .media-body { + .left-arrow; + } +} + +.left-arrow { + &::after, &::before { right: 100%; top: 15px; border: solid transparent; @@ -584,13 +593,13 @@ h6.uppercase, .h6.uppercase { pointer-events: none; } - .media-body:after { + &::after { border-color: rgba(136, 183, 213, 0); border-right-color: #fafbfc; border-width: 6px; margin-top: -6px; } - .media-body:before { + &::before { border-color: rgba(194, 225, 245, 0); border-right-color: @border-color; border-width: 7px; @@ -638,6 +647,18 @@ h6.uppercase, .h6.uppercase { top: 5px; } +.timeline-item.user-content.show-indicator { + position: relative; + .media-body { + margin-left: 50px; + } + &::before { + .timeline-indicator(); + left: 13px; + top: 13px; + } +} + .timeline-item.notification-content::before { .timeline-indicator(); } diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py index 1d326bd1c5..f73f80582a 100644 --- a/frappe/utils/csvutils.py +++ b/frappe/utils/csvutils.py @@ -175,12 +175,18 @@ def getlink(doctype, name): return '%(name)s' % locals() def get_csv_content_from_google_sheets(url): + # https://docs.google.com/spreadsheets/d/{sheetid}}/edit#gid={gid} validate_google_sheets_url(url) - + # get gid, defaults to first sheet + if "gid=" in url: + gid = url.rsplit('gid=', 1)[1] + else: + gid = 0 # remove /edit path url = url.rsplit('/edit', 1)[0] - # add /export path, defaults to first sheet - url = url + '/export?format=csv&gid=0' + # add /export path, + url = url + '/export?format=csv&gid={0}'.format(gid) + headers = { 'Accept': 'text/csv' }