diff --git a/frappe/__init__.py b/frappe/__init__.py index fac0927428..7d15fc716e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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 "[]") diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index a11de1d881..c2c84692d8 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -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", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..5ff4cbeead 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -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", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..830af68de7 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -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: diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..0d6229cd9e 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -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() \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat_day/__init__.py b/frappe/automation/doctype/auto_repeat_day/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json new file mode 100644 index 0000000000..6f5c3060cd --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json @@ -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 +} \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py new file mode 100644 index 0000000000..3a7ced1370 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class AutoRepeatDay(Document): + pass diff --git a/frappe/commands/site.py b/frappe/commands/site.py index bc65aa178c..4a631be3ac 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -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 diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json index 9d277db11d..964294b96e 100644 --- a/frappe/core/doctype/report_filter/report_filter.json +++ b/frappe/core/doctype/report_filter/report_filter.json @@ -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", diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 78ef2d0509..a317d69166 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { setup_help(frm) { frm.get_field('help_html').html(` -

Examples

DocType Event

-

+

Add logic for standard doctype events like Before Insert, After Submit, etc.

+
+	
 # 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()
-
+if doc.allocated_to: + frappe.get_doc(dict( + doctype = 'ToDo' + owner = doc.allocated_to, + description = doc.subject + )).insert() +
+
+

API Call

+

Respond to /api/method/<method-name> calls, just like whitelisted methods


 # respond to API
 
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
 else:
 	frappe.response['message'] = "ok"
 
+ +
+ +

Permission Query

+

Add conditions to the where clause of list queries.

+

+# 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
+
`); } diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 420f96ec2f..94a48f196c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -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", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index ded397d5e3..88d68dba14 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -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)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index e03504f30b..4dc4f12b34 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -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 \ No newline at end of file + return script_map diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 256cea57d7..957cbbf72d 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -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"}) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 7309528da6..da4026d8fd 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -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: diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 2d220b864c..17343573ed 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -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(); }, diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index 15b0bed699..a52efd01e3 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -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; diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index b12bcfe27d..fa03bf8f80 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -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 = [] diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 3f8d7c3c79..2fa36b5514 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -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) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index d4a2b00c57..6bddd09fc7 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -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) diff --git a/frappe/email/doctype/email_template/email_template.json b/frappe/email/doctype/email_template/email_template.json index 0d0922f16f..dc73acacc1 100644 --- a/frappe/email/doctype/email_template/email_template.json +++ b/frappe/email/doctype/email_template/email_template.json @@ -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": "

Email Reply Example

\n\n
Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n
\n\n

How to get fieldnames

\n\n

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 > Customize Form View and selecting the document type (e.g. Sales Invoice)

\n\n

Templating

\n\n

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

\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", diff --git a/frappe/email/doctype/email_template/email_template.py b/frappe/email/doctype/email_template/email_template.py index 2743032331..6708e9dd3f 100644 --- a/frappe/email/doctype/email_template/email_template.py +++ b/frappe/email/doctype/email_template/email_template.py @@ -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)} \ No newline at end of file + return email_template.get_formatted_email(doc) \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a4d60706eb..2791ebb75b 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -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): diff --git a/frappe/hooks.py b/frappe/hooks.py index d8c8cd841c..3d7ae0abb4 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -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", diff --git a/frappe/installer.py b/frappe/installer.py index 1245a08cb7..a11c8dfbfa 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -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) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 123bb21e88..2ca1723cb2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -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 -} +} \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 7c90d37f82..308d34c5c2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -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() diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 0a219b4253..5d86b3bac8 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -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): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ace9b04cec..b936251b50 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -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: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8c17a5b19b..c740d495c1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -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 diff --git a/frappe/patches.txt b/frappe/patches.txt index 0daf29e001..b459019dd7 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -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 diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index e6599b2496..786f8f97ab 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -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); + }); + } } -}) +}); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 63448ccc39..3a47fb554f 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -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", diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 6c17cb4351..477cfb0786 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -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 = ` diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c8ed29fb76..5fa7a9dbcb 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -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', diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 3f422d0a9b..f9a1d0b643 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -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 ` diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 2da7b8f236..eed49e070b 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -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); + } } }); }); diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index d233a47893..7b45db952e 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -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) { diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 308d9bd5f8..1d302215dd 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -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); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 4bf9c5bbd8..f8f25293b3 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -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]; }, }); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 60abb187ae..eccfa9c089 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -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'); diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 8855de435b..c41f9bc6e7 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -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] || ''; diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 3681a87f53..0d904bb59c 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -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 %} diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 8c76ce2f48..0786a0e14f 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -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) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 3ae89936e7..5ac4de618d 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -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) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 34659e1cac..4a88b5fda1 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -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", diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 545e5d581d..71316dc48c 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -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, diff --git a/package.json b/package.json index 2ef6ceab2d..8603d8e071 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5cbea3f938..072810faa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"