fix(minor): merge develop

This commit is contained in:
Rushabh Mehta 2021-01-08 14:35:55 +05:30
commit c93368b242
34 changed files with 488 additions and 178 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from datetime import timedelta
from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
@ -13,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:

View file

@ -7,10 +7,9 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
from frappe.utils import today, add_days, getdate, add_months
def add_custom_fields():
df = dict(
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
new_todo = frappe.db.get_value('ToDo',
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
new_todo = frappe.get_doc('ToDo', new_todo)
self.assertEqual(todo.get('description'), new_todo.get('description'))
def test_weekly_auto_repeat_with_weekdays(self):
todo = frappe.get_doc(
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
weekdays = list(week_map.keys())
current_weekday = getdate().weekday()
days = [
{'day': weekdays[current_weekday]},
{'day': weekdays[(current_weekday + 2) % 7]}
]
doc = make_auto_repeat(reference_doctype='ToDo',
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
self.assertEqual(doc.next_schedule_date, today())
data = get_auto_repeat_entries(getdate(today()))
create_repeated_entries(data)
frappe.db.commit()
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
self.assertEqual(todo.auto_repeat, doc.name)
doc.reload()
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
def test_monthly_auto_repeat(self):
start_date = today()
end_date = add_months(start_date, 12)
@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(`<div class="card-stats ${color_class}">
<span class="percentage-stat-area">
${caret_html}
<span class="percentage-stat">
${Math.abs(this.percentage_stat)} %
${get_stat()} %
</span>
</span>
<span class="stat-period text-muted">

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -295,7 +295,7 @@
"label": "Example",
"length": 0,
"no_copy": 0,
"options": "<pre><code>doc.grand_total &gt; 0</code></pre>\n\n<p>Conditions should be written in simple Python. Please use properties available in the form only.</p>",
"options": "<pre><code>doc.grand_total &gt; 0</code></pre>\n\n<p>Conditions should be written in simple Python. Please use properties available in the form only.</p>\n<p>Allowed functions: \n</p><ul>\n<li>frappe.db.get_value</li>\n<li>frappe.db.get_list</li>\n<li>frappe.session</li>\n<li>frappe.utils.now_datetime</li>\n<li>frappe.utils.get_datetime</li>\n<li>frappe.utils.add_to_date</li>\n<li>frappe.utils.now</li>\n</ul>\n<p>Example: </p><pre><code>doc.creation &gt; frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) </code></pre><p></p>",
"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",

View file

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

View file

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