diff --git a/.travis.yml b/.travis.yml index 63895675ea..2331217363 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,12 +31,12 @@ matrix: - name: "Python 3.7 MariaDB" python: 3.7 env: DB=mariadb TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Python 3.7 PostgreSQL" python: 3.7 env: DB=postgres TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Cypress" python: 3.7 diff --git a/frappe/__init__.py b/frappe/__init__.py index 4cde8bfe0e..b0d36e1020 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -327,7 +327,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal """ - from frappe.utils import encode + from frappe.utils import strip_html_tags msg = safe_decode(msg) out = _dict(message=msg) @@ -354,7 +354,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, out.as_list = 1 if flags.print_messages and out.message: - print(f"Message: {repr(out.message).encode('utf-8')}") + print(f"Message: {strip_html_tags(out.message)}") if title: out.title = title diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index 30b3b17fb4..06d15d6d2c 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -100,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 80975dd4f5..5ff4cbeead 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -23,6 +23,8 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", + "section_break_12", + "repeat_on_days", "notification", "notify_by_email", "recipients", @@ -189,6 +191,18 @@ "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", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 31d6539e61..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,9 +14,10 @@ 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): @@ -24,6 +26,7 @@ class AutoRepeat(Document): 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() @@ -49,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: @@ -88,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") @@ -113,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, @@ -122,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 = { @@ -132,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 @@ -211,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 '' @@ -291,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' @@ -337,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) @@ -346,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()) @@ -358,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']}) @@ -367,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 e40b12e3b9..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) @@ -144,7 +189,8 @@ def make_auto_repeat(**args): '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 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/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 09187b5467..6cca646fc9 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -289,9 +289,15 @@ class DocType(Document): self.update_fields_to_fetch() - from frappe import conf - allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode')) - if not self.custom and not frappe.flags.in_import and allow_doctype_export: + allow_doctype_export = ( + not self.custom + and not frappe.flags.in_import + and ( + frappe.conf.developer_mode + or frappe.flags.allow_doctype_export + ) + ) + if allow_doctype_export: self.export_doc() self.make_controller_template() @@ -369,13 +375,10 @@ class DocType(Document): if merge: frappe.throw(_("DocType can not be merged")) - # Do not rename and move files and folders for custom doctype - if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: - self.rename_files_and_folders(old, new) - def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" + if self.issingle: frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) frappe.db.sql("""update tabSingles set value=%s @@ -385,6 +388,20 @@ class DocType(Document): "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" }) + frappe.db.commit() + + # Do not rename and move files and folders for custom doctype + if not self.custom: + if not frappe.flags.in_patch: + self.rename_files_and_folders(old, new) + + for site in frappe.utils.get_sites(): + frappe.cache().delete(f"{site}:doctype_classes", old) + + def after_delete(self): + if not self.custom: + for site in frappe.utils.get_sites(): + frappe.cache().delete(f"{site}:doctype_classes", self.name) def rename_files_and_folders(self, old, new): # move files diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 930c46e60b..7e63572162 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -43,7 +43,7 @@ class ModuleDef(Document): def on_trash(self): """Delete module name from modules.txt""" - if frappe.flags.in_uninstall or self.custom: + if not frappe.conf.get('developer_mode') or frappe.flags.in_uninstall or self.custom: return modules = None 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/user/user.py b/frappe/core/doctype/user/user.py index 6f5c805a54..d6cbea8bc5 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/model/delete_doc.py b/frappe/model/delete_doc.py index f3710de39b..7b29692ad1 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -76,7 +76,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa delete_from_table(doctype, name, ignore_doctypes, None) - if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_uninstall or frappe.flags.in_test): + if frappe.conf.developer_mode and not doc.custom and not ( + for_reload + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_uninstall + ): try: delete_controllers(name, doc.module) except (FileNotFoundError, OSError, KeyError): diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 35fbf94dc6..2baf0c562c 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -49,9 +49,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F old_doc = frappe.get_doc(doctype, old) out = old_doc.run_method("before_rename", old, new, merge) or {} new = (out.get("new") or new) if isinstance(out, dict) else (out or new) - - if doctype != "DocType": - new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) + new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) if not merge: rename_parent_and_child(doctype, old, new, meta) @@ -250,6 +248,7 @@ def update_link_field_values(link_fields, old, new, doctype): pass else: parent = field['parent'] + docfield = field["fieldname"] # Handles the case where one of the link fields belongs to # the DocType being renamed. @@ -261,11 +260,8 @@ def update_link_field_values(link_fields, old, new, doctype): if parent == new and doctype == "DocType": parent = old - frappe.db.sql(""" - update `tab{table_name}` set `{fieldname}`=%s - where `{fieldname}`=%s""".format( - table_name=parent, - fieldname=field['fieldname']), (new, old)) + frappe.db.set_value(parent, {docfield: old}, docfield, new) + # update cached link_fields as per new if doctype=='DocType' and field['parent'] == old: field['parent'] = new diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 72ce8c9ce4..43e26cc5d0 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -53,14 +53,17 @@ def get_transitions(doc, workflow = None, raise_exception=False): return transitions def get_workflow_safe_globals(): - # access to frappe.db.get_value and frappe.db.get_list + # access to frappe.db.get_value, frappe.db.get_list, and date time utils. return dict( frappe=frappe._dict( - db=frappe._dict( - get_value=frappe.db.get_value, - get_list=frappe.db.get_list + db=frappe._dict(get_value=frappe.db.get_value, get_list=frappe.db.get_list), + session=frappe.session, + utils=frappe._dict( + now_datetime=frappe.utils.now_datetime, + add_to_date=frappe.utils.add_to_date, + get_datetime=frappe.utils.get_datetime, + now=frappe.utils.now, ), - session=frappe.session ) ) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 9ef5652dda..786f8f97ab 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", { 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]; + const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; if (doctype) { frappe.model.with_doctype(doctype, () => { const meta = frappe.get_meta(doctype); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 6e51ef0018..92d4a67d14 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -201,17 +201,17 @@ { "default": "0", "depends_on": "doc_type", - "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "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" + "label": "Show Absolute Values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-10 18:58:55.598269", + "modified": "2020-12-14 11:38:49.132061", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 27b3b869c5..27ddaa0712 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 d80aaf5107..4692e66140 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/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index d432e553f1..67aeb4474e 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -86,7 +86,7 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ var f = this.fields_dict[key]; if (f.get_value) { var v = f.get_value(); - if (f.df.reqd && is_null(v)) + if (f.df.reqd && is_null(strip_html(v))) errors.push(__(f.df.label)); if (f.df.reqd diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 80d0aa71da..a338feda08 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1132,7 +1132,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/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 4dc1a50bc4..0421147d49 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -103,6 +103,7 @@ export default class WebForm extends frappe.ui.FieldGroup { } save() { + let is_new = this.is_new; if (this.validate && !this.validate()) { frappe.throw(__("Couldn't save, please check the data you have entered"), __("Validation Error")); } @@ -139,6 +140,18 @@ export default class WebForm extends frappe.ui.FieldGroup { this.handle_success(response.message); frappe.web_form.events.trigger('after_save'); this.after_save && this.after_save(); + // args doctype and docname added to link doctype in file manager + if (is_new) { + frappe.call({ + type: 'POST', + method: "frappe.handler.upload_file", + args: { + file_url: response.message.attachment, + doctype: response.message.doctype, + docname: response.message.name + } + }); + } } }, always: function() { diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index c962f93fb1..93409a15a4 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -173,7 +173,7 @@ export default class NumberCardWidget extends Widget { get_number_for_custom_card(res) { if (typeof res === 'object') { this.number = res.value; - this.get_formatted_number(res); + this.set_formatted_number(res); } else { this.formatted_number = res; } @@ -185,7 +185,7 @@ export default class NumberCardWidget extends Widget { return frappe.model.with_doctype(this.card_doc.document_type, () => { const based_on_df = frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); - this.get_formatted_number(based_on_df); + this.set_formatted_number(based_on_df); }); } else { this.formatted_number = res; @@ -200,10 +200,10 @@ export default class NumberCardWidget extends Widget { }, []); const col = res.columns.find(col => col.fieldname == field); this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals); - this.get_formatted_number(col); + this.set_formatted_number(col); } - get_formatted_number(df) { + set_formatted_number(df) { const default_country = frappe.sys_defaults.country; const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5); let number_parts = shortened_number.split(' '); @@ -257,11 +257,17 @@ export default class NumberCardWidget extends Widget { }; const stats_qualifier = stats_qualifier_map[this.card_doc.stats_time_interval]; + let get_stat = () => { + const parts = this.percentage_stat.split(' '); + const symbol = parts[1] || ''; + return Math.abs(parts[0]) + ' ' + symbol; + }; + $(this.body).find('.widget-content').append(`
${caret_html} - ${Math.abs(this.percentage_stat)} % + ${get_stat()} % diff --git a/frappe/public/js/lib/highlight.pack.js b/frappe/public/js/lib/highlight.pack.js deleted file mode 100755 index ecee8ad109..0000000000 --- a/frappe/public/js/lib/highlight.pack.js +++ /dev/null @@ -1 +0,0 @@ -var hljs=new function(){function e(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function n(e,t){var n=e&&e.exec(t);return n&&0==n.index}function r(e){var t=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return t=t.map(function(e){return e.replace(/^lang(uage)?-/,"")}),t.filter(function(e){return m(e)||/no(-?)highlight/.test(e)})[0]}function i(e,t){var n={};for(var r in e)n[r]=e[r];if(t)for(var r in t)n[r]=t[r];return n}function a(e){var n=[];return function r(e,i){for(var a=e.firstChild;a;a=a.nextSibling)3==a.nodeType?i+=a.nodeValue.length:1==a.nodeType&&(n.push({event:"start",offset:i,node:a}),i=r(a,i),t(a).match(/br|hr|img|input/)||n.push({event:"stop",offset:i,node:a}));return i}(e,0),n}function s(n,r,i){function a(){return n.length&&r.length?n[0].offset!=r[0].offset?n[0].offset"}function o(e){l+=""}function c(e){("start"==e.event?s:o)(e.node)}for(var u=0,l="",f=[];n.length||r.length;){var h=a();if(l+=e(i.substr(u,h[0].offset-u)),u=h[0].offset,h==n){f.reverse().forEach(o);do c(h.splice(0,1)[0]),h=a();while(h==n&&h.length&&h[0].offset==u);f.reverse().forEach(s)}else"start"==h[0].event?f.push(h[0].node):f.pop(),c(h.splice(0,1)[0])}return l+e(i.substr(u))}function o(e){function t(e){return e&&e.source||e}function n(n,r){return RegExp(t(n),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,s){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},c=function(t,n){e.cI&&(n=n.toLowerCase()),n.split(" ").forEach(function(e){var n=e.split("|");o[n[0]]=[t,n[1]?Number(n[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=o}a.lR=n(a.l||/\b[A-Za-z0-9_]+\b/,!0),s&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=n(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=n(a.e)),a.tE=t(a.e)||"",a.eW&&s.tE&&(a.tE+=(a.e?"|":"")+s.tE)),a.i&&(a.iR=n(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var u=[];a.c.forEach(function(e){e.v?e.v.forEach(function(t){u.push(i(e,t))}):u.push("self"==e?a:e)}),a.c=u,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,s);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(t).filter(Boolean);a.t=l.length?n(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function c(t,r,i,a){function s(e,t){for(var r=0;r";return a+=e+'">',a+t+s}function p(){if(!w.k)return e(B);var t="",n=0;w.lR.lastIndex=0;for(var r=w.lR.exec(B);r;){t+=e(B.substr(n,r.index-n));var i=h(w,r);i?(y+=i[1],t+=g(i[0],e(r[0]))):t+=e(r[0]),n=w.lR.lastIndex,r=w.lR.exec(B)}return t+e(B.substr(n))}function v(){if(w.sL&&!E[w.sL])return e(B);var t=w.sL?c(w.sL,B,!0,L[w.sL]):u(B);return w.r>0&&(y+=t.r),"continuous"==w.subLanguageMode&&(L[w.sL]=t.top),g(t.language,t.value,!1,!0)}function b(){return void 0!==w.sL?v():p()}function d(t,n){var r=t.cN?g(t.cN,"",!0):"";t.rB?(M+=r,B=""):t.eB?(M+=e(n)+r,B=""):(M+=r,B=n),w=Object.create(t,{parent:{value:w}})}function R(t,n){if(B+=t,void 0===n)return M+=b(),0;var r=s(n,w);if(r)return M+=b(),d(r,n),r.rB?0:n.length;var i=l(w,n);if(i){var a=w;a.rE||a.eE||(B+=n),M+=b();do w.cN&&(M+=""),y+=w.r,w=w.parent;while(w!=i.parent);return a.eE&&(M+=e(n)),B="",i.starts&&d(i.starts,""),a.rE?0:n.length}if(f(n,w))throw new Error('Illegal lexeme "'+n+'" for mode "'+(w.cN||"")+'"');return B+=n,n.length||1}var x=m(t);if(!x)throw new Error('Unknown language: "'+t+'"');o(x);for(var w=a||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=g(k.cN,"",!0)+M);var B="",y=0;try{for(var C,I,j=0;;){if(w.t.lastIndex=j,C=w.t.exec(r),!C)break;I=R(r.substr(j,C.index-j),C[0]),j=C.index+I}R(r.substr(j));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:y,value:M,language:t,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:e(r)};throw A}}function u(t,n){n=n||N.languages||Object.keys(E);var r={r:0,value:e(t)},i=r;return n.forEach(function(e){if(m(e)){var n=c(e,t,!1);n.language=e,n.r>i.r&&(i=n),n.r>r.r&&(i=r,r=n)}}),i.language&&(r.second_best=i),r}function l(e){return N.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,N.tabReplace)})),N.useBR&&(e=e.replace(/\n/g,"
")),e}function f(e,t,n){var r=t?R[t]:n,i=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||i.push("hljs"),r&&i.push(r),i.join(" ").trim()}function h(e){var t=r(e);if(!/no(-?)highlight/.test(t)){var n;N.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e;var i=n.textContent,o=t?c(t,i,!0):u(i),h=a(n);if(h.length){var g=document.createElementNS("http://www.w3.org/1999/xhtml","div");g.innerHTML=o.value,o.value=s(h,a(g),i)}o.value=l(o.value),e.innerHTML=o.value,e.className=f(e.className,t,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function g(e){N=i(N,e)}function p(){if(!p.called){p.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,h)}}function v(){addEventListener("DOMContentLoaded",p,!1),addEventListener("load",p,!1)}function b(e,t){var n=E[e]=t(this);n.aliases&&n.aliases.forEach(function(t){R[t]=e})}function d(){return Object.keys(E)}function m(e){return E[e]||E[R[e]]}var N={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},E={},R={};this.highlight=c,this.highlightAuto=u,this.fixMarkup=l,this.highlightBlock=h,this.configure=g,this.initHighlighting=p,this.initHighlightingOnLoad=v,this.registerLanguage=b,this.listLanguages=d,this.getLanguage=m,this.inherit=i,this.IR="[a-zA-Z][a-zA-Z0-9_]*",this.UIR="[a-zA-Z_][a-zA-Z0-9_]*",this.NR="\\b\\d+(\\.\\d+)?",this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",this.BNR="\\b(0b[01]+)",this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",this.BE={b:"\\\\[\\s\\S]",r:0},this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]},this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]},this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]},this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]},this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]},this.NM={cN:"number",b:this.NR,r:0},this.CNM={cN:"number",b:this.CNR,r:0},this.BNM={cN:"number",b:this.BNR,r:0},this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},this.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]},this.TM={cN:"title",b:this.IR,r:0},this.UTM={cN:"title",b:this.UIR,r:0}};hljs.registerLanguage("markdown",function(){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("javascript",function(r){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},r.ASM,r.QSM,r.CLCM,r.CBCM,r.CNM,{b:"("+r.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[r.CLCM,r.CBCM,r.RM,{b:/;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[r.inherit(r.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[r.CLCM,r.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+r.IR,r:0}]}});hljs.registerLanguage("json",function(e){var t={literal:"true false null"},i=[e.QSM,e.CNM],l={cN:"value",e:",",eW:!0,eE:!0,c:i,k:t},c={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:l}],i:"\\S"},n={b:"\\[",e:"\\]",c:[e.inherit(l,{cN:null})],i:"\\S"};return i.splice(i.length,0,c,n),{c:i,k:t,i:"\\S"}});hljs.registerLanguage("python",function(e){var r={cN:"prompt",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,i,b]},n={e:/:/,i:/[${=;\n]/,c:[e.UTM,l]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[r,i,b,e.HCM,e.inherit(n,{cN:"function",bK:"def",r:10}),e.inherit(n,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("xml",function(){var t="[A-Za-z0-9\\._:-]+",e={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"},c={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[c],starts:{e:"",rE:!0,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[c],starts:{e:"",rE:!0,sL:"javascript"}},e,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},c]}]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",a={cN:"function",b:c+"\\(",rB:!0,eE:!0,e:"\\("};return{cI:!0,i:"[=/|']",c:[e.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a,e.ASM,e.QSM,e.CSSNM]}]},{cN:"tag",b:c,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[e.CBCM,{cN:"rule",b:"[^\\s]",rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{cN:"value",eW:!0,eE:!0,c:[a,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}}); \ No newline at end of file diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index cf55bf4b13..11f514396f 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -150,6 +150,8 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}> {% elif df.fieldtype=="HTML" %} {{ frappe.render_template(df.options, {"doc":doc}) }} + {% elif df.fieldtype=="Currency" %} + {{ 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) }} diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 4f595c9419..2be92be1f5 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -249,82 +249,6 @@ class TestDocument(unittest.TestCase): self.assertEqual(cint(old_current) - 1, new_current) - def test_rename_doc(self): - from random import choice, sample - - available_documents = [] - doctype = "ToDo" - - # data generation: 4 todo documents - for num in range(1, 5): - doc = frappe.get_doc({ - "doctype": doctype, - "date": add_to_date(now(), days=num), - "description": "this is todo #{}".format(num) - }).insert() - available_documents.append(doc.name) - - # test 1: document renaming - old_name = choice(available_documents) - new_name = old_name + '.new' - self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True)) - available_documents.remove(old_name) - available_documents.append(new_name) - - # test 2: merge documents - first_todo, second_todo = sample(available_documents, 2) - - second_todo_doc = frappe.get_doc(doctype, second_todo) - second_todo_doc.priority = "High" - second_todo_doc.save() - - merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True) - merged_todo_doc = frappe.get_doc(doctype, merged_todo) - available_documents.remove(first_todo) - - with self.assertRaises(DoesNotExistError): - frappe.get_doc(doctype, first_todo) - - self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) - - for docname in available_documents: - frappe.delete_doc(doctype, docname) - - def test_rename_doctype(self): - from frappe.core.doctype.doctype.test_doctype import new_doctype - - fields =[{ - "label": "Linked To", - "fieldname": "linked_to_doctype", - "fieldtype": "Link", - "options": "DocType", - "unique": 0 - }] - if not frappe.db.exists("DocType", "Rename This"): - new_doctype("Rename This", unique=0, fields=fields).insert() - - to_rename_record = frappe.get_doc({ - "doctype": "Rename This", - "linked_to_doctype": "Rename This" - }) - to_rename_record.insert() - - # Rename doctype - self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)) - - # Test if Doctype value has changed in Link field - renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name) - self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc") - - # Test if there are conflicts between a record and a DocType - # having the same name - old_name = to_rename_record.name - new_name = "ToDo" - self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)) - - frappe.delete_doc_if_exists("Renamed Doc", "ToDo") - frappe.delete_doc_if_exists("DocType", "Renamed Doc") - def test_non_negative_check(self): frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py new file mode 100644 index 0000000000..58cc5bb125 --- /dev/null +++ b/frappe/tests/test_rename_doc.py @@ -0,0 +1,159 @@ +import os +import unittest + +import frappe +from frappe.utils import add_to_date, now +from frappe.exceptions import DoesNotExistError + +from random import choice, sample +from frappe.model.base_document import get_controller +from frappe.modules.utils import get_doc_path + + +class TestRenameDoc(unittest.TestCase): + @classmethod + def setUpClass(self): + """Setting Up data for the tests defined under TestRenameDoc""" + # set developer_mode to rename doc controllers + self._original_developer_flag = frappe.conf.developer_mode + frappe.conf.developer_mode = 1 + + # data generation: for base and merge tests + self.available_documents = [] + self.test_doctype = "ToDo" + + for num in range(1, 5): + doc = frappe.get_doc({ + "doctype": self.test_doctype, + "date": add_to_date(now(), days=num), + "description": "this is todo #{}".format(num), + }).insert() + self.available_documents.append(doc.name) + + # data generation: for controllers tests + self.doctype = frappe._dict({ + "old": "Test Rename Document Old", + "new": "Test Rename Document New", + }) + + frappe.get_doc({ + "doctype": "DocType", + "module": "Custom", + "name": self.doctype.old, + "custom": 0, + "fields": [ + {"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data"} + ], + "permissions": [{"role": "System Manager", "read": 1}], + }).insert() + + @classmethod + def tearDownClass(self): + """Deleting data generated for the tests defined under TestRenameDoc""" + # delete the documents created + for docname in self.available_documents: + frappe.delete_doc(self.test_doctype, docname) + + for dt in self.doctype.values(): + if frappe.db.exists("DocType", dt): + frappe.delete_doc("DocType", dt) + frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`") + + frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + + # reset original value of developer_mode conf + frappe.conf.developer_mode = self._original_developer_flag + + def setUp(self): + frappe.flags.link_fields = {} + super().setUp() + + def test_rename_doc(self): + """Rename an existing document via frappe.rename_doc""" + old_name = choice(self.available_documents) + new_name = old_name + ".new" + self.assertEqual(new_name, frappe.rename_doc(self.test_doctype, old_name, new_name, force=True)) + self.available_documents.remove(old_name) + self.available_documents.append(new_name) + + def test_merging_docs(self): + """Merge two documents via frappe.rename_doc""" + first_todo, second_todo = sample(self.available_documents, 2) + + second_todo_doc = frappe.get_doc(self.test_doctype, second_todo) + second_todo_doc.priority = "High" + second_todo_doc.save() + + merged_todo = frappe.rename_doc( + self.test_doctype, first_todo, second_todo, merge=True, force=True + ) + merged_todo_doc = frappe.get_doc(self.test_doctype, merged_todo) + self.available_documents.remove(first_todo) + + with self.assertRaises(DoesNotExistError): + frappe.get_doc(self.test_doctype, first_todo) + + self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) + + def test_rename_controllers(self): + """Rename doctypes with controller code paths""" + # check if module exists exists; + # if custom, get_controller will return Document class + # if not custom, a different class will be returned + self.assertNotEqual(get_controller(self.doctype.old), frappe.model.document.Document) + + old_doctype_path = get_doc_path("Custom", "DocType", self.doctype.old) + + # rename doc via wrapper API accessible via /desk + frappe.rename_doc("DocType", self.doctype.old, self.doctype.new) + + # check if database and controllers are updated + self.assertTrue(frappe.db.exists("DocType", self.doctype.new)) + self.assertFalse(frappe.db.exists("DocType", self.doctype.old)) + self.assertFalse(os.path.exists(old_doctype_path)) + + def test_rename_doctype(self): + """Rename DocType via frappe.rename_doc""" + from frappe.core.doctype.doctype.test_doctype import new_doctype + + if not frappe.db.exists("DocType", "Rename This"): + new_doctype( + "Rename This", + fields=[ + { + "label": "Linked To", + "fieldname": "linked_to_doctype", + "fieldtype": "Link", + "options": "DocType", + "unique": 0, + } + ], + ).insert() + + to_rename_record = frappe.get_doc( + {"doctype": "Rename This", "linked_to_doctype": "Rename This"} + ).insert() + + # Rename doctype + self.assertEqual( + "Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True) + ) + + # Test if Doctype value has changed in Link field + linked_to_doctype = frappe.db.get_value( + "Renamed Doc", to_rename_record.name, "linked_to_doctype" + ) + self.assertEqual(linked_to_doctype, "Renamed Doc") + + # Test if there are conflicts between a record and a DocType + # having the same name + old_name = to_rename_record.name + new_name = "ToDo" + self.assertEqual( + new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True) + ) + + # delete_doc doesnt drop tables + # this is done to bypass inconsistencies in the db + frappe.delete_doc_if_exists("DocType", "Renamed Doc") + frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`") diff --git a/frappe/website/js/syntax_highlight.js b/frappe/website/js/syntax_highlight.js index 199174b1e5..80914d9d99 100644 --- a/frappe/website/js/syntax_highlight.js +++ b/frappe/website/js/syntax_highlight.js @@ -1,4 +1,4 @@ -const hljs = require('highlight.js/lib/highlight'); +const hljs = require('highlight.js/lib/core'); hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); hljs.registerLanguage('python', require('highlight.js/lib/languages/python')); diff --git a/frappe/workflow/doctype/workflow/workflow.json b/frappe/workflow/doctype/workflow/workflow.json index 3cb72d0eed..e8db8dcb10 100644 --- a/frappe/workflow/doctype/workflow/workflow.json +++ b/frappe/workflow/doctype/workflow/workflow.json @@ -99,7 +99,7 @@ "icon": "fa fa-random", "idx": 1, "links": [], - "modified": "2020-07-16 04:29:20.898040", + "modified": "2020-12-17 20:35:16.898040", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow", diff --git a/frappe/workflow/doctype/workflow_transition/workflow_transition.json b/frappe/workflow/doctype/workflow_transition/workflow_transition.json index 8bc06bf18a..5e5cec5880 100644 --- a/frappe/workflow/doctype/workflow_transition/workflow_transition.json +++ b/frappe/workflow/doctype/workflow_transition/workflow_transition.json @@ -295,7 +295,7 @@ "label": "Example", "length": 0, "no_copy": 0, - "options": "
doc.grand_total > 0
\n\n

Conditions should be written in simple Python. Please use properties available in the form only.

", + "options": "
doc.grand_total > 0
\n\n

Conditions should be written in simple Python. Please use properties available in the form only.

\n

Allowed functions: \n

    \n
  • frappe.db.get_value
  • \n
  • frappe.db.get_list
  • \n
  • frappe.session
  • \n
  • frappe.utils.now_datetime
  • \n
  • frappe.utils.get_datetime
  • \n
  • frappe.utils.add_to_date
  • \n
  • frappe.utils.now
  • \n
\n

Example:

doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) 

", "permlevel": 0, "precision": "", "print_hide": 0, @@ -320,7 +320,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-10-09 10:28:53.294908", + "modified": "2020-11-08 12:11:00.294908", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow Transition", diff --git a/package.json b/package.json index a99f45ffdd..3a8291b5a9 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", - "highlight.js": "^9.18.2", + "highlight.js": "^10.4.1", "js-sha256": "^0.9.0", "jsbarcode": "^3.9.0", "moment": "^2.20.1", diff --git a/yarn.lock b/yarn.lock index 109105b406..e79622b7d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2931,10 +2931,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@^9.18.2: - version "9.18.5" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" - integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== +highlight.js@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" + integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== homedir-polyfill@^1.0.1: version "1.0.3" @@ -3147,9 +3147,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"