Merge branch 'develop' of github.com:frappe/frappe into snyk-fix-e205bf085ec74cf13bd3f75b89038f28

This commit is contained in:
Gavin D'souza 2020-12-16 15:50:19 +05:30
commit 5db02513e5
50 changed files with 688 additions and 213 deletions

View file

@ -945,7 +945,11 @@ def get_installed_apps(sort=False, frappe_last=False):
connect()
if not local.all_apps:
local.all_apps = get_all_apps(True)
local.all_apps = cache().get_value('all_apps', get_all_apps)
#cache bench apps
if not cache().get_value('all_apps'):
cache().set_value('all_apps', local.all_apps)
installed = json.loads(db.get_global("installed_apps") or "[]")

View file

@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', {
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
frm.trigger('toggle_submit_on_creation');
},
reference_doctype: function(frm) {
frm.trigger('toggle_submit_on_creation');
},
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
},
template: function(frm) {
@ -86,10 +100,7 @@ frappe.ui.form.on('Auto Repeat', {
frappe.auto_repeat.render_schedule = function(frm) {
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc
}).done((r) => {
frm.call("get_auto_repeat_schedule").then(r => {
frm.dashboard.wrapper.empty();
frm.dashboard.add_section(
frappe.render_template("auto_repeat_schedule", {

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
@ -12,6 +13,7 @@
"section_break_3",
"reference_doctype",
"reference_document",
"submit_on_creation",
"column_break_5",
"start_date",
"end_date",
@ -21,6 +23,8 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"section_break_12",
"repeat_on_days",
"notification",
"notify_by_email",
"recipients",
@ -186,9 +190,28 @@
"fieldname": "repeat_on_last_day",
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "repeat_on_days",
"fieldtype": "Table",
"label": "Repeat on Days",
"options": "Auto Repeat Day"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "section_break_12",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
}
],
"modified": "2019-07-17 11:30:51.412317",
"links": [],
"modified": "2020-12-10 10:43:13.449172",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
@ -13,16 +14,19 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
class AutoRepeat(Document):
def validate(self):
self.update_status()
self.validate_reference_doctype()
self.validate_submit_on_creation()
self.validate_dates()
self.validate_email_id()
self.validate_auto_repeat_days()
self.set_dates()
self.update_auto_repeat_id()
self.unlink_if_applicable()
@ -48,7 +52,7 @@ class AutoRepeat(Document):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
@ -60,6 +64,11 @@ class AutoRepeat(Document):
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
def validate_submit_on_creation(self):
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
frappe.bold('Submit on Creation')))
def validate_dates(self):
if frappe.flags.in_patch:
return
@ -82,6 +91,12 @@ class AutoRepeat(Document):
else:
frappe.throw(_("'Recipients' not specified"))
def validate_auto_repeat_days(self):
auto_repeat_days = self.get_auto_repeat_days()
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
repeated_days = get_repeated(auto_repeat_days)
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
def update_auto_repeat_id(self):
#check if document is already on auto repeat
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
@ -107,7 +122,7 @@ class AutoRepeat(Document):
end_date = getdate(self.end_date)
if not self.end_date:
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
next_date = self.get_next_schedule_date(schedule_date=start_date)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
@ -116,8 +131,7 @@ class AutoRepeat(Document):
schedule_details.append(row)
if self.end_date:
next_date = get_next_schedule_date(
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)):
row = {
@ -126,8 +140,7 @@ class AutoRepeat(Document):
"next_scheduled_date" : next_date
}
schedule_details.append(row)
next_date = get_next_schedule_date(
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
return schedule_details
@ -150,6 +163,9 @@ class AutoRepeat(Document):
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
if self.submit_on_creation:
new_doc.submit()
return new_doc
def update_doc(self, new_doc, reference_doc):
@ -160,7 +176,7 @@ class AutoRepeat(Document):
if new_doc.meta.get_field('auto_repeat'):
new_doc.set('auto_repeat', self.name)
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
if new_doc.meta.get_field(fieldname):
new_doc.set(fieldname, reference_doc.get(fieldname))
@ -202,6 +218,75 @@ class AutoRepeat(Document):
new_doc.set('from_date', from_date)
new_doc.set('to_date', to_date)
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
"""
Returns the next schedule date for auto repeat after a recurring document has been created.
Adds required offset to the schedule_date param and returns the next schedule date.
:param schedule_date: The date when the last recurring document was created.
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
"""
if month_map.get(self.frequency):
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and self.repeat_on_last_day:
day_count = 31
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count and self.repeat_on_day:
day_count = self.repeat_on_day
next_date = get_next_date(self.start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(self.start_date, month_count)
else:
days = self.get_days(schedule_date)
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(self.frequency, 0)
next_date = get_next_date(self.start_date, month_count, day_count)
else:
days = self.get_days(next_date)
next_date = add_days(next_date, days)
return next_date
def get_days(self, schedule_date):
if self.frequency == "Weekly":
days = self.get_offset_for_weekly_frequency(schedule_date)
else:
# daily frequency
days = 1
return days
def get_offset_for_weekly_frequency(self, schedule_date):
# if weekdays are not set, offset is 7 from current schedule date
if not self.repeat_on_days:
return 7
repeat_on_days = self.get_auto_repeat_days()
current_schedule_day = getdate(schedule_date).weekday()
weekdays = list(week_map.keys())
# if repeats on more than 1 day or
# start date's weekday is not in repeat days, then get next weekday
# else offset is 7
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
weekday = get_next_weekday(current_schedule_day, repeat_on_days)
next_weekday_number = week_map.get(weekday, 0)
# offset for upcoming weekday
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
return 7
def get_auto_repeat_days(self):
return [d.day for d in self.get('repeat_on_days', [])]
def send_notification(self, new_doc):
"""Notify concerned people about recurring document generation"""
subject = self.subject or ''
@ -282,42 +367,24 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
if month_map.get(frequency):
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and repeat_on_last_day:
day_count = 31
next_date = get_next_date(start_date, month_count, day_count)
elif month_count and repeat_on_day:
day_count = repeat_on_day
next_date = get_next_date(start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
elif days:
next_date = add_days(next_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)
return dt
def get_next_weekday(current_schedule_day, weekdays):
days = list(week_map.keys())
if current_schedule_day > 0:
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
else:
days = days[(current_schedule_day + 1):]
for entry in days:
if entry in weekdays:
return entry
#called through hooks
def make_auto_repeat_entry():
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
@ -328,6 +395,7 @@ def make_auto_repeat_entry():
data = get_auto_repeat_entries(date)
frappe.enqueue(enqueued_method, data=data)
def create_repeated_entries(data):
for d in data:
doc = frappe.get_doc('Auto Repeat', d.name)
@ -337,10 +405,11 @@ def create_repeated_entries(data):
if schedule_date == current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
def get_auto_repeat_entries(date=None):
if not date:
date = getdate(today())
@ -349,6 +418,7 @@ def get_auto_repeat_entries(date=None):
['status', '=', 'Active']
])
#called through hooks
def set_auto_repeat_as_completed():
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
@ -358,6 +428,7 @@ def set_auto_repeat_as_completed():
doc.status = 'Completed'
doc.save()
@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
if not start_date:

View file

@ -7,10 +7,9 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.get_doc('ToDo', new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
weekdays = list(week_map.keys())
current_weekday = getdate().weekday()
days = [
{'day': weekdays[current_weekday]},
{'day': weekdays[(current_weekday + 2) % 7]}
]
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
doc.reload()
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
def test_monthly_auto_repeat(self):
start_date = today()
end_date = add_months(start_date, 12)
@ -111,6 +156,25 @@ class TestAutoRepeat(unittest.TestCase):
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
self.assertEqual(getdate(doc.next_schedule_date), current_date)
def test_submit_on_creation(self):
doctype = 'Test Submittable DocType'
create_submittable_doctype(doctype)
current_date = getdate()
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
submittable_doc.submit()
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
start_date=add_days(current_date, -1), submit_on_creation=1)
data = get_auto_repeat_entries(current_date)
create_repeated_entries(data)
docnames = frappe.db.get_all(doc.reference_doctype,
filters={'auto_repeat': doc.name},
fields=['docstatus'],
limit=1
)
self.assertEquals(docnames[0].docstatus, 1)
def make_auto_repeat(**args):
args = frappe._dict(args)
@ -118,13 +182,46 @@ def make_auto_repeat(**args):
'doctype': 'Auto Repeat',
'reference_doctype': args.reference_doctype or 'ToDo',
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
'submit_on_creation': args.submit_on_creation or 0,
'frequency': args.frequency or 'Daily',
'start_date': args.start_date or add_days(today(), -1),
'end_date': args.end_date or "",
'notify_by_email': args.notify or 0,
'recipients': args.recipients or "",
'subject': args.subject or "",
'message': args.message or ""
'message': args.message or "",
'repeat_on_days': args.days or []
}).insert(ignore_permissions=True)
return doc
def create_submittable_doctype(doctype):
if frappe.db.exists('DocType', doctype):
return
else:
doc = frappe.get_doc({
'doctype': 'DocType',
'__newname': doctype,
'module': 'Custom',
'custom': 1,
'is_submittable': 1,
'fields': [{
'fieldname': 'test',
'label': 'Test',
'fieldtype': 'Data'
}],
'permissions': [{
'role': 'System Manager',
'read': 1,
'write': 1,
'create': 1,
'delete': 1,
'submit': 1,
'cancel': 1,
'amend': 1
}]
}).insert()
doc.allow_auto_repeat = 1
doc.save()

View file

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2020-11-10 22:30:53.690228",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"day"
],
"fields": [
{
"fieldname": "day",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Day",
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-10 22:30:53.690228",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat Day",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -100,13 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
with_public_files = os.path.join(base_path, with_public_files)
public = extract_files(site, with_public_files, 'public')
public = extract_files(site, with_public_files)
os.remove(public)
if with_private_files:
with_private_files = os.path.join(base_path, with_private_files)
private = extract_files(site, with_private_files, 'private')
private = extract_files(site, with_private_files)
os.remove(private)
# Removing temporarily created file

View file

@ -44,7 +44,7 @@
},
{
"fieldname": "options",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Options"
},
{
@ -58,7 +58,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 16:15:46.937267",
"modified": "2020-12-05 19:20:00.503097",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Filter",

View file

@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', {
setup_help(frm) {
frm.get_field('help_html').html(`
<h3>Examples</h3>
<h4>DocType Event</h4>
<pre><code>
<p>Add logic for standard doctype events like Before Insert, After Submit, etc.</p>
<pre>
<code>
# set property
if "test" in doc.description:
doc.status = 'Closed'
doc.status = 'Closed'
# validate
if "validate" in doc.description:
raise frappe.ValidationError
raise frappe.ValidationError
# auto create another document
if doc.allocted_to:
frappe.get_doc(dict(
doctype = 'ToDo'
owner = doc.allocated_to,
description = doc.subject
)).insert()
</code></pre>
if doc.allocated_to:
frappe.get_doc(dict(
doctype = 'ToDo'
owner = doc.allocated_to,
description = doc.subject
)).insert()
</code>
</pre>
<hr>
<h4>API Call</h4>
<p>Respond to <code>/api/method/&lt;method-name&gt;</code> calls, just like whitelisted methods</p>
<pre><code>
# respond to API
@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
else:
frappe.response['message'] = "ok"
</code></pre>
<hr>
<h4>Permission Query</h4>
<p>Add conditions to the where clause of list queries.</p>
<pre><code>
# generate dynamic conditions and set it in the conditions variable
tenant_id = frappe.db.get_value(...)
conditions = 'tenant_id = {}'.format(tenant_id)
# resulting select query
select name from \`tabPerson\`
where tenant_id = 2
order by creation desc
</code></pre>
`);
}

View file

@ -24,7 +24,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
"options": "DocType Event\nScheduler Event\nAPI",
"options": "DocType Event\nScheduler Event\nPermission Query\nAPI",
"reqd": 1
},
{
@ -35,7 +35,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.script_type==='DocType Event'",
"depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-11-11 12:39:41.391052",
"modified": "2020-12-03 22:42:02.708148",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -43,6 +43,12 @@ class ServerScript(Document):
# wrong report type!
raise frappe.DoesNotExistError
def get_permission_query_conditions(self, user):
locals = {"user": user, "conditions": ""}
safe_exec(self.script, None, locals)
if locals["conditions"]:
return locals["conditions"]
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))

View file

@ -50,6 +50,9 @@ def get_server_script_map():
# },
# '_api': {
# '[path]': '[server script]'
# },
# 'permission_query': {
# 'DocType': '[server script]'
# }
# }
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'):
@ -57,16 +60,20 @@ def get_server_script_map():
script_map = frappe.cache().get_value('server_script_map')
if script_map is None:
script_map = {}
script_map = {
'permission_query': {}
}
enabled_server_scripts = frappe.get_all('Server Script',
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
filters={'disabled': 0})
for script in enabled_server_scripts:
if script.script_type == 'DocType Event':
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
elif script.script_type == 'Permission Query':
script_map['permission_query'][script.reference_doctype] = script.name
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
return script_map
return script_map

View file

@ -48,6 +48,13 @@ frappe.flags = 'hello'
'''
),
dict(
name='test_permission_query',
script_type = 'Permission Query',
reference_doctype = 'ToDo',
script = '''
conditions = '1 = 1'
'''),
dict(
name='test_invalid_namespace_method',
script_type = 'DocType Event',
doctype_event = 'Before Insert',
@ -95,6 +102,10 @@ class TestServerScript(unittest.TestCase):
def test_api_return(self):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
def test_permission_query(self):
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
def test_attribute_error(self):
"""Raise AttributeError if method not found in Namespace"""
note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})

View file

@ -98,15 +98,16 @@ class User(Document):
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
now=frappe.flags.in_test or frappe.flags.in_install
self.send_password_notification(self.__new_password)
frappe.enqueue(
'frappe.core.doctype.user.user.create_contact',
user=self,
ignore_mandatory=True,
now=frappe.flags.in_test or frappe.flags.in_install
now=now
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now)
# Set user selected timezone
if self.time_zone:

View file

@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", {
} else {
f._sortable = false;
}
if (f.fieldtype == "Table") {
frm.add_custom_button(f.options, function() {
frm.set_value('doc_type', f.options);
}, __('Customize Child Table'));
}
});
frm.fields_dict.fields.grid.refresh();
},

View file

@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` (
DROP TABLE IF EXISTS `tabSeries`;
CREATE TABLE `tabSeries` (
`name` varchar(100) DEFAULT NULL,
`name` varchar(100),
`current` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY(`name`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.config import get_modules_from_all_apps_for_user
import frappe
from frappe import _
import json
@ -42,6 +43,24 @@ class Dashboard(Document):
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
def get_permission_query_conditions(user):
if not user:
user = frappe.session.user
if user == 'Administrator':
return
roles = frappe.get_roles(user)
if "System Manager" in roles:
return None
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format(
allowed_modules=','.join(allowed_modules))
return module_condition
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []

View file

@ -13,12 +13,12 @@ from frappe.utils.dateutils import\
get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
def get_permission_query_conditions(user):
if not user:
user = frappe.session.user
@ -31,9 +31,11 @@ def get_permission_query_conditions(user):
doctype_condition = False
report_condition = False
module_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()]
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
@ -41,18 +43,24 @@ def get_permission_query_conditions(user):
if allowed_reports:
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
allowed_reports=','.join(allowed_reports))
if allowed_modules:
module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules})
or `tabDashboard Chart`.`module` is NULL'''.format(
allowed_modules=','.join(allowed_modules))
return '''
(`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
and {doctype_condition})
or
(`tabDashboard Chart`.`chart_type` = 'Report'
and {report_condition})
'''.format(
doctype_condition=doctype_condition,
report_condition=report_condition
)
((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
and {doctype_condition})
or
(`tabDashboard Chart`.`chart_type` = 'Report'
and {report_condition}))
and
({module_condition})
'''.format(
doctype_condition=doctype_condition,
report_condition=report_condition,
module_condition=module_condition
)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)

View file

@ -8,6 +8,7 @@ from frappe.model.document import Document
from frappe.utils import cint
from frappe.model.naming import append_number_if_name_exists
from frappe.modules.export_file import export_to_files
from frappe.config import get_modules_from_all_apps_for_user
class NumberCard(Document):
def autoname(self):
@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None):
return None
doctype_condition = False
module_condition = False
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
if allowed_doctypes:
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
allowed_doctypes=','.join(allowed_doctypes))
if allowed_modules:
module_condition = '''`tabNumber Card`.`module` in ({allowed_modules})
or `tabNumber Card`.`module` is NULL'''.format(
allowed_modules=','.join(allowed_modules))
return '''
{doctype_condition}
'''.format(doctype_condition=doctype_condition)
{doctype_condition}
and
{module_condition}
'''.format(doctype_condition=doctype_condition, module_condition=module_condition)
def has_permission(doc, ptype, user):
roles = frappe.get_roles(user)

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
@ -8,6 +9,8 @@
"engine": "InnoDB",
"field_order": [
"subject",
"use_html",
"response_html",
"response",
"owner",
"section_break_4",
@ -22,11 +25,12 @@
"reqd": 1
},
{
"depends_on": "eval:!doc.use_html",
"fieldname": "response",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Response",
"reqd": 1
"mandatory_depends_on": "eval:!doc.use_html"
},
{
"default": "user",
@ -45,10 +49,24 @@
"fieldtype": "HTML",
"label": "Email Reply Help",
"options": "<h4>Email Reply Example</h4>\n\n<pre>Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>\n"
},
{
"default": "0",
"fieldname": "use_html",
"fieldtype": "Check",
"label": "Use HTML"
},
{
"depends_on": "eval:doc.use_html",
"fieldname": "response_html",
"fieldtype": "Code",
"label": "Response ",
"options": "HTML"
}
],
"icon": "fa fa-comment",
"modified": "2019-10-30 14:15:00.956347",
"links": [],
"modified": "2020-11-30 14:12:50.321633",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Template",

View file

@ -9,7 +9,29 @@ from six import string_types
class EmailTemplate(Document):
def validate(self):
validate_template(self.response)
if self.use_html:
validate_template(self.response_html)
else:
validate_template(self.response)
def get_formatted_subject(self, doc):
return frappe.render_template(self.subject, doc)
def get_formatted_response(self, doc):
if self.use_html:
return frappe.render_template(self.response_html, doc)
return frappe.render_template(self.response, doc)
def get_formatted_email(self, doc):
if isinstance(doc, string_types):
doc = json.loads(doc)
return {
"subject" : self.get_formatted_subject(doc),
"message" : self.get_formatted_response(doc)
}
@frappe.whitelist()
def get_email_template(template_name, doc):
@ -18,5 +40,4 @@ def get_email_template(template_name, doc):
doc = json.loads(doc)
email_template = frappe.get_doc("Email Template", template_name)
return {"subject" : frappe.render_template(email_template.subject, doc),
"message" : frappe.render_template(email_template.response, doc)}
return email_template.get_formatted_email(doc)

View file

@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator):
self.db_set("scheduled_to_send", len(self.recipients))
def get_message(self):
if self.content_type == "HTML":
return frappe.render_template(self.message_html, {"doc": self.as_dict()})
return {
'Rich Text': self.message,
'Markdown': markdown(self.message_md),
'HTML': self.message_html
'Markdown': markdown(self.message_md)
}[self.content_type or 'Rich Text']
def get_recipients(self):

View file

@ -94,6 +94,7 @@ permission_query_conditions = {
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
"Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions",
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",

View file

@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path):
Returns:
str: Path of the decompressed SQL file
"""
from frappe.utils import get_bench_relative_path
sql_file_path = get_bench_relative_path(sql_file_path)
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
if not os.path.exists(sql_file_path):
base_path = '..'
sql_file_path = os.path.join(base_path, sql_file_path)
if not os.path.exists(sql_file_path):
print('Invalid path {0}'.format(sql_file_path[3:]))
sys.exit(1)
elif sql_file_path.startswith(os.sep):
base_path = os.sep
else:
base_path = '.'
if sql_file_path.endswith('sql.gz'):
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
decompressed_file_name = extract_sql_gzip(sql_file_path)
else:
decompressed_file_name = sql_file_path
@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path):
return decompressed_file
def extract_files(site_name, file_path, folder_name):
def extract_files(site_name, file_path):
import shutil
import subprocess
from frappe.utils import get_bench_relative_path
file_path = get_bench_relative_path(file_path)
# Need to do frappe.init to maintain the site locals
frappe.init(site=site_name)

View file

@ -18,12 +18,9 @@
"bucket",
"endpoint_url",
"column_break_13",
"region",
"backup_details_section",
"frequency",
"backup_files",
"column_break_18",
"backup_limit"
"backup_files"
],
"fields": [
{
@ -42,7 +39,7 @@
},
{
"default": "1",
"description": "Note: By default emails for failed backups are sent.",
"description": "By default, emails are only sent for failed backups.",
"fieldname": "send_email_for_successful_backup",
"fieldtype": "Check",
"label": "Send Email for Successful Backup"
@ -73,14 +70,7 @@
"reqd": 1
},
{
"default": "us-east-1",
"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\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"
},
{
"default": "https://s3.amazonaws.com",
"fieldname": "endpoint_url",
"fieldtype": "Data",
"label": "Endpoint URL"
@ -92,14 +82,6 @@
"mandatory_depends_on": "enabled",
"reqd": 1
},
{
"description": "Set to 0 for no limit on the number of backups taken",
"fieldname": "backup_limit",
"fieldtype": "Int",
"label": "Backup Limit",
"mandatory_depends_on": "enabled",
"reqd": 1
},
{
"depends_on": "enabled",
"fieldname": "api_access_section",
@ -142,16 +124,12 @@
"fieldname": "backup_files",
"fieldtype": "Check",
"label": "Backup Files"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2020-07-27 17:27:21.400000",
"modified": "2020-12-07 15:30:55.047689",
"modified_by": "Administrator",
"module": "Integrations",
"name": "S3 Backup Settings",
@ -172,4 +150,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -24,6 +24,7 @@ class S3BackupSettings(Document):
if not self.endpoint_url:
self.endpoint_url = 'https://s3.amazonaws.com'
conn = boto3.client(
's3',
aws_access_key_id=self.access_key_id,
@ -31,25 +32,21 @@ class S3BackupSettings(Document):
endpoint_url=self.endpoint_url
)
bucket_lower = str(self.bucket)
try:
conn.list_buckets()
except ClientError:
frappe.throw(_("Invalid Access Key ID or Secret Access Key."))
try:
# Head_bucket returns a 200 OK if the bucket exists and have access to it.
conn.head_bucket(Bucket=bucket_lower)
# Requires ListBucket permission
conn.head_bucket(Bucket=self.bucket)
except ClientError as e:
error_code = e.response['Error']['Code']
bucket_name = frappe.bold(self.bucket)
if error_code == '403':
frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower))
else: # '400'-Bad request or '404'-Not Found return
# try to create bucket
conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={
'LocationConstraint': self.region})
msg = _("Do not have permission to access bucket {0}.").format(bucket_name)
elif error_code == '404':
msg = _("Bucket {0} not found.").format(bucket_name)
else:
msg = e.args[0]
frappe.throw(msg)
@frappe.whitelist()
@ -70,11 +67,13 @@ def take_backups_weekly():
def take_backups_monthly():
take_backups_if("Monthly")
def take_backups_if(freq):
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
take_backups_s3()
@frappe.whitelist()
def take_backups_s3(retry_count=0):
try:
@ -146,42 +145,13 @@ def backup_to_s3():
if files_filename:
upload_file_to_s3(files_filename, folder, conn, bucket)
delete_old_backups(doc.backup_limit, bucket)
def upload_file_to_s3(filename, folder, conn, bucket):
destpath = os.path.join(folder, os.path.basename(filename))
try:
print("Uploading file:", filename)
conn.upload_file(filename, bucket, destpath)
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission
except Exception as e:
frappe.log_error()
print("Error uploading: %s" % (e))
def delete_old_backups(limit, bucket):
all_backups = []
doc = frappe.get_single("S3 Backup Settings")
backup_limit = int(limit)
s3 = boto3.resource(
's3',
aws_access_key_id=doc.access_key_id,
aws_secret_access_key=doc.get_password('secret_access_key'),
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com'
)
bucket = s3.Bucket(bucket)
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/')
if objects:
for obj in objects.get('CommonPrefixes'):
all_backups.append(obj.get('Prefix'))
oldest_backup = sorted(all_backups)[0] if all_backups else ''
if len(all_backups) > backup_limit:
print("Deleting Backup: {0}".format(oldest_backup))
for obj in bucket.objects.filter(Prefix=oldest_backup):
# delete all keys that are inside the oldest_backup
s3.Object(bucket.name, obj.key).delete()

View file

@ -802,12 +802,12 @@ class BaseDocument(object):
if translated:
val = _(val)
if absolute_value and isinstance(val, (int, float)):
val = abs(self.get(fieldname))
if not doc:
doc = getattr(self, "parent_doc", None) or self
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
val = abs(self.get(fieldname))
return format_value(val, df=df, doc=doc, currency=currency)
def is_print_hide(self, fieldname, df=None, for_print=True):

View file

@ -18,6 +18,7 @@ from frappe.client import check_parent_permission
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
from frappe.model.meta import get_table_columns
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
class DatabaseQuery(object):
def __init__(self, doctype, user=None):
@ -683,15 +684,23 @@ class DatabaseQuery(object):
self.match_filters.append(match_filters)
def get_permission_query_conditions(self):
conditions = []
condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, [])
if condition_methods:
conditions = []
for method in condition_methods:
c = frappe.call(frappe.get_attr(method), self.user)
if c:
conditions.append(c)
return " and ".join(conditions) if conditions else None
permision_script_name = get_server_script_map().get("permission_query").get(self.doctype)
if permision_script_name:
script = frappe.get_doc("Server Script", permision_script_name)
condition = script.get_permission_query_conditions(self.user)
if condition:
conditions.append(condition)
return " and ".join(conditions) if conditions else ""
def run_custom_query(self, query):
if '%(key)s' in query:

View file

@ -209,7 +209,8 @@ class Meta(Document):
'owner': _('Created By'),
'modified_by': _('Modified By'),
'creation': _('Created On'),
'modified': _('Last Modified On')
'modified': _('Last Modified On'),
'_assign': _('Assigned To')
}.get(fieldname) or _('No Label')
return label

View file

@ -21,6 +21,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
execute:frappe.reload_doc('core', 'doctype', 'has_role')
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
execute:frappe.reload_doc('core', 'doctype', 'server_script')
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
frappe.patches.v7_1.rename_scheduler_log_to_error_log

View file

@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", {
}
frm.trigger('render_buttons');
frm.toggle_display('standard', frappe.boot.developer_mode);
frm.trigger('hide_absolute_value_field');
},
render_buttons: function (frm) {
frm.page.clear_inner_toolbar();
@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", {
frm.set_value('show_section_headings', value);
frm.set_value('line_breaks', value);
frm.trigger('render_buttons');
},
doc_type: function (frm) {
frm.trigger('hide_absolute_value_field');
},
hide_absolute_value_field: function (frm) {
// TODO: make it work with frm.doc.doc_type
// Problem: frm isn't updated in some random cases
const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type;
if (doctype) {
frappe.model.with_doctype(doctype, () => {
const meta = frappe.get_meta(doctype);
const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype));
frm.toggle_display('absolute_value', has_int_float_currency_field.length);
});
}
}
})
});

View file

@ -22,6 +22,7 @@
"align_labels_right",
"show_section_headings",
"line_breaks",
"absolute_value",
"column_break_11",
"font",
"css_section",
@ -196,13 +197,21 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Print Format Builder"
},
{
"default": "0",
"depends_on": "doc_type",
"description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive",
"fieldname": "absolute_value",
"fieldtype": "Check",
"label": "Show Absolute Values"
}
],
"icon": "fa fa-print",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-27 18:27:58.307070",
"modified": "2020-12-14 11:38:49.132061",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",

View file

@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
.replace('%H', 'HH')
.replace('%M', 'mm')
.replace('%S', 'ss')
.replace('%b', 'Mon')
: null;
let column_title = `<span class="indicator green">

View file

@ -148,7 +148,6 @@ frappe.Application = Class.extend({
user: frappe.session.user
},
callback: function(r) {
console.log(r);
if(r.message.show_alert){
frappe.show_alert({
indicator: 'red',

View file

@ -50,7 +50,15 @@ frappe.form.formatters = {
return frappe.form.formatters._right(value==null ? "" : cint(value), options)
},
Percent: function(value, docfield, options) {
return frappe.form.formatters._right(flt(value, 2) + "%", options)
const precision = (
docfield.precision
|| cint(
frappe.boot.sysdefaults
&& frappe.boot.sysdefaults.float_precision
)
|| 2
);
return frappe.form.formatters._right(flt(value, precision) + "%", options);
},
Rating: function(value) {
return `<span class="rating">

View file

@ -36,9 +36,14 @@ frappe.ui.form.QuickEntryForm = Class.extend({
this.render_dialog();
resolve(this);
} else {
// no quick entry, open full form
frappe.quick_entry = null;
frappe.set_route('Form', this.doctype, this.doc.name)
.then(() => resolve(this));
// call init_callback for consistency
if (this.init_callback) {
this.init_callback(this.doc);
}
}
});
});

View file

@ -89,11 +89,19 @@ frappe.render_template = function(name, data) {
}
frappe.render_grid = function(opts) {
// build context
if(opts.grid) {
if (opts.grid) {
opts.columns = opts.grid.getColumns();
opts.data = opts.grid.getData().getItems();
}
if (
opts.print_settings &&
opts.print_settings.orientation &&
opts.print_settings.orientation.toLowerCase() === "landscape"
) {
opts.landscape = true;
}
// show landscape view if columns more than 10
if (opts.landscape == null) {
if(opts.columns && opts.columns.length > 10) {

View file

@ -103,6 +103,31 @@ $.extend(frappe.model, {
return docfield[0];
},
get_from_localstorage: function(doctype) {
if (localStorage["_doctype:" + doctype]) {
return JSON.parse(localStorage["_doctype:" + doctype]);
}
},
set_in_localstorage: function(doctype, docs) {
try {
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
} catch(e) {
// if quota is exceeded, clear local storage and set item
console.warn("localStorage quota exceeded, clearing doctype cache")
frappe.model.clear_local_storage();
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
}
},
clear_local_storage: function() {
for(var key in localStorage) {
if (key.startsWith("_doctype:")) {
localStorage.removeItem(key);
}
}
},
with_doctype: function(doctype, callback, async) {
if(locals.DocType[doctype]) {
callback && callback();
@ -110,13 +135,15 @@ $.extend(frappe.model, {
let cached_timestamp = null;
let cached_doc = null;
if(localStorage["_doctype:" + doctype]) {
let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]);
let cached_docs = frappe.model.get_from_localstorage(doctype)
if (cached_docs) {
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
if(cached_doc) {
cached_timestamp = cached_doc.modified;
}
}
return frappe.call({
method:'frappe.desk.form.load.getdoctype',
type: "GET",
@ -134,7 +161,7 @@ $.extend(frappe.model, {
if(r.message=="use_cache") {
frappe.model.sync(cached_doc);
} else {
localStorage["_doctype:" + doctype] = JSON.stringify(r.docs);
frappe.model.set_in_localstorage(doctype, r.docs)
}
frappe.model.init_doctype(doctype);

View file

@ -979,17 +979,42 @@ Object.assign(frappe.utils, {
return route;
},
shorten_number: function (number, country) {
country = (country == 'India') ? country : '';
shorten_number: function (number, country, min_length=4, max_no_of_decimals=2) {
/* returns the number as an abbreviated string
* PARAMS
* number - number to be shortened
* country - country that determines the numnber system to be used
* min_length - length below which the number will not be shortened
* max_no_of_decimals - max number of decimals of the shortened number
*/
// return number if total digits is lesser than min_length
const len = String(number).match(/\d/g).length;
if (len < min_length) return number.toString();
const number_system = this.get_number_system(country);
let x = Math.abs(Math.round(number));
for (const map of number_system) {
const condition = map.condition ? map.condition(x) : x >= map.divisor;
if (condition) {
return (number/map.divisor).toFixed(2) + ' ' + map.symbol;
if (x >= map.divisor) {
let result = number/map.divisor;
const no_of_decimals = this.get_number_of_decimals(result);
/*
If no_of_decimals is greater than max_no_of_decimals,
round the number to max_no_of_decimals
*/
result = no_of_decimals > max_no_of_decimals
? result.toFixed(max_no_of_decimals)
: result;
return result + ' ' + map.symbol;
}
}
return number.toFixed();
return number.toFixed(max_no_of_decimals);
},
get_number_of_decimals: function (number) {
if (Math.floor(number) === number) return 0;
return number.toString().split(".")[1].length || 0;
},
get_number_system: function (country) {
@ -1019,9 +1044,11 @@ Object.assign(frappe.utils, {
{
divisor: 1.0e+3,
symbol: 'K',
condition: (num) => num.toFixed().length > 5
}]
};
if (!Object.keys(number_system_map).includes(country)) country = '';
return number_system_map[country];
},
});

View file

@ -1112,7 +1112,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
get_filter_values(raise) {
const mandatory = this.filters.filter(f => f.df.reqd);
// check for mandatory property for filters added via UI
const mandatory = this.filters.filter(f => (f.df.reqd || f.df.mandatory));
const missing_mandatory = mandatory.filter(f => !f.get_value());
if (raise && missing_mandatory.length > 0) {
let message = __('Please set filters');

View file

@ -204,7 +204,7 @@ export default class NumberCardWidget extends Widget {
get_formatted_number(df) {
const default_country = frappe.sys_defaults.country;
const shortened_number = frappe.utils.shorten_number(this.number, default_country);
const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5);
let number_parts = shortened_number.split(' ');
const symbol = number_parts[1] || '';

View file

@ -137,9 +137,10 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% elif df.fieldtype=="HTML" %}
{{ frappe.render_template(df.options, {"doc":doc}) }}
{% elif df.fieldtype=="Currency" %}
{{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }}
{% else %}
{{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
{% else %}
{%- set parent = parent_doc or doc -%}
{{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }}
{% endif %}
{%- endmacro %}

View file

@ -14,7 +14,7 @@ import glob
import frappe
import frappe.recorder
from frappe.installer import add_to_installed_apps
from frappe.utils import add_to_date, now
from frappe.utils import add_to_date, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups
@ -364,3 +364,21 @@ class TestCommands(BaseTestCommands):
else:
installed_apps = set(frappe.get_installed_apps())
self.assertSetEqual(list_apps, installed_apps)
def test_get_bench_relative_path(self):
bench_path = frappe.utils.get_bench_path()
test1_path = os.path.join(bench_path, "test1.txt")
test2_path = os.path.join(bench_path, "sites", "test2.txt")
with open(test1_path, "w+") as test1:
test1.write("asdf")
with open(test2_path, "w+") as test2:
test2.write("asdf")
self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
with self.assertRaises(SystemExit):
get_bench_relative_path("test3.txt")
os.remove(test1_path)
os.remove(test2_path)

View file

@ -734,3 +734,27 @@ def get_build_version():
# .build can sometimes not exist
# this is not a major problem so send fallback
return frappe.utils.random_string(8)
def get_bench_relative_path(file_path):
"""Fixes paths relative to the bench root directory if exists and returns the absolute path
Args:
file_path (str, Path): Path of a file that exists on the file system
Returns:
str: Absolute path of the file_path
"""
if not os.path.exists(file_path):
base_path = '..'
elif file_path.startswith(os.sep):
base_path = os.sep
else:
base_path = '.'
file_path = os.path.join(base_path, file_path)
if not os.path.exists(file_path):
print('Invalid path {0}'.format(file_path[3:]))
sys.exit(1)
return os.path.abspath(file_path)

View file

@ -369,6 +369,8 @@ def format_duration(seconds, hide_days=False):
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
"""
seconds = cint(seconds)
total_duration = {
'days': math.floor(seconds / (3600 * 24)),
@ -1321,12 +1323,14 @@ def generate_hash(*args, **kwargs):
def guess_date_format(date_string):
DATE_FORMATS = [
r"%d/%b/%y",
r"%d-%m-%Y",
r"%m-%d-%Y",
r"%Y-%m-%d",
r"%d-%m-%y",
r"%m-%d-%y",
r"%y-%m-%d",
r"%y-%b-%d",
r"%d/%m/%Y",
r"%m/%d/%Y",
r"%Y/%m/%d",

View file

@ -100,6 +100,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
doc.print_section_headings = print_format.show_section_headings
doc.print_line_breaks = print_format.line_breaks
doc.align_labels_right = print_format.align_labels_right
doc.absolute_value = print_format.absolute_value
def get_template_from_string():
return jenv.from_string(get_print_format(doc.doctype,

View file

@ -28,7 +28,7 @@
"driver.js": "^0.9.8",
"express": "^4.17.1",
"fast-deep-equal": "^2.0.1",
"frappe-charts": "^1.5.1",
"frappe-charts": "^1.5.5",
"frappe-datatable": "^1.15.3",
"frappe-gantt": "^0.5.0",
"fuse.js": "^3.4.6",

View file

@ -2299,10 +2299,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
frappe-charts@^1.5.1:
version "1.5.4"
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b"
integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw==
frappe-charts@^1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.5.tgz#5f44a3639aecc6f8fc7d15752abc80bb68e26734"
integrity sha512-L9pJTsrSuRobS/EaBKT8i1x+DVOjkXyUwT85cteZAPqynU/7K+uqjQOy4tMSTv5zsTWJNWFJ37ax68T73YdR3g==
frappe-datatable@^1.15.3:
version "1.15.3"
@ -2918,9 +2918,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inquirer@^7.3.3:
version "7.3.3"