Merge branch 'develop' of https://github.com/frappe/frappe into rebrand-ui

This commit is contained in:
Suraj Shetty 2020-08-10 17:09:20 +05:30
commit 4863d511ab
120 changed files with 1502 additions and 1645 deletions

5
.gitignore vendored
View file

@ -188,4 +188,7 @@ typings/
# cypress
cypress/screenshots
cypress/videos
cypress/videos
# JetBrains IDEs
.idea/

View file

@ -153,6 +153,7 @@ def init(site, sites_path=None, new_site=False):
local.site = site
local.sites_path = sites_path
local.site_path = os.path.join(sites_path, site)
local.all_apps = None
local.request_ip = None
local.response = _dict({"docs":[]})
@ -231,8 +232,7 @@ def get_site_config(sites_path=None, site_path=None):
if os.path.exists(site_config):
config.update(get_file_json(site_config))
elif local.site and not local.flags.new_site:
print("Site {0} does not exist".format(local.site))
sys.exit(1)
raise IncorrectSitePath("{0} does not exist".format(local.site))
return _dict(config)
@ -300,7 +300,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None):
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -310,6 +310,8 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
"""
from frappe.utils import encode
@ -367,6 +369,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
if primary_action:
out.primary_action = primary_action
if wide:
out.wide = wide
message_log.append(json.dumps(out))
if raise_exception and hasattr(raise_exception, '__name__'):
@ -388,12 +393,12 @@ def clear_last_message():
if len(local.message_log) > 0:
local.message_log = local.message_log[:-1]
def throw(msg, exc=ValidationError, title=None, is_minimizable=None):
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None):
"""Throw execption and show message (`msgprint`).
:param msg: Message.
:param exc: Exception class. Default `frappe.ValidationError`"""
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable)
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide)
def emit_js(js, user=False, **kwargs):
if user == False:
@ -436,12 +441,8 @@ def get_roles(username=None):
"""Returns roles of current user."""
if not local.session:
return ["Guest"]
if username:
import frappe.permissions
return frappe.permissions.get_roles(username)
else:
return get_user().get_roles()
import frappe.permissions
return frappe.permissions.get_roles(username or local.session.user)
def get_request_header(key, default=None):
"""Return HTTP request header.
@ -921,10 +922,13 @@ def get_installed_apps(sort=False, frappe_last=False):
if not db:
connect()
if not local.all_apps:
local.all_apps = get_all_apps(True)
installed = json.loads(db.get_global("installed_apps") or "[]")
if sort:
installed = [app for app in get_all_apps(True) if app in installed]
installed = [app for app in local.all_apps if app in installed]
if frappe_last:
if 'frappe' in installed:
@ -1559,10 +1563,10 @@ def get_doctype_app(doctype):
loggers = {}
log_level = None
def logger(module=None, with_more_info=False):
def logger(module=None, with_more_info=False, allow_site=True, filter=None, max_size=100_000, file_count=20):
'''Returns a python logger that uses StreamHandler'''
from frappe.utils.logger import get_logger
return get_logger(module=module, with_more_info=with_more_info)
return get_logger(module=module, with_more_info=with_more_info, allow_site=allow_site, filter=filter, max_size=max_size, file_count=file_count)
def log_error(message=None, title=_("Error")):
'''Log error to Error Log'''
@ -1707,3 +1711,7 @@ def mock(type, size=1, locale='en'):
from frappe.chat.util import squashify
return squashify(results)
def validate_and_sanitize_search_inputs(fn):
from frappe.desk.search import validate_and_sanitize_search_inputs as func
return func(fn)

View file

@ -99,15 +99,16 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
frappe.logger("frappe.web").info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
frappe.logger("frappe.web", allow_site=frappe.local.site).info({
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
"scheme": getattr(request, "scheme", "NOTFOUND"),
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})
if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())
@ -256,9 +257,11 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
'SERVER_NAME': 'localhost:8000'
}
log = logging.getLogger('werkzeug')
log.propagate = False
in_test_env = os.environ.get('CI')
if in_test_env:
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
run_simple('0.0.0.0', int(port), application,

View file

@ -338,7 +338,7 @@ class CookieManager:
self.set_cookie("country", frappe.session.session_country)
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
if not secure:
if not secure and hasattr(frappe.local, 'request'):
secure = frappe.local.request.scheme == "https"
self.cookies[key] = {
"value": value,

View file

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,

View file

@ -374,6 +374,7 @@ def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, e
# method for reference_doctype filter
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
res = frappe.db.get_all('Property Setter', {
'property': 'allow_auto_repeat',

View file

@ -27,6 +27,11 @@ def get_data():
"name": "Stripe Settings",
"description": _("Stripe payment gateway settings"),
},
{
"type": "doctype",
"name": "Paytm Settings",
"description": _("Paytm payment gateway settings"),
},
]
},
{

View file

@ -146,6 +146,8 @@ def delete_contact_and_address(doctype, docname):
if len(doc.links)==1:
doc.delete()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, filters):
if not txt: txt = ""

View file

@ -230,6 +230,8 @@ def get_company_address(company):
return ret
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def address_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
@ -237,16 +239,17 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
link_name = filters.pop('link_name')
condition = ""
for fieldname, value in iteritems(filters):
condition += " and {field}={value}".format(
field=fieldname,
value=value
)
meta = frappe.get_meta("Address")
for fieldname, value in iteritems(filters):
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
condition += " and {field}={value}".format(
field=fieldname,
value=frappe.db.escape(value))
searchfields = meta.get_search_fields()
if searchfield:
if searchfield and (meta.get_field(searchfield)\
or searchfield in frappe.db.DEFAULT_COLUMNS):
searchfields.append(searchfield)
search_condition = ''
@ -289,4 +292,4 @@ def get_condensed_address(doc):
return ", ".join([doc.get(d) for d in fields if doc.get(d)])
def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)
frappe.db.set_value('Address', address, field, 0)

View file

@ -182,19 +182,18 @@ def update_contact(doc, method):
contact.flags.ignore_mandatory = True
contact.save(ignore_permissions=True)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def contact_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
if not frappe.get_meta("Contact").get_field(searchfield)\
or searchfield not in frappe.db.DEFAULT_COLUMNS:
return []
link_doctype = filters.pop('link_doctype')
link_name = filters.pop('link_name')
condition = ""
for fieldname, value in iteritems(filters):
condition += " and {field}={value}".format(
field=fieldname,
value=value
)
return frappe.db.sql("""select
`tabContact`.name, `tabContact`.first_name, `tabContact`.last_name
from
@ -209,9 +208,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
order by
if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999),
`tabContact`.idx desc, `tabContact`.name
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
key=searchfield), {
limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), {
'txt': '%' + txt + '%',
'_txt': txt.replace("%", ""),
'start': start,

View file

@ -236,7 +236,7 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
if doc.sender:
# combine for sending to get the format 'Jane <jane@example.com>'
doc.sender = formataddr([doc.sender_full_name, doc.sender])
doc.sender = get_formatted_email(doc.sender_full_name, mail=doc.sender)
doc.attachments = []

View file

@ -317,6 +317,7 @@ frappe.ui.form.on('Data Import', {
},
show_import_warnings(frm, preview_data) {
let columns = preview_data.columns;
let warnings = JSON.parse(frm.doc.template_warnings || '[]');
warnings = warnings.concat(preview_data.warnings || []);
@ -367,11 +368,13 @@ frappe.ui.form.on('Data Import', {
.map(warning => {
let header = '';
if (warning.col) {
header = __('Column {0}', [warning.col]);
let column_number = `<span class="text-uppercase">${__('Column {0}', [warning.col])}</span>`;
let column_header = columns[warning.col].header_title;
header = `${column_number} (${column_header})`;
}
return `
<div class="warning" data-col="${warning.col}">
<h5 class="text-uppercase">${header}</h5>
<h5>${header}</h5>
<div class="body">${warning.message}</div>
</div>
`;

View file

@ -17,6 +17,7 @@ frappe.listview_settings['Data Import'] = {
get_indicator: function(doc) {
var colors = {
'Pending': 'orange',
'Not Started': 'orange',
'Partial Success': 'orange',
'Success': 'green',
'In Progress': 'orange',
@ -26,6 +27,9 @@ frappe.listview_settings['Data Import'] = {
if (imports_in_progress.includes(doc.name)) {
status = 'In Progress';
}
if (status == 'Pending') {
status = 'Not Started';
}
return [__(status), colors[status], 'status,=,' + doc.status];
},
formatters: {

View file

@ -7,7 +7,7 @@ import io
import frappe
import timeit
import json
from datetime import datetime
from datetime import datetime, date
from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
@ -233,7 +233,7 @@ class Importer:
return updated_doc
else:
# throw if no changes
frappe.throw('No changes to update')
frappe.throw("No changes to update")
def get_eta(self, current, total, processing_time):
self.last_eta = getattr(self, "last_eta", 0)
@ -322,7 +322,7 @@ class ImportFile:
if isinstance(file, frappe.string_types):
if frappe.db.exists("File", {"file_url": file}):
self.file_doc = frappe.get_doc("File", {"file_url": file})
elif 'docs.google.com/spreadsheets' in file:
elif "docs.google.com/spreadsheets" in file:
self.google_sheets_url = file
elif os.path.exists(file):
self.file_path = file
@ -348,7 +348,7 @@ class ImportFile:
elif self.google_sheets_url:
content = get_csv_content_from_google_sheets(self.google_sheets_url)
extension = 'csv'
extension = "csv"
if not content:
frappe.throw(_("Invalid or corrupted content for import"))
@ -602,12 +602,20 @@ class Row:
is_table = frappe.get_meta(doctype).istable
is_update = self.import_type == UPDATE
if is_table and is_update and doc.get("name") in INVALID_VALUES:
# for table rows being inserted in update
# create a new doc with defaults set
new_doc = frappe.new_doc(doctype, as_dict=True)
new_doc.update(doc)
doc = new_doc
if is_table and is_update:
# check if the row already exists
# if yes, fetch the original doc so that it is not updated
# if no, create a new doc
id_field = get_id_field(doctype)
id_value = doc.get(id_field.fieldname)
if id_value and frappe.db.exists(doctype, id_value):
doc = frappe.get_doc(doctype, id_value)
else:
# for table rows being inserted in update
# create a new doc with defaults set
new_doc = frappe.new_doc(doctype, as_dict=True)
new_doc.update(doc)
doc = new_doc
self.check_mandatory_fields(doctype, doc, table_df)
return doc
@ -615,16 +623,12 @@ class Row:
def validate_value(self, value, col):
df = col.df
if df.fieldtype == "Select":
select_options = [d for d in (df.options or '').split('\n') if d]
select_options = get_select_options(df)
if select_options and value not in select_options:
options_string = ", ".join([frappe.bold(d) for d in select_options])
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
)
return
@ -635,11 +639,7 @@ class Row:
frappe.bold(value), frappe.bold(df.options)
)
self.warnings.append(
{
"row": self.row_number,
"field": df_as_json(df),
"message": msg,
}
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
)
return
elif df.fieldtype in ["Date", "Datetime"]:
@ -668,7 +668,7 @@ class Row:
def parse_value(self, value, col):
df = col.df
if isinstance(value, datetime) and df.fieldtype in ["Date", "Datetime"]:
if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]:
return value
value = cstr(value)
@ -689,7 +689,7 @@ class Row:
return value
def get_date(self, value, column):
if isinstance(value, datetime):
if isinstance(value, (datetime, date)):
return value
date_format = column.date_format
@ -786,9 +786,7 @@ class Header(Row):
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, map_to_field, self.seen
)
column = Column(j, header, self.doctype, column_values, map_to_field, self.seen)
self.seen.append(header)
self.columns.append(column)
@ -918,13 +916,20 @@ class Column:
self.skip_import = skip_import
def guess_date_format_for_column(self):
""" Guesses date format for a column by parsing all the values in the column,
"""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
"""
date_formats = [
frappe.utils.guess_date_format(d) for d in self.column_values if isinstance(d, str)
]
def guess_date_format(d):
if isinstance(d, (datetime, date)):
if self.df.fieldtype == "Date":
return "%Y-%m-%d"
if self.df.fieldtype == "Datetime":
return "%Y-%m-%d %H:%M:%S"
if isinstance(d, str):
return frappe.utils.guess_date_format(d)
date_formats = [guess_date_format(d) for d in self.column_values]
date_formats = [d for d in date_formats if d]
if not date_formats:
return
@ -955,28 +960,61 @@ class Column:
if not self.df:
return
if self.df.fieldtype == 'Link':
if self.skip_import:
return
if self.df.fieldtype == "Link":
# find all values that dont exist
values = list(set([cstr(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)})]
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'
})
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()
if not self.date_format:
self.date_format = '%Y-%m-%d'
self.warnings.append({
'col': self.column_number,
'message': _("Date format could not determined from the values in this column. Defaulting to yyyy-mm-dd."),
'type': 'info'
})
self.date_format = "%Y-%m-%d"
self.warnings.append(
{
"col": self.column_number,
"message": _(
"Date format could not be determined from the values in"
" this column. Defaulting to yyyy-mm-dd."
),
"type": "info",
}
)
elif self.df.fieldtype == "Select":
options = get_select_options(self.df)
if options:
values = list(set([cstr(v) for v in self.column_values[1:] if v]))
invalid = list(set(values) - set(options))
if invalid:
valid_values = ", ".join([frappe.bold(o) for o in options])
invalid_values = ", ".join([frappe.bold(i) for i in invalid])
self.warnings.append(
{
"col": self.column_number,
"message": (
"The following values are invalid: {0}. Values must be"
" one of {1}".format(invalid_values, valid_values)
),
}
)
def as_dict(self):
d = frappe._dict()
@ -987,7 +1025,7 @@ 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'):
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
@ -1067,7 +1105,7 @@ def build_fields_dict_for_column_matching(parent_doctype):
# other fields
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
for df in fields:
label = (df.label or '').strip()
label = (df.label or "").strip()
fieldtype = df.fieldtype or "Data"
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
@ -1161,12 +1199,17 @@ def get_user_format(date_format):
.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
"fieldname": df.fieldname,
"fieldtype": df.fieldtype,
"label": df.label,
"options": df.options,
"parent": df.parent,
"default": df.default,
}
def get_select_options(df):
return [d for d in (df.options or "").split("\n") if d]

View file

@ -1,6 +1,6 @@
frappe.ui.form.on('Report', {
refresh: function(frm) {
if(!frappe.boot.developer_mode && frappe.session.user !== 'Administrator') {
if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) {
// make the document read-only
frm.set_read_only();
}

View file

@ -52,9 +52,10 @@ class TestServerScript(unittest.TestCase):
frappe.db.commit()
# @classmethod
# def tearDownClass(cls):
# frappe.db.sql('truncate `tabServer Script`')
@classmethod
def tearDownClass(cls):
frappe.db.commit()
frappe.db.sql('truncate `tabServer Script`')
def setUp(self):
frappe.cache().delete_value('server_script_map')

View file

@ -812,6 +812,7 @@ def reset_password(user):
return 'not found'
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond

View file

@ -119,6 +119,8 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
return has_same_user_permission
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len, filters):
linked_doctypes_map = get_linked_doctypes(doctype, True)

View file

@ -1,8 +0,0 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Video', {
// refresh: function(frm) {
// }
});

View file

@ -1,106 +0,0 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-10-17 05:47:13.087395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"provider",
"url",
"column_break_4",
"publish_date",
"duration",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "provider",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Provider",
"options": "YouTube\nVimeo",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "publish_date",
"fieldtype": "Date",
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-22 12:09:49.057403",
"modified_by": "Administrator",
"module": "Core",
"name": "Video",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class Video(Document):
pass

View file

@ -1,16 +0,0 @@
.dashboard {
margin-top: var(--margin-xs);
padding: 1px;
overflow-x: hidden;
}
.dashboard .grid-col-2 {
column-gap: 20px;
row-gap: 20px;
}
/* .restricted-button {
cursor: default;
position: relative;
right: -5px;
} */

View file

@ -26,13 +26,6 @@ class Dashboard {
</div>`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
this.page = wrapper.page;
this.page.set_title_sub(
$(`<button class="restricted-button">
<span class="octicon octicon-lock"></span>
<span>${__('Restricted')}</span>
</button>`)
);
}
show() {
@ -172,19 +165,26 @@ class Dashboard {
set_dropdown() {
this.page.clear_menu();
this.page.add_menu_item('Edit...', () => {
this.page.add_menu_item(__('Edit'), () => {
frappe.set_route('Form', 'Dashboard', frappe.dashboard.dashboard_name);
}, 1);
});
this.page.add_menu_item('New...', () => {
this.page.add_menu_item(__('New'), () => {
frappe.new_doc('Dashboard');
}, 1);
});
frappe.db.get_list("Dashboard").then(dashboards => {
this.page.add_menu_item(__('Refresh All'), () => {
this.chart_group &&
this.chart_group.widgets_list.forEach(chart => chart.refresh());
this.number_card_group &&
this.number_card_group.widgets_list.forEach(card => card.render_card());
});
frappe.db.get_list('Dashboard').then(dashboards => {
dashboards.map(dashboard => {
let name = dashboard.name;
if(name != this.dashboard_name){
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name));
this.page.add_menu_item(name, () => frappe.set_route("dashboard", name), 1);
}
});
});

View file

@ -41,6 +41,8 @@ def get_columns_and_fields(doctype):
return columns, fields
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user = filters.get("user")
user_perms = frappe.utils.user.UserPermissions(user)

View file

@ -108,7 +108,7 @@ def create_plan():
'connector_name': 'Local Connector',
'connector_type': 'Frappe',
# connect to same host.
'hostname': frappe.conf.host_name,
'hostname': frappe.conf.host_name or frappe.utils.get_site_url(frappe.local.site),
'username': 'Administrator',
'password': 'admin'
'password': frappe.conf.get("admin_password") or 'admin'
}).insert(ignore_if_duplicate=True)

View file

@ -221,6 +221,8 @@ class Workspace:
incomplete_dependencies = [d for d in item.dependencies if not _doctype_contains_a_record(d)]
if len(incomplete_dependencies):
item.incomplete_dependencies = incomplete_dependencies
else:
item.incomplete_dependencies = ""
if item.onboard:
# Mark Spotlights for initial

View file

@ -5,7 +5,7 @@ frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
if (!frappe.boot.developer_mode) {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.disable_form();
}

View file

@ -54,7 +54,8 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -66,7 +67,7 @@
}
],
"links": [],
"modified": "2020-07-10 17:48:19.468813",
"modified": "2020-07-23 11:05:41.890459",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -23,43 +23,20 @@ frappe.ui.form.on('Dashboard Chart', {
frm.chart_filters = null;
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frm.set_df_property('chart_options_section', 'hidden', 1);
frm.disable_form();
}
frm.add_custom_button('Add Chart to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
fields: [
{
label: __('Select Dashboard'),
fieldtype: 'Link',
fieldname: 'dashboard',
options: 'Dashboard',
}
],
primary_action: (values) => {
values.chart_name = frm.doc.chart_name;
frappe.xcall(
'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard',
{args: values}
).then(()=> {
let dashboard_route_html =
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message =
__(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html);
frappe.msgprint(message);
});
d.hide();
}
});
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
'Dashboard Chart',
'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard'
);
if (!frm.doc.chart_name) {
frappe.msgprint(__('Please create chart first'));
} else {
d.show();
dialog.show();
}
});
@ -79,11 +56,6 @@ frappe.ui.form.on('Dashboard Chart', {
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
},
is_standard: function(frm) {
@ -147,6 +119,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.set_df_property('x_field', 'options', []);
frm.set_value('filters_json', '{}');
frm.set_value('dynamic_filters_json', '{}');
frm.set_value('use_report_chart', 0);
frm.trigger('set_chart_report_filters');
},
@ -175,8 +148,8 @@ frappe.ui.form.on('Dashboard Chart', {
set_chart_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json.length > 2) {
filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)};
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
frappe.xcall(
'frappe.desk.query_report.run',
@ -187,14 +160,11 @@ frappe.ui.form.on('Dashboard Chart', {
}
).then(data => {
frm.report_data = data;
if (!data.chart) {
frm.set_value('is_custom', 0);
frm.set_df_property('is_custom', 'hidden', 1);
} else {
frm.set_df_property('is_custom', 'hidden', 0);
}
let report_has_chart = Boolean(data.chart);
if (!frm.doc.is_custom) {
frm.set_df_property('use_report_chart', 'hidden', !report_has_chart);
if (!frm.doc.use_report_chart) {
if (data.result.length) {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
@ -315,7 +285,7 @@ frappe.ui.form.on('Dashboard Chart', {
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields;
let fields = [];
if (is_document_type) {
fields = [
{
@ -340,7 +310,7 @@ frappe.ui.form.on('Dashboard Chart', {
} else if (frm.chart_filters.length) {
fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
fields.map(f => {
if (filters[f.fieldname]) {
let condition = '=';
const filter_row =

View file

@ -12,7 +12,7 @@
"chart_name",
"chart_type",
"report_name",
"is_custom",
"use_report_chart",
"x_field",
"y_axis",
"source",
@ -194,33 +194,27 @@
"label": "To Date"
},
{
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "x_field",
"fieldtype": "Select",
"label": "X Field",
"mandatory_depends_on": "eval: doc.report_name && !doc.is_custom"
"mandatory_depends_on": "eval: doc.report_name && !doc.use_report_chart"
},
{
"depends_on": "eval:doc.chart_type === 'Report'",
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Report Name",
"mandatory_depends_on": "eval:doc.chart_type === 'Report'",
"options": "Report",
"set_only_once": 1
},
{
"default": "0",
"depends_on": "eval: doc.report_name",
"fieldname": "is_custom",
"fieldtype": "Check",
"label": "Is Custom"
},
{
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom",
"depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.use_report_chart",
"fieldname": "y_axis",
"fieldtype": "Table",
"label": "Y Axis",
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
"mandatory_depends_on": "eval:doc.report_name && !doc.use_report_chart",
"options": "Dashboard Chart Field"
},
{
@ -247,8 +241,7 @@
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"show_days": 1,
"show_seconds": 1
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -256,28 +249,29 @@
"fieldtype": "Link",
"label": "Module",
"mandatory_depends_on": "eval: doc.is_standard",
"options": "Module Def",
"show_days": 1,
"show_seconds": 1
"options": "Module Def"
},
{
"fieldname": "dynamic_filters_json",
"fieldtype": "Code",
"label": "Dynamic Filters JSON",
"options": "JSON",
"show_days": 1,
"show_seconds": 1
"options": "JSON"
},
{
"fieldname": "dynamic_filters_section",
"fieldtype": "Section Break",
"label": "Dynamic Filters",
"show_days": 1,
"show_seconds": 1
"label": "Dynamic Filters"
},
{
"default": "0",
"depends_on": "eval: doc.report_name",
"fieldname": "use_report_chart",
"fieldtype": "Check",
"label": "Use Report Chart"
}
],
"links": [],
"modified": "2020-07-10 16:09:47.102062",
"modified": "2020-07-23 11:10:33.509497",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -28,15 +28,28 @@ def get_permission_query_conditions(user):
if "System Manager" in roles:
return None
allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = ['"%s"' % key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
doctype_condition = False
report_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
return '''
`tabDashboard Chart`.`document_type` in ({allowed_doctypes})
or `tabDashboard Chart`.`report_name` in ({allowed_reports})
(`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
and {doctype_condition})
or
(`tabDashboard Chart`.`chart_type` = 'Report'
and {report_condition})
'''.format(
allowed_doctypes=','.join(allowed_doctypes),
allowed_reports=','.join(allowed_reports)
doctype_condition=doctype_condition,
report_condition=report_condition
)
@ -128,7 +141,13 @@ def add_chart_to_dashboard(args):
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name
dashboard_link.chart = args.chart_name or args.name
if args.set_standard and dashboard.is_standard:
chart = frappe.get_doc('Dashboard Chart', dashboard_link.chart)
chart.is_standard = 1
chart.module = dashboard.module
chart.save()
dashboard.append('charts', dashboard_link)
dashboard.save()
@ -338,6 +357,8 @@ def get_year_ending(date):
# last day of this month
return add_to_date(date, days=-1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
or_filters = {'owner': frappe.session.user, 'is_public': 1}
return frappe.db.get_list('Dashboard Chart',

View file

@ -5,14 +5,23 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils.data import validate_json_string
from frappe.modules.export_file import export_to_files
from frappe.model.document import Document
class DeskPage(Document):
def validate(self):
self.validate_cards_json()
if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()):
frappe.throw(_("You need to be in developer mode to edit this document"))
def validate_cards_json(self):
for card in self.cards:
try:
validate_json_string(card.links)
except frappe.ValidationError:
frappe.throw(_("Invalid JSON in card links for {0}").format(frappe.bold(card.label)))
def on_update(self):
if disable_saving_as_standard():
return

View file

@ -32,40 +32,16 @@ frappe.ui.form.on('Number Card', {
create_add_to_dashboard_button: function(frm) {
frm.add_custom_button('Add Card to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
fields: [
{
label: __('Select Dashboard'),
fieldtype: 'Link',
fieldname: 'dashboard',
options: 'Dashboard',
}
],
primary_action: (values) => {
values.name = frm.doc.name;
frappe.xcall(
'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard',
{
args: values
}
).then(()=> {
let dashboard_route_html =
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message =
__(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html);
frappe.msgprint(message);
});
d.hide();
}
});
const dialog = frappe.dashboard_utils.get_add_to_dashboard_dialog(
frm.doc.name,
'Number Card',
'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard'
);
if (!frm.doc.name) {
frappe.msgprint(__('Please create Card first'));
} else {
d.show();
dialog.show();
}
});
},
@ -140,6 +116,7 @@ frappe.ui.form.on('Number Card', {
},
report_name: function(frm) {
frm.filters = [];
frm.set_value('filters_json', '{}');
frm.set_value('dynamic_filters_json', '{}');
frm.set_df_property('report_field', 'options', []);
@ -215,8 +192,8 @@ frappe.ui.form.on('Number Card', {
set_report_field_options: function(frm) {
let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null;
if (frm.doc.dynamic_filters_json.length > 2) {
filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)};
if (frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2) {
filters = frappe.dashboard_utils.get_all_filters(frm.doc);
}
frappe.xcall(
'frappe.desk.query_report.run',
@ -271,7 +248,7 @@ frappe.ui.form.on('Number Card', {
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields;
let fields = [];
if (is_document_type) {
fields = [
{

View file

@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"allow_workflow": 1,
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
@ -116,7 +115,8 @@
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
"label": "Is Standard",
"read_only_depends_on": "eval: !frappe.boot.developer_mode"
},
{
"depends_on": "eval: doc.is_standard",
@ -191,7 +191,7 @@
}
],
"links": [],
"modified": "2020-07-17 18:04:00.814756",
"modified": "2020-07-23 11:11:03.391719",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",

View file

@ -32,13 +32,17 @@ def get_permission_query_conditions(user=None):
if "System Manager" in roles:
return None
allowed_doctypes = ['"%s"' % doctype for doctype in frappe.permissions.get_doctypes_with_read()]
doctype_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
return '''
`tabNumber Card`.`document_type` in ({allowed_doctypes})
'''.format(
allowed_doctypes=','.join(allowed_doctypes)
)
{doctype_condition}
'''.format(doctype_condition=doctype_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)
@ -124,11 +128,16 @@ def create_number_card(args):
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype)
searchfields = meta.get_search_fields()
search_conditions = []
if not frappe.db.exists('DocType', doctype):
return
if txt:
for field in searchfields:
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
@ -172,5 +181,11 @@ def add_card_to_dashboard(args):
dashboard_link = frappe.new_doc('Number Card Link')
dashboard_link.card = args.name
if args.set_standard and dashboard.is_standard:
card = frappe.get_doc('Number Card', dashboard_link.card)
card.is_standard = 1
card.module = dashboard.module
card.save()
dashboard.append('cards', dashboard_link)
dashboard.save()

View file

@ -1,4 +1,5 @@
{
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2016-05-25 09:43:44.767581",
"doctype": "DocType",
@ -46,4 +47,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

View file

@ -67,8 +67,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
if report.report_type == 'Query Report':
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = add_data_to_custom_columns(columns, result)
@ -216,15 +215,21 @@ def add_data_to_custom_columns(columns, result):
return data
def reorder_data_for_custom_columns(custom_columns, columns, result):
def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
custom_column_labels = [col["label"] for col in custom_columns]
if report_type == 'Query Report':
original_column_labels = [col.split(":")[0] for col in columns]
else:
original_column_labels = [col["label"] for col in columns]
reordered_result = []
columns = [col.split(":")[0] for col in columns]
for res in result:
r = []
for col in custom_columns:
for col_name in custom_column_labels:
try:
idx = columns.index(col.get("label"))
idx = original_column_labels.index(col_name)
r.append(res[idx])
except ValueError:
pass

View file

@ -10,6 +10,7 @@ from frappe.handler import is_whitelisted
from frappe import _
from six import string_types
import re
import wrapt
UNTRANSLATED_DOCTYPES = ["DocType", "Role"]
@ -206,3 +207,15 @@ def scrub_custom_query(query, key, txt):
if '%s' in query:
query = query.replace('%s', ((txt or '') + '%'))
return query
@wrapt.decorator
def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
sanitize_searchfield(kwargs['searchfield'])
kwargs['start'] = cint(kwargs['start'])
kwargs['page_len'] = cint(kwargs['page_len'])
if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']):
return []
return fn(**kwargs)

View file

@ -57,6 +57,8 @@ def relink(name, reference_doctype=None, reference_name=None):
communication_type = "Communication" and
name = %s""", (reference_doctype, reference_name, name))
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_communication_doctype(doctype, txt, searchfield, start, page_len, filters):
user_perms = frappe.utils.user.UserPermissions(frappe.session.user)
user_perms.build_permissions()

View file

@ -95,6 +95,11 @@ frappe.ui.form.on("Email Account", {
enable_incoming: function(frm) {
frm.doc.no_remaining = null; //perform full sync
//frm.set_df_property("append_to", "reqd", frm.doc.enable_incoming);
frm.trigger("warn_autoreply_on_incoming");
},
enable_auto_reply: function(frm) {
frm.trigger("warn_autoreply_on_incoming");
},
notify_if_unreplied: function(frm) {
@ -184,7 +189,18 @@ frappe.ui.form.on("Email Account", {
read as well as unread message from server. This may also cause the duplication\
of Communication (emails).");
frappe.confirm(msg, null, function() {
frm.set_value("email_sync_option", "ALL");
frm.set_value("email_sync_option", "UNSEEN");
});
}
},
warn_autoreply_on_incoming: function(frm) {
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
var msg = __("Enabling auto reply on an incoming email account will send automated replies \
to all the synchronized emails. Do you wish to continue?");
frappe.confirm(msg, null, function() {
frm.set_value("enable_auto_reply", 0);
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
});
}
}

View file

@ -251,7 +251,7 @@ class EmailAccount(Document):
email_server = None
if frappe.local.flags.in_test:
incoming_mails = test_mails
incoming_mails = test_mails or []
else:
email_sync_rule = self.build_email_sync_rule()

View file

@ -5,7 +5,10 @@ from __future__ import unicode_literals
import frappe, os
import unittest, email
test_records = frappe.get_test_records('Email Account')
from frappe.test_runner import make_test_records
make_test_records("User")
make_test_records("Email Account")
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments

View file

@ -1,640 +1,166 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "hash",
"beta": 0,
"creation": "2012-08-02 15:17:28",
"custom": 0,
"description": "Email Queue records.",
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"sender",
"recipients",
"show_as_cc",
"message",
"status",
"error",
"message_id",
"reference_doctype",
"reference_name",
"communication",
"send_after",
"priority",
"add_unsubscribe_link",
"unsubscribe_param",
"unsubscribe_method",
"expose_recipients",
"attachments",
"retry"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sender",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"ignore_xss_filter": 1,
"label": "Sender",
"length": 0,
"no_copy": 0,
"options": "Email",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Email"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "recipients",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Recipient",
"length": 0,
"no_copy": 0,
"options": "Email Queue Recipient",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Email Queue Recipient"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "show_as_cc",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Show as cc",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Show as cc"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Message"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Not Sent",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nNot Sent\nSending\nSent\nError\nExpired",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "\nNot Sent\nSending\nSent\nError\nExpired"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "error",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Error",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Error"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference DocName",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "communication",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Communication",
"length": 0,
"no_copy": 0,
"options": "Communication",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "send_after",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Send After",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "priority",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Priority",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "add_unsubscribe_link",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add Unsubscribe Link",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Add Unsubscribe Link"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "unsubscribe_param",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Unsubscribe Param",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "unsubscribe_method",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Unsubscribe Method",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Unsubscribe Method"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "expose_recipients",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expose Recipients",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Expose Recipients"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attachments",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attachments",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "retry",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Retry",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-envelope",
"idx": 1,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"links": [],
"modified": "2020-07-17 15:58:15.369419",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"role": "System Manager"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -107,6 +107,9 @@ class Newsletter(WebsiteGenerator):
if self.get("__islocal"):
throw(_("Please save the Newsletter before sending"))
if not self.recipients:
frappe.throw(_("Newsletter should have at least one recipient"))
def get_context(self, context):
newsletters = get_newsletter_list("Newsletter", None, None, 0)
if newsletters:

View file

@ -11,6 +11,7 @@ import email.utils
from six import iteritems, text_type, string_types
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email import policy
def get_email(recipients, sender='', msg='', subject='[No Subject]',
@ -68,8 +69,8 @@ class EMail:
self.subject = subject
self.expose_recipients = expose_recipients
self.msg_root = MIMEMultipart('mixed')
self.msg_alternative = MIMEMultipart('alternative')
self.msg_root = MIMEMultipart('mixed', policy=policy.SMTPUTF8)
self.msg_alternative = MIMEMultipart('alternative', policy=policy.SMTPUTF8)
self.msg_root.attach(self.msg_alternative)
self.cc = cc or []
self.bcc = bcc or []
@ -100,7 +101,7 @@ class EMail:
Attach message in the text portion of multipart/alternative
"""
from email.mime.text import MIMEText
part = MIMEText(message, 'plain', 'utf-8')
part = MIMEText(message, 'plain', 'utf-8', policy=policy.SMTPUTF8)
self.msg_alternative.attach(part)
def set_part_html(self, message, inline_images):
@ -113,9 +114,9 @@ class EMail:
message, _inline_images = replace_filename_with_cid(message)
# prepare parts
msg_related = MIMEMultipart('related')
msg_related = MIMEMultipart('related', policy=policy.SMTPUTF8)
html_part = MIMEText(message, 'html', 'utf-8')
html_part = MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8)
msg_related.attach(html_part)
for image in _inline_images:
@ -124,7 +125,7 @@ class EMail:
self.msg_alternative.attach(msg_related)
else:
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8'))
self.msg_alternative.attach(MIMEText(message, 'html', 'utf-8', policy=policy.SMTPUTF8))
def set_html_as_text(self, html):
"""Set plain text from HTML"""
@ -135,7 +136,7 @@ class EMail:
from email.mime.text import MIMEText
maintype, subtype = mime_type.split('/')
part = MIMEText(message, _subtype = subtype)
part = MIMEText(message, _subtype = subtype, policy=policy.SMTPUTF8)
if as_attachment:
part.add_header('Content-Disposition', 'attachment', filename=filename)
@ -222,7 +223,8 @@ class EMail:
# reset headers as values may be changed.
for key, val in iteritems(headers):
self.set_header(key, val)
if val:
self.set_header(key, val)
# call hook to enable apps to modify msg_root before sending
for hook in frappe.get_hooks("make_email_body_message"):
@ -238,7 +240,7 @@ class EMail:
"""validate, build message and convert to string"""
self.validate()
self.make()
return self.msg_root.as_string()
return self.msg_root.as_string(policy=policy.SMTPUTF8)
def get_formatted_html(subject, message, footer=None, print_html=None,
email_account=None, header=None, unsubscribe_link=None, sender=None):

View file

@ -39,7 +39,7 @@ This is the text version of this email
subject='Test Subject',
content=email_html,
text_content=email_text
).as_string()
).as_string().replace("\r\n", "\n")
def test_prepare_message_returns_already_encoded_string(self):
@ -153,7 +153,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
subject='Test Subject',
content=email_html,
header=['Email Title', 'green']
).as_string()
).as_string().replace("\r\n", "\n")
self.assertTrue('''<span class=3D"indicator indicator-green" style=3D"background-color:#98=
d85b; border-radius:8px; display:inline-block; height:8px; margin-right:5px=

View file

@ -620,7 +620,12 @@
},
"Congo, The Democratic Republic of the": {
"code": "cd",
"number_format": "#,###.##"
"number_format": "#,###.##",
"currency": "CDF",
"currency_name": "Congolese franc",
"currency_symbol": "FC",
"currency_fraction": "Centime",
"currency_fraction_units": 100
},
"Cook Islands": {
"code": "ck",

View file

@ -189,14 +189,17 @@ def upload_system_backup_to_google_drive():
if frappe.flags.create_new_backup:
set_progress(1, "Backing up Data.")
backup = new_backup()
fileurl_backup = backup.backup_path_db
fileurl_site_config = backup.site_config_backup_path
fileurl_public_files = backup.backup_path_files
fileurl_private_files = backup.backup_path_private_files
else:
fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True)
file_urls = []
file_urls.append(backup.backup_path_db)
file_urls.append(backup.site_config_backup_path)
for fileurl in [fileurl_backup, fileurl_site_config, fileurl_public_files, fileurl_private_files]:
if account.file_backup:
file_urls.append(backup.backup_path_files)
file_urls.append(backup.backup_path_private_files)
else:
file_urls = get_latest_backup_file(with_files=account.file_backup)
for fileurl in file_urls:
if not fileurl:
continue

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Paytm Settings', {
refresh: function(frm) {
frm.dashboard.set_headline(__("For more information, {0}.", [`<a href='https://erpnext.com/docs/user/manual/en/erpnext_integration/paytm-integration'>${__('Click here')}</a>`]));
}
});

View file

@ -0,0 +1,89 @@
{
"actions": [],
"creation": "2020-04-02 00:11:22.846697",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"merchant_id",
"merchant_key",
"staging",
"column_break_4",
"industry_type_id",
"website"
],
"fields": [
{
"fieldname": "merchant_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Merchant ID",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "merchant_key",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Merchant Key",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "staging",
"fieldtype": "Check",
"label": "Staging",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval: !doc.staging",
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"mandatory_depends_on": "eval: !doc.staging",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval: !doc.staging",
"fieldname": "industry_type_id",
"fieldtype": "Data",
"label": "Industry Type ID",
"mandatory_depends_on": "eval: !doc.staging",
"show_days": 1,
"show_seconds": 1
}
],
"issingle": 1,
"links": [],
"modified": "2020-06-08 13:36:09.703143",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Paytm Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import requests
from six.moves.urllib.parse import urlencode
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_url, call_hook_method, cint, flt, cstr
from frappe.integrations.utils import create_request_log, create_payment_gateway
from frappe.utils import get_request_site_address
from paytmchecksum import generateSignature, verifySignature
from frappe.utils.password import get_decrypted_password
class PaytmSettings(Document):
supported_currencies = ["INR"]
def validate(self):
create_payment_gateway('Paytm')
call_hook_method('payment_gateway_enabled', gateway='Paytm')
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs):
'''Return payment url with several params'''
# create unique order id by making it equal to the integration request
integration_request = create_request_log(kwargs, "Host", "Paytm")
kwargs.update(dict(order_id=integration_request.name))
return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs)))
def get_paytm_config():
''' Returns paytm config '''
paytm_config = frappe.db.get_singles_dict('Paytm Settings')
paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key')))
if cint(paytm_config.staging):
paytm_config.update(dict(
website="WEBSTAGING",
url='https://securegw-stage.paytm.in/order/process',
transaction_status_url='https://securegw-stage.paytm.in/order/status',
industry_type_id='RETAIL'
))
else:
paytm_config.update(dict(
url='https://securegw.paytm.in/order/process',
transaction_status_url='https://securegw.paytm.in/order/status',
))
return paytm_config
def get_paytm_params(payment_details, order_id, paytm_config):
# initialize a dictionary
paytm_params = dict()
redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction"
paytm_params.update({
"MID" : paytm_config.merchant_id,
"WEBSITE" : paytm_config.website,
"INDUSTRY_TYPE_ID" : paytm_config.industry_type_id,
"CHANNEL_ID" : "WEB",
"ORDER_ID" : order_id,
"CUST_ID" : payment_details['payer_email'],
"EMAIL" : payment_details['payer_email'],
"TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)),
"CALLBACK_URL" : redirect_uri,
})
checksum = generateSignature(paytm_params, paytm_config.merchant_key)
paytm_params.update({
"CHECKSUMHASH" : checksum
})
return paytm_params
@frappe.whitelist(allow_guest=True)
def verify_transaction(**paytm_params):
'''Verify checksum for received data in the callback and then verify the transaction'''
paytm_config = get_paytm_config()
is_valid_checksum = False
paytm_params.pop('cmd', None)
paytm_checksum = paytm_params.pop('CHECKSUMHASH', None)
if paytm_params and paytm_config and paytm_checksum:
# Verify checksum
is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum)
if is_valid_checksum and paytm_params.get('RESPCODE') == '01':
verify_transaction_status(paytm_config, paytm_params['ORDERID'])
else:
frappe.respond_as_web_page("Payment Failed",
"Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.",
http_status_code=401, indicator_color='red')
frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed')
def verify_transaction_status(paytm_config, order_id):
'''Verify transaction completion after checksum has been verified'''
paytm_params=dict(
MID=paytm_config.merchant_id,
ORDERID= order_id
)
checksum = generateSignature(paytm_params, paytm_config.merchant_key)
paytm_params["CHECKSUMHASH"] = checksum
post_data = json.dumps(paytm_params)
url = paytm_config.transaction_status_url
response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json()
finalize_request(order_id, response)
def finalize_request(order_id, transaction_response):
request = frappe.get_doc('Integration Request', order_id)
transaction_data = frappe._dict(json.loads(request.data))
redirect_to = transaction_data.get('redirect_to') or None
redirect_message = transaction_data.get('redirect_message') or None
if transaction_response['STATUS'] == "TXN_SUCCESS":
if transaction_data.reference_doctype and transaction_data.reference_docname:
custom_redirect_to = None
try:
custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype,
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
request.db_set('status', 'Completed')
except Exception:
request.db_set('status', 'Failed')
frappe.log_error(frappe.get_traceback())
if custom_redirect_to:
redirect_to = custom_redirect_to
redirect_url = '/integrations/payment-success'
else:
request.db_set('status', 'Failed')
redirect_url = '/integrations/payment-failed'
if redirect_to:
redirect_url += '?' + urlencode({'redirect_to': redirect_to})
if redirect_message:
redirect_url += '&' + urlencode({'redirect_message': redirect_message})
frappe.local.response['type'] = 'redirect'
frappe.local.response['location'] = redirect_url
def get_gateway_controller(doctype, docname):
reference_doc = frappe.get_doc(doctype, docname)
gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller")
return gateway_controller

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestVideo(unittest.TestCase):
class TestPaytmSettings(unittest.TestCase):
pass

View file

@ -74,11 +74,11 @@
},
{
"default": "us-east-1",
"description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.",
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.",
"fieldname": "region",
"fieldtype": "Select",
"label": "Region",
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1"
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1"
},
{
"fieldname": "endpoint_url",
@ -151,7 +151,7 @@
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2020-04-13 20:57:24.432183",
"modified": "2020-07-27 17:27:21.400000",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
@ -172,4 +172,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -702,16 +702,13 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
sanitized_value = value
if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
sanitized_value = sanitize_email(value)
if df and (df.get("ignore_xss_filter")
or (df.get("fieldtype") in ("Data", "Small Text", "Text") and df.get("options")=="Email")
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
elif df and (df.get("ignore_xss_filter")
or (df.get("fieldtype")=="Code" and df.get("options")!="Email")
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode")
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
# cancelled and submit but not update after submit should be ignored
or self.docstatus==2
or (self.docstatus==1 and not df.get("allow_on_submit"))):
continue
else:

View file

@ -1307,6 +1307,16 @@ class Document(BaseDocument):
users = set([assignment.owner for assignment in assignments])
return users
def add_tag(self, tag):
"""Add a Tag to this document"""
from frappe.desk.doctype.tag.tag import DocTags
DocTags(self.doctype).add(self.name, tag)
def get_tags(self):
"""Return a list of Tags attached to this document"""
from frappe.desk.doctype.tag.tag import DocTags
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
def execute_action(doctype, name, action, **kwargs):
"""Execute an action on a document (called by background worker)"""
doc = frappe.get_doc(doctype, name)

View file

@ -307,4 +307,4 @@ def set_workflow_state_on_action(doc, workflow_name, action):
for state in workflow.states:
if state.doc_status == docstatus:
doc.set(workflow_state_field, state.state)
return
return

View file

@ -4,6 +4,7 @@ import pytz
from frappe import _
from frappe.auth import LoginManager
from http import cookies
from oauthlib.oauth2.rfc6749.tokens import BearerToken
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant
from oauthlib.oauth2 import RequestValidator
@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator):
oac.scopes = get_url_delimiter().join(request.scopes)
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
oac.client = client_id
oac.user = unquote(cookie_dict['user_id'])
oac.user = unquote(cookie_dict['user_id'].value)
oac.authorization_code = code['code']
oac.save(ignore_permissions=True)
frappe.db.commit()
def authenticate_client(self, request, *args, **kwargs):
cookie_dict = get_cookie_dict_from_headers(request)
#Get ClientID in URL
if request.client_id:
oc = frappe.get_doc("OAuth Client", request.client_id)
@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator):
except Exception as e:
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest"))
cookie_dict = get_cookie_dict_from_headers(request)
user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest"
return frappe.session.user == user_id
def authenticate_client_id(self, client_id, request, *args, **kwargs):
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name')
@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator):
return True
def get_cookie_dict_from_headers(r):
cookie = cookies.BaseCookie()
if r.headers.get('Cookie'):
cookie = r.headers.get('Cookie')
cookie = cookie.split("; ")
cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)}
return cookie_dict
else:
return {}
cookie.load(r.headers.get('Cookie'))
return cookie
def calculate_at_hash(access_token, hash_alg):
"""Helper method for calculating an access token

View file

@ -295,3 +295,5 @@ 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 # 2020-06-24
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
frappe.patches.v13_0.generate_theme_files_in_public_folder

View file

@ -0,0 +1,15 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
themes = frappe.db.get_all(
"Website Theme", filters={"theme_url": ("not like", "/files/website_theme/%")}
)
for theme in themes:
doc = frappe.get_doc("Website Theme", theme.name)
doc.generate_bootstrap_theme()
doc.save()

View file

@ -0,0 +1,11 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if not frappe.db.table_exists('Dashboard Chart'):
return
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
if frappe.db.has_column('Dashboard Chart', 'is_custom'):
rename_field('Dashboard Chart', 'is_custom', 'use_report_chart')

View file

@ -35,13 +35,20 @@ frappe.ui.form.on("Print Format", {
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
frm.set_df_property("html", "reqd", 1);
}
frm.add_custom_button(__("Make Default"), function () {
frappe.call({
method: "frappe.printing.doctype.print_format.print_format.make_default",
args: {
name: frm.doc.name
}
})
frappe.db.get_value('DocType', frm.doc.doc_type, 'default_print_format', (r) => {
if (r.default_print_format != frm.doc.name) {
frm.add_custom_button(__("Set as Default"), function () {
frappe.call({
method: "frappe.printing.doctype.print_format.print_format.make_default",
args: {
name: frm.doc.name
},
callback: function() {
frm.refresh();
}
});
});
}
});
}
},

View file

@ -7,6 +7,12 @@ body {
p {
margin: 1em 0 !important;
}
.ql-editor {
white-space: normal;
}
.ql-editor p {
margin: 0 !important;
}
hr {
border-top: 1px solid #d1d8dd;
}

View file

@ -274,23 +274,29 @@ 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');
let is_field_mandatory = df => {
if (df.reqd && this.exporting_for == 'Insert New Records') {
return true;
}
if (autoname_field && df.fieldname == autoname_field.fieldname) {
return true;
}
if (df.fieldname === 'name') {
return true;
}
return false;
};
return fields
.filter(df => {
if (autoname_field && df.fieldname === autoname_field.fieldname) {
if (autoname_field && df.fieldname === 'name') {
return false;
}
return true;
})
.map(df => {
let label = __(df.label);
if (autoname_field && df.fieldname === 'name') {
label = label + ` (${__(autoname_field.label)})`;
}
return {
label,
label: __(df.label),
value: df.fieldname,
danger: is_field_mandatory(df),
checked: false,

View file

@ -98,6 +98,9 @@ frappe.data_import.ImportPreview = class ImportPreview {
.replace('%y', 'yy')
.replace('%m', 'mm')
.replace('%d', 'dd')
.replace('%H', 'HH')
.replace('%M', 'mm')
.replace('%S', 'ss')
: null;
let column_title = `<span class="indicator green">
@ -354,4 +357,4 @@ function get_fields_as_options(doctype, column_map) {
});
})
);
}
}

View file

@ -17,7 +17,7 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({
set_description: function() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
if (!frappe.datetime.is_timezone_same()) {
if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) {
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {

View file

@ -332,6 +332,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
let docfield = frappe.meta.get_docfield(doctype, fieldname);
let label = docfield ? docfield.label : frappe.model.unscrub(fieldname);
if (docfield && docfield.fieldtype === 'Check') {
filter[3] = filter[3] ? __('Yes'): __('No');
}
if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) {
filter[3] = filter[3].slice(0, 5);
filter[3].push('...');

View file

@ -2,8 +2,8 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
make_input() {
let template = `
<div class="multiselect-list dropdown">
<div class="form-control cursor-pointer dropdown-toggle input-xs" data-toggle="dropdown" tabindex=0>
<span class="status-text ellipsis"></span>
<div class="form-control cursor-pointer dropdown-toggle input-sm" data-toggle="dropdown" tabindex=0>
<div class="status-text ellipsis"></div>
</div>
<ul class="dropdown-menu">
<li class="dropdown-input-wrapper">

View file

@ -69,7 +69,6 @@ Quill.register(CustomColor, true);
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
make_wrapper() {
this._super();
this.$wrapper.find(".like-disabled-input").addClass('ql-editor');
},
make_input() {
@ -201,6 +200,10 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
let value = this.quill ? this.quill.root.innerHTML : '';
// hack to retain space sequence.
value = value.replace(/(\s)(\s)/g, ' &nbsp;');
if (!$(value).find('.ql-editor').length) {
value = `<div class="ql-editor read-mode">${value}</div>`;
}
return value;
},

View file

@ -1401,7 +1401,13 @@ frappe.ui.form.Form = class FrappeForm {
var docperms = frappe.perm.get_perm(this.doc.doctype);
for (var i=0, l=docperms.length; i<l; i++) {
var p = docperms[i];
perm[p.permlevel || 0] = {read:1, print:1, cancel:1, email:1};
perm[p.permlevel || 0] = {
read: p.read,
cancel: p.cancel,
share: p.share,
print: p.print,
email: p.email
};
}
this.perm = perm;
}

View file

@ -228,7 +228,12 @@ frappe.form.formatters = {
return frappe.form.formatters.Text(value);
},
TextEditor: function(value) {
return frappe.form.formatters.Text(value);
let formatted_value = frappe.form.formatters.Text(value);
// to use ql-editor styles
if (!$(formatted_value).find('.ql-editor').length) {
formatted_value = `<div class="ql-editor read-mode">${formatted_value}</div>`;
}
return formatted_value;
},
Code: function(value) {
return "<pre>" + (value==null ? "" : $("<div>").text(value).html()) + "</pre>"

View file

@ -393,8 +393,11 @@ export default class GridRow {
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
field.df.onchange = function() {
me.grid.grid_rows[this.doc.idx-1].refresh_field(this.df.fieldname);
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
};
field.refresh();
if(field.$input) {

View file

@ -19,7 +19,7 @@ frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, hand
let _handler = (...args) => {
try {
handler(...args);
return handler(...args);
} catch (error) {
console.error(handler);
throw error;

View file

@ -57,7 +57,7 @@ frappe.ui.Filter = class {
this.conditions.push([key, __(`{0}`, [filter.label])]);
for (let fieldtype of Object.keys(this.invalid_condition_map)) {
if (!filter.valid_for_fieldtypes.includes(fieldtype)) {
this.invalid_condition_map[fieldtype].push(filter.label);
this.invalid_condition_map[fieldtype].push(key);
}
}
}
@ -243,7 +243,7 @@ frappe.ui.Filter = class {
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);
const filter_value = this.filter_list.get_filter_value(fieldname);
args[field_name] = filter_value;
}
frappe

View file

@ -195,13 +195,18 @@ frappe.ui.FilterGroup = class {
filter_items: (doctype, fieldname) => {
return !this.filter_exists([doctype, fieldname]);
},
base_list: this.base_list,
filter_list: this.base_list || this,
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
return filter;
}
get_filter_value(fieldname) {
let filter_obj = this.filters.find(f => f.fieldname == fieldname) || {};
return filter_obj.value;
}
filter_exists(filter_value) {
// filter_value of form: [doctype, fieldname, condition, value]
let exists = false;

View file

@ -212,6 +212,55 @@ frappe.dashboard_utils = {
}
return filters;
},
get_dashboard_link_field() {
let field = {
label: __('Select Dashboard'),
fieldtype: 'Link',
fieldname: 'dashboard',
options: 'Dashboard',
};
if (!frappe.boot.developer_mode) {
field.get_query = () => {
return {
filters: {
is_standard: 0
}
};
};
}
return field;
},
get_add_to_dashboard_dialog(docname, doctype, method) {
const field = this.get_dashboard_link_field();
const dialog = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
fields: [field],
primary_action: (values) => {
values.name = docname;
values.set_standard = frappe.boot.developer_mode;
frappe.xcall(
method,
{args: values}
).then(()=> {
let dashboard_route_html =
`<a href = "#dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message =
__(`${doctype} ${values.name} added to Dashboard ` + dashboard_route_html);
frappe.msgprint(message);
});
dialog.hide();
}
});
return dialog;
}
};

View file

@ -17,7 +17,7 @@ frappe.help.show = function(doctype) {
frappe.help.show_video = function(youtube_id, title) {
if (frappe.utils.is_url(youtube_id)) {
const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^\"&?/s]{11})';
const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^"&?\\s]{11})';
youtube_id = youtube_id.match(expression)[1];
}

View file

@ -6,7 +6,6 @@ frappe.breadcrumbs = {
preferred: {
"File": "",
"Video": "",
"Dashboard": "Customization",
"Dashboard Chart": "Customization",
"Dashboard Chart Source": "Customization"

View file

@ -44,26 +44,6 @@ frappe.views.CommunicationComposer = Class.extend({
}
});
$(document).on("upload_complete", function(event, attachment) {
if(me.dialog.display) {
var wrapper = $(me.dialog.fields_dict.select_attachments.wrapper);
// find already checked items
var checked_items = wrapper.find('[data-file-name]:checked').map(function() {
return $(this).attr("data-file-name");
});
// reset attachment list
me.render_attach();
// check latest added
checked_items.push(attachment.name);
$.each(checked_items, function(i, filename) {
wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true);
});
}
})
this.prepare();
this.dialog.show();
@ -387,77 +367,86 @@ frappe.views.CommunicationComposer = Class.extend({
folder: 'Home/Attachments',
on_success: attachment => {
this.attachments.push(attachment);
this.render_attach();
this.render_attachment_rows(attachment);
}
};
if(this.frm) {
if (this.frm) {
args = {
doctype: this.frm.doctype,
docname: this.frm.docname,
folder: 'Home/Attachments',
on_success: attachment => {
this.frm.attachments.attachment_uploaded(attachment);
this.render_attach();
this.render_attachment_rows(attachment);
}
}
};
}
$("<h6 class='text-muted add-attachment' style='margin-top: 12px; cursor:pointer;'>"
+__("Select Attachments")+"</h6><div class='attach-list'></div>\
<p class='add-more-attachments'>\
<a class='text-muted small'><i class='octicon octicon-plus' style='font-size: 12px'></i> "
+__("Add Attachment")+"</a></p>").appendTo(attach.empty())
$(`
<h6 class='text-muted add-attachment' style='margin-top: 12px; cursor:pointer;'>
${__("Select Attachments")}
</h6>
<div class='attach-list'></div>
<p class='add-more-attachments'>
<a class='text-muted small'>
<i class='octicon octicon-plus' style='font-size: 12px'></i>
${__("Add Attachment")}
</a>
</p>
`).appendTo(attach.empty());
attach
.find(".add-more-attachments a")
.on('click',() => new frappe.ui.FileUploader(args));
this.render_attach();
.on('click', () => new frappe.ui.FileUploader(args));
this.render_attachment_rows();
},
render_attach:function(){
var fields = this.dialog.fields_dict;
var attach = $(fields.select_attachments.wrapper).find(".attach-list").empty();
var files = [];
if (this.attachments && this.attachments.length) {
files = files.concat(this.attachments);
}
if (cur_frm) {
files = files.concat(cur_frm.get_files());
}
render_attachment_rows: function(attachment) {
const select_attachments = this.dialog.fields_dict.select_attachments;
const attachment_rows = $(select_attachments.wrapper).find(".attach-list");
if (attachment) {
attachment_rows.append(this.get_attachment_row(attachment, true));
} else {
let files = [];
if (this.attachments && this.attachments.length) {
files = files.concat(this.attachments);
}
if (this.frm) {
files = files.concat(this.frm.get_files());
}
if(files.length) {
$.each(files, function(i, f) {
if (!f.file_name) return;
f.file_url = frappe.urllib.get_full_url(f.file_url);
$(repl('<p class="checkbox">'
+ '<label><span><input type="checkbox" data-file-name="%(name)s"></input></span>'
+ '<span class="small">%(file_name)s</span>'
+ ' <a href="%(file_url)s" target="_blank" class="text-muted small">'
+ '<i class="fa fa-share" style="vertical-align: middle; margin-left: 3px;"></i>'
+ '</label></p>', f))
.appendTo(attach)
});
}
this.select_attachments();
},
select_attachments:function(){
let me = this;
if(me.dialog.display) {
let wrapper = $(me.dialog.fields_dict.select_attachments.wrapper);
let unchecked_items = wrapper.find('[data-file-name]:not(:checked)').map(function() {
return $(this).attr("data-file-name");
});
$.each(unchecked_items, function(i, filename) {
wrapper.find('[data-file-name="'+ filename +'"]').prop("checked", true);
});
if (files.length) {
$.each(files, (i, f) => {
if (!f.file_name) return;
if (!attachment_rows.find(`[data-file-name="${f.name}"]`).length) {
f.file_url = frappe.urllib.get_full_url(f.file_url);
attachment_rows.append(this.get_attachment_row(f));
}
});
}
}
},
get_attachment_row(attachment, checked) {
return $(`<p class="checkbox">
<label>
<span>
<input
type="checkbox"
data-file-name="${attachment.name}"
${checked ? 'checked': ''}>
</input>
</span>
<span class="small">${attachment.file_name}</span>
<a href="${attachment.file_url}" target="_blank" class="text-muted small">
<i class="fa fa-share" style="vertical-align: middle; margin-left: 3px;"></i>
</label>
</p>`);
},
setup_email: function() {
// email
var me = this;
var fields = this.dialog.fields_dict;
if(this.attach_document_print) {

View file

@ -168,6 +168,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
add_card_to_dashboard() {
let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data);
const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field();
const set_standard = frappe.boot.developer_mode;
const dialog = new frappe.ui.Dialog({
title: __('Create Card'),
fields: [
@ -192,12 +195,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
label: __('Add to Dashboard'),
fieldtype: 'Section Break'
},
{
fieldname: 'dashboard',
label: __('Choose Dashboard'),
fieldtype: 'Link',
options: 'Dashboard',
},
dashboard_field,
{
fieldname: 'cb_2',
fieldtype: 'Column Break'
@ -213,7 +211,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (!values.label) {
values.label = `${values.report_function} of ${toTitle(values.report_field)}`;
}
this.create_number_card(values, values.dashboard, values.label);
this.create_number_card(values, values.dashboard, values.label, set_standard);
dialog.hide();
}
});
@ -224,27 +222,26 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
add_chart_to_dashboard() {
if (this.chart_fields || this.chart_options) {
const dashboard_field = frappe.dashboard_utils.get_dashboard_link_field();
const set_standard = frappe.boot.developer_mode;
const dialog = new frappe.ui.Dialog({
title: __('Create Chart'),
fields: [
{
fieldname: 'dashboard',
label: 'Choose Dashboard',
fieldtype: 'Link',
options: 'Dashboard',
},
{
fieldname: 'dashboard_chart_name',
label: 'Chart Name',
fieldtype: 'Data',
}
},
dashboard_field,
],
primary_action_label: __('Add'),
primary_action: (values) => {
this.create_dashboard_chart(
this.chart_fields || this.chart_options,
values.dashboard,
values.dashboard_chart_name
values.dashboard_chart_name,
set_standard
);
dialog.hide();
}
@ -256,12 +253,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
create_number_card(values, dashboard_name, card_name) {
create_number_card(values, dashboard_name, card_name, set_standard) {
let args = {
'dashboard': dashboard_name || null,
'type': 'Report',
'report_name': this.report_name,
'filters_json': JSON.stringify(this.get_filter_values()),
set_standard: set_standard,
};
Object.assign(args, values);
@ -274,7 +272,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
);
}
create_dashboard_chart(chart_args, dashboard_name, chart_name) {
create_dashboard_chart(chart_args, dashboard_name, chart_name, set_standard) {
let args = {
'dashboard': dashboard_name || null,
'chart_type': 'Report',
@ -282,7 +280,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type),
'color': chart_args.color,
'filters_json': JSON.stringify(this.get_filter_values()),
'custom_options': {}
'custom_options': {},
'set_standard': set_standard,
};
for (let key in chart_args) {
@ -303,7 +302,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
'y_axis': chart_args.y_axis_fields.map(f => {
return {'y_field': f.y_field, 'color': f.color};
}),
'is_custom': 0
'use_report_chart': 0
}
);
} else {
@ -311,7 +310,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
Object.assign(args,
{
'chart_name': chart_name,
'is_custom': 1
'use_report_chart': 1
}
);
}
@ -969,8 +968,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (column.isHeader && !data && this.data) {
// totalRow doesn't have a data object
// proxy it using the first data object
// this is needed only for currency formatting
data = this.data[0];
// applied to Float, Currency fields, needed only for currency formatting.
// make first data column have value 'Total'
let index = 1;
if (this.datatable && this.datatable.options.checkboxColumn) index = 2;
if (column.colIndex === index && !value) {
value = "Total";
column.fieldtype = "Data"; // avoid type issues for value if Date column
} else if (in_list(["Currency", "Float"], column.fieldtype)) {
// proxy for currency and float
data = this.data[0];
}
}
return frappe.format(value, column,
{for_print: false, always_show_decimals: true}, data);

View file

@ -767,10 +767,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
const index = this.fields.findIndex(f => column.field === f[0]);
if (index === -1) return;
const field = this.fields[index];
if (field[0] === 'name' && this.group_by === null) {
if (field[0] === 'name') {
this.refresh();
frappe.throw(__('Cannot remove ID field'));
}
this.fields.splice(index, 1);
this.build_fields();
this.setup_columns();
@ -849,7 +851,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
columns: 2,
options: columns[this.doctype]
.filter(df => {
return !df.hidden;
return !df.hidden && df.fieldname !== 'name';
})
.map(df => ({
label: __(df.label),

View file

@ -266,7 +266,7 @@ export default class ChartWidget extends Widget {
}
get_report_chart_data(result) {
if (result.chart && this.chart_doc.is_custom) {
if (result.chart && this.chart_doc.use_report_chart) {
return result.chart.data;
} else {
let y_fields = [];
@ -638,7 +638,7 @@ export default class ChartWidget extends Widget {
update_last_synced() {
if (!this.chart_doc.last_synced_on) {
return
return;
}
let last_synced_text = __("Last synced {0}", [
comment_when(this.chart_doc.last_synced_on)
@ -680,14 +680,15 @@ export default class ChartWidget extends Widget {
}
update_default_date_filters(report_filters, chart_filters) {
report_filters.map(f => {
if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) {
if (f.reqd || chart_filters[f.fieldname]) {
chart_filters[f.fieldname] = f.default;
if (report_filters) {
report_filters.map(f => {
if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) {
if (f.reqd || chart_filters[f.fieldname]) {
chart_filters[f.fieldname] = f.default;
}
}
}
});
});
}
return chart_filters;
}

View file

@ -165,7 +165,7 @@ export default class NumberCardWidget extends Widget {
get_number() {
return frappe.xcall(this.settings.method, this.settings.args).then(res => {
this.settings.get_number(res);
return this.settings.get_number(res);
});
}

View file

@ -136,7 +136,7 @@ export default class OnboardingWidget extends Widget {
if (step.is_single) {
route = `Form/${step.reference_document}`;
} else {
route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`;
route = `Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`;
}
let current_route = frappe.get_route();
@ -262,7 +262,7 @@ export default class OnboardingWidget extends Widget {
frappe.route_hooks.after_save = callback;
}
frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)}`);
frappe.set_route(`Form/${step.reference_document}/${__('New')} ${__(step.reference_document)} 1`);
}
show_quick_entry(step) {

View file

@ -131,7 +131,8 @@ function shorten_number(number, country) {
const number_system = get_number_system(country);
let x = Math.abs(Math.round(number));
for (const map of number_system) {
if (x >= map.divisor) {
const condition = map.condition ? map.condition(x) : x >= map.divisor;
if (condition) {
return Math.round(number/map.divisor) + ' ' + map.symbol;
}
}
@ -161,9 +162,14 @@ function get_number_system(country) {
{
divisor: 1.0e+6,
symbol: 'M'
},
{
divisor: 1.0e+3,
symbol: 'K',
condition: (num) => num.toFixed().length > 5
}]
};
return number_system_map[country];
}
export { generate_route, generate_grid, build_summary_item, shorten_number };
export { generate_route, generate_grid, build_summary_item, shorten_number };

View file

@ -174,18 +174,12 @@ class ShortcutDialog extends WidgetDialog {
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
let doctype = this.dialog.get_value("link_to");
doctype &&
frappe.db
.get_value("DocType", doctype, "issingle")
.then((res) => {
if (res.message && res.message.issingle) {
this.hide_filters();
} else {
this.setup_filter(doctype);
this.show_filters();
}
});
if (doctype && frappe.boot.single_types.includes(doctype)) {
this.hide_filters();
} else if (doctype) {
this.setup_filter(doctype);
this.show_filters();
}
} else {
this.hide_filters();
}

View file

@ -719,7 +719,6 @@ li.user-progress {
margin-right: 5px;
margin-left: 0px;
position: relative;
height: 12px;
}
// Will not be required after commonifying lists with empty state
@ -731,28 +730,11 @@ li.user-progress {
height: 100%;
}
// mozilla doesn't support
// Firefox doesn't support
// pseudo elements on checkbox
@-moz-document url-prefix() {
@supports (-moz-appearance: none) or (-ms-ime-align:auto) {
input[type="checkbox"] {
visibility: visible;
left: 0;
}
}
@supports (-moz-appearance: none) {
input[type="checkbox"] {
visibility: visible;
left: 0;
}
}
// edge doesn't support pseudo elements on checkbox
//Microsoft Edge Browser 12+ (All)
@supports (-ms-ime-align:auto) {
input[type="checkbox"] {
visibility: visible;
left: 0;
height: @checkbox-height !important
}
}

View file

@ -10,6 +10,13 @@ p {
margin: 1em 0 !important;
}
.ql-editor {
white-space: normal;
p {
margin: 0 !important;
}
}
hr {
border-top: 1px solid @border-color;
}
@ -210,4 +217,4 @@ hr {
.report-title {
margin-bottom: 20px;
}
/* csslint ignore:end */
/* csslint ignore:end */

View file

@ -77,7 +77,7 @@
}
}
.ql-editor .mention {
.ql-editor:not(.read-mode) .mention {
height: auto;
width: auto;
border-radius: 10px;
@ -132,3 +132,7 @@
margin-top: 0px;
margin-bottom: 0px;
}
.ql-editor.read-mode {
padding: 0;
}

View file

@ -1,5 +1,6 @@
@import "variables";
@import "mixins";
@import '~quill/dist/quill.core';
@import 'frappe/public/css/font-awesome';
@import "~bootstrap/scss/bootstrap";
@import 'multilevel-dropdown';
@ -12,6 +13,20 @@
@import 'portal';
@import 'doc';
.ql-editor.read-mode {
padding: 0;
line-height: 1.6;
h1,
h2,
h3,
h4,
h5 {
margin-top: 0.5em;
margin-bottom: 0.25em;
}
}
.container {
padding-left: 1.25rem;
padding-right: 1.25rem;
@ -323,4 +338,4 @@ h5.modal-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

View file

@ -42,7 +42,11 @@
{{ head_include or "" }}
{% endblock -%}
{%- block style %}{%- endblock -%}
{%- block style %}
{% if colocated_css -%}
<style>{{ colocated_css }}</style>
{%- endif %}
{%- endblock -%}
<script>
window.frappe = {};
@ -86,7 +90,11 @@
<script type="text/javascript" src="{{ link | abs_url }}"></script>
{%- endfor -%}
{%- block script %}{%- endblock %}
{%- block script %}
{% if colocated_js -%}
<script>{{ colocated_js }}</script>
{%- endif %}
{%- endblock %}
<!-- csrf_token -->
{%- block body_include %}{{ body_include or "" }}{% endblock -%}
</body>

View file

@ -7,8 +7,6 @@
{%- endif -%}
</a>
{% endmacro %}
{% if footer_items -%}
<div class="footer-links">
<div class="row">
<div class="footer-col-left col-sm-6">
@ -24,4 +22,3 @@
</div>
</div>
</div>
{% endif %}

View file

@ -1,9 +1,7 @@
{%- set res = frappe.utils.get_thumbnail_base64_for_image(src) if src else false -%}
{%- if res and res['base64'].startswith('data:') -%}
<img src="{{ res['base64'] }}" class="image-with-blur {{ resolve_class(class) }}"
data-src="{{ src or '' }}" alt="{{ alt or '' }}"
width="{{ res['width'] }}" height="{{ res['height'] }}"
style="width: {{ res['width'] }}px; height: {{ res['height'] }}px;" />
alt="{{ alt or '' }}" width="{{ res['width'] }}" height="{{ res['height'] }}" data-src="{{ src or '' }}" />
{%- else -%}
<img src="{{ src or '' }}" class="{{ resolve_class(class) }}" alt="{{ alt or '' }}" />
{%- endif -%}

View file

@ -186,7 +186,7 @@ login.login_handlers = (function() {
} else if(data.message == 'Password Reset'){
window.location.href = frappe.utils.sanitise_redirect(data.redirect_to);
} else if(data.message=="No App") {
login.set_indicator("{{ _("Success") }}", 'green');
login.set_indicator("{{ _('Success') }}", 'green');
if(localStorage) {
var last_visited =
localStorage.getItem("last_visited")

View file

@ -1,5 +1,8 @@
{%- if metatags -%}
{%- for name in metatags %}
<meta {% if name.startswith('og:') %}property="{{ name }}"{% else %}name="{{ name }}"{% endif %} content="{{ metatags[name] | striptags | escape }}">
{%- set content = metatags.get(name, '') -%}
{%- if content -%}
<meta {% if name.startswith('og:') %}property="{{ name }}"{% else %}name="{{ name }}"{% endif %} content="{{ content | striptags | escape }}">
{%- endif -%}
{%- endfor -%}
{%- endif -%}

View file

@ -0,0 +1,43 @@
{% extends "templates/web.html" %}
{% block title %} Payment {% endblock %}
{%- block header -%}
<head>
<title>Merchant Checkout Page</title>
</head>
{% endblock %}
{% block script %}
<script defer type="text/javascript">
document.paytm_form.submit();
</script>
{% endblock %}
{%- block page_content -%}
<body>
<div class="centered">
<h2>Please do not refresh this page...</h2>
<form method="post" action="{{ url }}" name="paytm_form">
{% for name, value in payment_details.items() %}
<input type="hidden" name="{{ name }}" value="{{ value }}">
{% endfor %}
</form>
</div>
</body>
{% endblock %}
{% block style %}
<style>
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.web-footer {
display: none;
}
</style>
{% endblock %}

View file

@ -0,0 +1,27 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
import json
from frappe.integrations.doctype.paytm_settings.paytm_settings import get_paytm_params, get_paytm_config
def get_context(context):
context.no_cache = 1
paytm_config = get_paytm_config()
try:
doc = frappe.get_doc("Integration Request", frappe.form_dict['order_id'])
context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config)
context.url = paytm_config.url
except Exception:
frappe.log_error()
frappe.redirect_to_message(_('Invalid Token'),
_('Seems token you are using is invalid!'),
http_status_code=400, indicator_color='red')
frappe.local.flags.redirect_location = frappe.local.response.location
raise frappe.Redirect

View file

@ -99,9 +99,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{%- if df.fieldtype=="Code" %}
<pre class="value">{{ doc.get(df.fieldname) }}</pre>
{% else -%}
{%- if df.fieldtype=="Text Editor" -%}<div class='ql-editor'>{%- endif -%}
{{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
{%- if df.fieldtype=="Text Editor" -%}</div>{%- endif -%}
{% endif -%}
</div>
{%- endif -%}

View file

@ -88,6 +88,7 @@ class TestNaming(unittest.TestCase):
series = 'TEST-'
key = 'TEST-'
name = 'TEST-00003'
frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series)
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,))
revert_series_if_last(key, name)
count = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0]

View file

@ -49,7 +49,7 @@ class TestScheduler(TestCase):
# 2nd job not loaded
self.assertFalse(job.enqueue())
job.delete()
frappe.db.sql('DELETE FROM `tabScheduled Job Log` WHERE `scheduled_job_type`=%s', job.name)
def test_is_dormant(self):
self.assertTrue(is_dormant(check_time= get_datetime('2100-01-01 00:00:00')))

Some files were not shown because too many files have changed in this diff Show more