Merge branch 'develop' into chart-source-permission

This commit is contained in:
Suraj Shetty 2020-07-01 10:45:13 +05:30 committed by GitHub
commit 77976b7bc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 500 additions and 325 deletions

View file

@ -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}
<button class="btn btn-default btn-xs margin-top" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}">
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
${__('Show Traceback')}
</button>
<div class="collapse margin-top" id="${id}">
<div class="collapse" id="${id}" style="margin-top: 15px;">
<div class="well">
<pre>${log.exception}</pre>
</div>

View file

@ -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",

View file

@ -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
}

View file

@ -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):

View file

@ -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 = {

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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(

View file

@ -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:

View file

@ -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

View file

@ -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')

View file

@ -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 = $(`
<div>
<h6 class="form-section-heading uppercase">${__('Select fields to export')}</h6>
<h6 class="form-section-heading uppercase">${section_title}</h6>
<button class="btn btn-default btn-xs" data-action="select_all">
${__('Select All')}
</button>
<button class="btn btn-default btn-xs" data-action="select_mandatory">
${for_insert ? `<button class="btn btn-default btn-xs" data-action="select_mandatory">
${__('Select Mandatory')}
</button>
</button>`: ''}
<button class="btn btn-default btn-xs" data-action="unselect_all">
${__('Unselect All')}
</button>
@ -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)') : ''}`
};

View file

@ -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
};
});
})
);
}

View file

@ -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={}) {

View file

@ -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", [`<b><a class="timeline-email-import-link copy-to-clipboard">${docinfo.document_email}</a></b>`]);
$('.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 => {

View file

@ -17,9 +17,7 @@
{% } %}
{% } %}
</div>
<div class="timeline-email-import text-muted small">
</div>
<div class="timeline-email-import text-muted small hide"></div>
<div class="timeline-items">
</div>

View file

@ -1,4 +1,6 @@
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %} {{ data.color || "" }}"
<div class="media timeline-item
{% if (data.user_content || data.template) { %} user-content {% } else { %} notification-content {% } %}
{% if (data.template) { %} show-indicator {% }%} {{ data.color || "" }}"
data-doctype="{{ data.doctype }}" data-name="{{ data.name }}" data-communication-type = "{{ data.communication_type }}">
{% if (data.user_content) { %}
<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
@ -186,7 +188,7 @@
{% } %}
{%= __("Liked by {0}", [data.fullname]) %}
</span>
{% } else if (data.comment_type == "Energy Points") { %}
{% } else if (data.comment_type == "Energy Points" || data.template) { %}
{{ data.content_html }}
{% } else { %}
<b title="{{ data.comment_by }}">{%= data.fullname %}</b>
@ -200,8 +202,11 @@
</a>
{% } %}
{% } %}
<span class="text-muted commented-on" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>
{% if (!data.template) { %}
<span class="text-muted commented-on" style="font-weight: normal;">
&ndash; {%= data.comment_on %}
</span>
{% } %}
</div>
{% } %}
</div>

View file

@ -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]);

View file

@ -10,6 +10,7 @@
</div>
<div class="col-sm-4 form-group">
<div class="filter-field"></div>
<div class="text-muted small filter-description"></div>
</div>
<div class="col-sm-2">
<div class="filter-actions">

View file

@ -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 $(`<div class="filter-tag btn-group">
<button class="btn btn-default btn-xs toggle-filter"
title="${ __("Edit Filter") }">
title="${__('Edit Filter')}">
</button>
<button class="btn btn-default btn-xs remove-filter"
title="${ __("Remove Filter") }">
title="${__('Remove Filter')}">
<i class="fa fa-remove text-muted"></i>
</button>
</div>`);
}
add_condition_help(condition) {
let $desc = this.field.desc_area;
if(!$desc) {
$desc = $('<div class="text-muted small">').appendTo(this.field.wrapper);
}
// set description
$desc.html((in_list(["in", "not in"], condition)==="in"
? __("values separated by commas")
: __("use % as wildcard"))+'</div>');
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;
}
},
};

View file

@ -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();
}

View file

@ -175,12 +175,18 @@ def getlink(doctype, name):
return '<a href="#Form/%(doctype)s/%(name)s">%(name)s</a>' % 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'
}