Merge branch 'develop' of github.com:frappe/frappe into snyk-fix-e205bf085ec74cf13bd3f75b89038f28
This commit is contained in:
commit
5db02513e5
50 changed files with 688 additions and 213 deletions
|
|
@ -945,7 +945,11 @@ def get_installed_apps(sort=False, frappe_last=False):
|
|||
connect()
|
||||
|
||||
if not local.all_apps:
|
||||
local.all_apps = get_all_apps(True)
|
||||
local.all_apps = cache().get_value('all_apps', get_all_apps)
|
||||
|
||||
#cache bench apps
|
||||
if not cache().get_value('all_apps'):
|
||||
cache().set_value('all_apps', local.all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
// auto repeat schedule
|
||||
frappe.auto_repeat.render_schedule(frm);
|
||||
|
||||
frm.trigger('toggle_submit_on_creation');
|
||||
},
|
||||
|
||||
reference_doctype: function(frm) {
|
||||
frm.trigger('toggle_submit_on_creation');
|
||||
},
|
||||
|
||||
toggle_submit_on_creation: function(frm) {
|
||||
// submit on creation checkbox
|
||||
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
|
||||
let meta = frappe.get_meta(frm.doc.reference_doctype);
|
||||
frm.toggle_display('submit_on_creation', meta.is_submittable);
|
||||
});
|
||||
},
|
||||
|
||||
template: function(frm) {
|
||||
|
|
@ -86,10 +100,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
|
||||
frappe.call({
|
||||
method: "get_auto_repeat_schedule",
|
||||
doc: frm.doc
|
||||
}).done((r) => {
|
||||
frm.call("get_auto_repeat_schedule").then(r => {
|
||||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:AUT-AR-{#####}",
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
"section_break_3",
|
||||
"reference_doctype",
|
||||
"reference_document",
|
||||
"submit_on_creation",
|
||||
"column_break_5",
|
||||
"start_date",
|
||||
"end_date",
|
||||
|
|
@ -21,6 +23,8 @@
|
|||
"repeat_on_last_day",
|
||||
"column_break_12",
|
||||
"next_schedule_date",
|
||||
"section_break_12",
|
||||
"repeat_on_days",
|
||||
"notification",
|
||||
"notify_by_email",
|
||||
"recipients",
|
||||
|
|
@ -186,9 +190,28 @@
|
|||
"fieldname": "repeat_on_last_day",
|
||||
"fieldtype": "Check",
|
||||
"label": "Repeat on Last Day of the Month"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "repeat_on_days",
|
||||
"fieldtype": "Table",
|
||||
"label": "Repeat on Days",
|
||||
"options": "Auto Repeat Day"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_on_creation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit on Creation"
|
||||
}
|
||||
],
|
||||
"modified": "2019-07-17 11:30:51.412317",
|
||||
"links": [],
|
||||
"modified": "2020-12-10 10:43:13.449172",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
|
@ -13,16 +14,19 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_
|
|||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
|
||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def validate(self):
|
||||
self.update_status()
|
||||
self.validate_reference_doctype()
|
||||
self.validate_submit_on_creation()
|
||||
self.validate_dates()
|
||||
self.validate_email_id()
|
||||
self.validate_auto_repeat_days()
|
||||
self.set_dates()
|
||||
self.update_auto_repeat_id()
|
||||
self.unlink_if_applicable()
|
||||
|
|
@ -48,7 +52,7 @@ class AutoRepeat(Document):
|
|||
if self.disabled:
|
||||
self.next_schedule_date = None
|
||||
else:
|
||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
|
||||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
|
||||
|
||||
def unlink_if_applicable(self):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
|
|
@ -60,6 +64,11 @@ class AutoRepeat(Document):
|
|||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
|
||||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
|
||||
|
||||
def validate_submit_on_creation(self):
|
||||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
|
||||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
|
||||
frappe.bold('Submit on Creation')))
|
||||
|
||||
def validate_dates(self):
|
||||
if frappe.flags.in_patch:
|
||||
return
|
||||
|
|
@ -82,6 +91,12 @@ class AutoRepeat(Document):
|
|||
else:
|
||||
frappe.throw(_("'Recipients' not specified"))
|
||||
|
||||
def validate_auto_repeat_days(self):
|
||||
auto_repeat_days = self.get_auto_repeat_days()
|
||||
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
|
||||
repeated_days = get_repeated(auto_repeat_days)
|
||||
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
#check if document is already on auto repeat
|
||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
|
||||
|
|
@ -107,7 +122,7 @@ class AutoRepeat(Document):
|
|||
end_date = getdate(self.end_date)
|
||||
|
||||
if not self.end_date:
|
||||
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
|
||||
next_date = self.get_next_schedule_date(schedule_date=start_date)
|
||||
row = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
|
|
@ -116,8 +131,7 @@ class AutoRepeat(Document):
|
|||
schedule_details.append(row)
|
||||
|
||||
if self.end_date:
|
||||
next_date = get_next_schedule_date(
|
||||
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
|
||||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
|
||||
|
||||
while (getdate(next_date) < getdate(end_date)):
|
||||
row = {
|
||||
|
|
@ -126,8 +140,7 @@ class AutoRepeat(Document):
|
|||
"next_scheduled_date" : next_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
next_date = get_next_schedule_date(
|
||||
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
|
||||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
|
||||
|
||||
return schedule_details
|
||||
|
||||
|
|
@ -150,6 +163,9 @@ class AutoRepeat(Document):
|
|||
self.update_doc(new_doc, reference_doc)
|
||||
new_doc.insert(ignore_permissions = True)
|
||||
|
||||
if self.submit_on_creation:
|
||||
new_doc.submit()
|
||||
|
||||
return new_doc
|
||||
|
||||
def update_doc(self, new_doc, reference_doc):
|
||||
|
|
@ -160,7 +176,7 @@ class AutoRepeat(Document):
|
|||
if new_doc.meta.get_field('auto_repeat'):
|
||||
new_doc.set('auto_repeat', self.name)
|
||||
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
|
||||
if new_doc.meta.get_field(fieldname):
|
||||
new_doc.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
|
|
@ -202,6 +218,75 @@ class AutoRepeat(Document):
|
|||
new_doc.set('from_date', from_date)
|
||||
new_doc.set('to_date', to_date)
|
||||
|
||||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
|
||||
"""
|
||||
Returns the next schedule date for auto repeat after a recurring document has been created.
|
||||
Adds required offset to the schedule_date param and returns the next schedule date.
|
||||
|
||||
:param schedule_date: The date when the last recurring document was created.
|
||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
|
||||
"""
|
||||
if month_map.get(self.frequency):
|
||||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
|
||||
else:
|
||||
month_count = 0
|
||||
|
||||
day_count = 0
|
||||
if month_count and self.repeat_on_last_day:
|
||||
day_count = 31
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
elif month_count and self.repeat_on_day:
|
||||
day_count = self.repeat_on_day
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
elif month_count:
|
||||
next_date = get_next_date(self.start_date, month_count)
|
||||
else:
|
||||
days = self.get_days(schedule_date)
|
||||
next_date = add_days(schedule_date, days)
|
||||
|
||||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
if month_count:
|
||||
month_count += month_map.get(self.frequency, 0)
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
else:
|
||||
days = self.get_days(next_date)
|
||||
next_date = add_days(next_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
def get_days(self, schedule_date):
|
||||
if self.frequency == "Weekly":
|
||||
days = self.get_offset_for_weekly_frequency(schedule_date)
|
||||
else:
|
||||
# daily frequency
|
||||
days = 1
|
||||
|
||||
return days
|
||||
|
||||
def get_offset_for_weekly_frequency(self, schedule_date):
|
||||
# if weekdays are not set, offset is 7 from current schedule date
|
||||
if not self.repeat_on_days:
|
||||
return 7
|
||||
|
||||
repeat_on_days = self.get_auto_repeat_days()
|
||||
current_schedule_day = getdate(schedule_date).weekday()
|
||||
weekdays = list(week_map.keys())
|
||||
|
||||
# if repeats on more than 1 day or
|
||||
# start date's weekday is not in repeat days, then get next weekday
|
||||
# else offset is 7
|
||||
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
|
||||
weekday = get_next_weekday(current_schedule_day, repeat_on_days)
|
||||
next_weekday_number = week_map.get(weekday, 0)
|
||||
# offset for upcoming weekday
|
||||
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
|
||||
return 7
|
||||
|
||||
def get_auto_repeat_days(self):
|
||||
return [d.day for d in self.get('repeat_on_days', [])]
|
||||
|
||||
def send_notification(self, new_doc):
|
||||
"""Notify concerned people about recurring document generation"""
|
||||
subject = self.subject or ''
|
||||
|
|
@ -282,42 +367,24 @@ class AutoRepeat(Document):
|
|||
)
|
||||
|
||||
|
||||
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
|
||||
if month_map.get(frequency):
|
||||
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
|
||||
else:
|
||||
month_count = 0
|
||||
|
||||
day_count = 0
|
||||
if month_count and repeat_on_last_day:
|
||||
day_count = 31
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif month_count and repeat_on_day:
|
||||
day_count = repeat_on_day
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif month_count:
|
||||
next_date = get_next_date(start_date, month_count)
|
||||
else:
|
||||
days = 7 if frequency == 'Weekly' else 1
|
||||
next_date = add_days(schedule_date, days)
|
||||
|
||||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
if month_count:
|
||||
month_count += month_map.get(frequency)
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif days:
|
||||
next_date = add_days(next_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
|
||||
def get_next_date(dt, mcount, day=None):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
return dt
|
||||
|
||||
|
||||
def get_next_weekday(current_schedule_day, weekdays):
|
||||
days = list(week_map.keys())
|
||||
if current_schedule_day > 0:
|
||||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
|
||||
else:
|
||||
days = days[(current_schedule_day + 1):]
|
||||
|
||||
for entry in days:
|
||||
if entry in weekdays:
|
||||
return entry
|
||||
|
||||
|
||||
#called through hooks
|
||||
def make_auto_repeat_entry():
|
||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
|
||||
|
|
@ -328,6 +395,7 @@ def make_auto_repeat_entry():
|
|||
data = get_auto_repeat_entries(date)
|
||||
frappe.enqueue(enqueued_method, data=data)
|
||||
|
||||
|
||||
def create_repeated_entries(data):
|
||||
for d in data:
|
||||
doc = frappe.get_doc('Auto Repeat', d.name)
|
||||
|
|
@ -337,10 +405,11 @@ def create_repeated_entries(data):
|
|||
|
||||
if schedule_date == current_date and not doc.disabled:
|
||||
doc.create_documents()
|
||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
|
||||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
|
||||
if schedule_date and not doc.disabled:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
|
||||
|
||||
def get_auto_repeat_entries(date=None):
|
||||
if not date:
|
||||
date = getdate(today())
|
||||
|
|
@ -349,6 +418,7 @@ def get_auto_repeat_entries(date=None):
|
|||
['status', '=', 'Active']
|
||||
])
|
||||
|
||||
|
||||
#called through hooks
|
||||
def set_auto_repeat_as_completed():
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
|
||||
|
|
@ -358,6 +428,7 @@ def set_auto_repeat_as_completed():
|
|||
doc.status = 'Completed'
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
|
||||
if not start_date:
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import unittest
|
|||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
|
||||
from frappe.utils import today, add_days, getdate, add_months
|
||||
|
||||
|
||||
def add_custom_fields():
|
||||
df = dict(
|
||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
|
||||
|
|
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
|
||||
def test_weekly_auto_repeat(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
|
||||
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
|
||||
|
||||
new_todo = frappe.get_doc('ToDo', new_todo)
|
||||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
|
||||
def test_weekly_auto_repeat_with_weekdays(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
|
||||
|
||||
weekdays = list(week_map.keys())
|
||||
current_weekday = getdate().weekday()
|
||||
days = [
|
||||
{'day': weekdays[current_weekday]},
|
||||
{'day': weekdays[(current_weekday + 2) % 7]}
|
||||
]
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
doc.reload()
|
||||
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
|
||||
|
||||
def test_monthly_auto_repeat(self):
|
||||
start_date = today()
|
||||
end_date = add_months(start_date, 12)
|
||||
|
|
@ -111,6 +156,25 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
|
||||
self.assertEqual(getdate(doc.next_schedule_date), current_date)
|
||||
|
||||
def test_submit_on_creation(self):
|
||||
doctype = 'Test Submittable DocType'
|
||||
create_submittable_doctype(doctype)
|
||||
|
||||
current_date = getdate()
|
||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
|
||||
submittable_doc.submit()
|
||||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
|
||||
start_date=add_days(current_date, -1), submit_on_creation=1)
|
||||
|
||||
data = get_auto_repeat_entries(current_date)
|
||||
create_repeated_entries(data)
|
||||
docnames = frappe.db.get_all(doc.reference_doctype,
|
||||
filters={'auto_repeat': doc.name},
|
||||
fields=['docstatus'],
|
||||
limit=1
|
||||
)
|
||||
self.assertEquals(docnames[0].docstatus, 1)
|
||||
|
||||
|
||||
def make_auto_repeat(**args):
|
||||
args = frappe._dict(args)
|
||||
|
|
@ -118,13 +182,46 @@ def make_auto_repeat(**args):
|
|||
'doctype': 'Auto Repeat',
|
||||
'reference_doctype': args.reference_doctype or 'ToDo',
|
||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
|
||||
'submit_on_creation': args.submit_on_creation or 0,
|
||||
'frequency': args.frequency or 'Daily',
|
||||
'start_date': args.start_date or add_days(today(), -1),
|
||||
'end_date': args.end_date or "",
|
||||
'notify_by_email': args.notify or 0,
|
||||
'recipients': args.recipients or "",
|
||||
'subject': args.subject or "",
|
||||
'message': args.message or ""
|
||||
'message': args.message or "",
|
||||
'repeat_on_days': args.days or []
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def create_submittable_doctype(doctype):
|
||||
if frappe.db.exists('DocType', doctype):
|
||||
return
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'__newname': doctype,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'is_submittable': 1,
|
||||
'fields': [{
|
||||
'fieldname': 'test',
|
||||
'label': 'Test',
|
||||
'fieldtype': 'Data'
|
||||
}],
|
||||
'permissions': [{
|
||||
'role': 'System Manager',
|
||||
'read': 1,
|
||||
'write': 1,
|
||||
'create': 1,
|
||||
'delete': 1,
|
||||
'submit': 1,
|
||||
'cancel': 1,
|
||||
'amend': 1
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
doc.allow_auto_repeat = 1
|
||||
doc.save()
|
||||
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal file
0
frappe/automation/doctype/auto_repeat_day/__init__.py
Normal 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
|
||||
}
|
||||
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal file
10
frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py
Normal 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
|
||||
|
|
@ -100,13 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
with_public_files = os.path.join(base_path, with_public_files)
|
||||
public = extract_files(site, with_public_files, 'public')
|
||||
public = extract_files(site, with_public_files)
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
with_private_files = os.path.join(base_path, with_private_files)
|
||||
private = extract_files(site, with_private_files, 'private')
|
||||
private = extract_files(site, with_private_files)
|
||||
os.remove(private)
|
||||
|
||||
# Removing temporarily created file
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', {
|
|||
|
||||
setup_help(frm) {
|
||||
frm.get_field('help_html').html(`
|
||||
<h3>Examples</h3>
|
||||
<h4>DocType Event</h4>
|
||||
<pre><code>
|
||||
<p>Add logic for standard doctype events like Before Insert, After Submit, etc.</p>
|
||||
<pre>
|
||||
<code>
|
||||
# set property
|
||||
if "test" in doc.description:
|
||||
doc.status = 'Closed'
|
||||
doc.status = 'Closed'
|
||||
|
||||
|
||||
# validate
|
||||
if "validate" in doc.description:
|
||||
raise frappe.ValidationError
|
||||
raise frappe.ValidationError
|
||||
|
||||
# auto create another document
|
||||
if doc.allocted_to:
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'ToDo'
|
||||
owner = doc.allocated_to,
|
||||
description = doc.subject
|
||||
)).insert()
|
||||
</code></pre>
|
||||
if doc.allocated_to:
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'ToDo'
|
||||
owner = doc.allocated_to,
|
||||
description = doc.subject
|
||||
)).insert()
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>API Call</h4>
|
||||
<p>Respond to <code>/api/method/<method-name></code> calls, just like whitelisted methods</p>
|
||||
<pre><code>
|
||||
# respond to API
|
||||
|
||||
|
|
@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping":
|
|||
else:
|
||||
frappe.response['message'] = "ok"
|
||||
</code></pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Permission Query</h4>
|
||||
<p>Add conditions to the where clause of list queries.</p>
|
||||
<pre><code>
|
||||
# generate dynamic conditions and set it in the conditions variable
|
||||
tenant_id = frappe.db.get_value(...)
|
||||
conditions = 'tenant_id = {}'.format(tenant_id)
|
||||
|
||||
# resulting select query
|
||||
select name from \`tabPerson\`
|
||||
where tenant_id = 2
|
||||
order by creation desc
|
||||
</code></pre>
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Script Type",
|
||||
"options": "DocType Event\nScheduler Event\nAPI",
|
||||
"options": "DocType Event\nScheduler Event\nPermission Query\nAPI",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.script_type==='DocType Event'",
|
||||
"depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)",
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-11 12:39:41.391052",
|
||||
"modified": "2020-12-03 22:42:02.708148",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ class ServerScript(Document):
|
|||
# wrong report type!
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
def get_permission_query_conditions(self, user):
|
||||
locals = {"user": user, "conditions": ""}
|
||||
safe_exec(self.script, None, locals)
|
||||
if locals["conditions"]:
|
||||
return locals["conditions"]
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_scheduler_events(script_name, frequency):
|
||||
method = frappe.scrub('{0}-{1}'.format(script_name, frequency))
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ def get_server_script_map():
|
|||
# },
|
||||
# '_api': {
|
||||
# '[path]': '[server script]'
|
||||
# },
|
||||
# 'permission_query': {
|
||||
# 'DocType': '[server script]'
|
||||
# }
|
||||
# }
|
||||
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'):
|
||||
|
|
@ -57,16 +60,20 @@ def get_server_script_map():
|
|||
|
||||
script_map = frappe.cache().get_value('server_script_map')
|
||||
if script_map is None:
|
||||
script_map = {}
|
||||
script_map = {
|
||||
'permission_query': {}
|
||||
}
|
||||
enabled_server_scripts = frappe.get_all('Server Script',
|
||||
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
|
||||
filters={'disabled': 0})
|
||||
for script in enabled_server_scripts:
|
||||
if script.script_type == 'DocType Event':
|
||||
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
|
||||
elif script.script_type == 'Permission Query':
|
||||
script_map['permission_query'][script.reference_doctype] = script.name
|
||||
else:
|
||||
script_map.setdefault('_api', {})[script.api_method] = script.name
|
||||
|
||||
frappe.cache().set_value('server_script_map', script_map)
|
||||
|
||||
return script_map
|
||||
return script_map
|
||||
|
|
|
|||
|
|
@ -48,6 +48,13 @@ frappe.flags = 'hello'
|
|||
'''
|
||||
),
|
||||
dict(
|
||||
name='test_permission_query',
|
||||
script_type = 'Permission Query',
|
||||
reference_doctype = 'ToDo',
|
||||
script = '''
|
||||
conditions = '1 = 1'
|
||||
'''),
|
||||
dict(
|
||||
name='test_invalid_namespace_method',
|
||||
script_type = 'DocType Event',
|
||||
doctype_event = 'Before Insert',
|
||||
|
|
@ -95,6 +102,10 @@ class TestServerScript(unittest.TestCase):
|
|||
def test_api_return(self):
|
||||
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')
|
||||
|
||||
def test_permission_query(self):
|
||||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1))
|
||||
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))
|
||||
|
||||
def test_attribute_error(self):
|
||||
"""Raise AttributeError if method not found in Namespace"""
|
||||
note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"})
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", {
|
|||
} else {
|
||||
f._sortable = false;
|
||||
}
|
||||
if (f.fieldtype == "Table") {
|
||||
frm.add_custom_button(f.options, function() {
|
||||
frm.set_value('doc_type', f.options);
|
||||
}, __('Customize Child Table'));
|
||||
}
|
||||
});
|
||||
frm.fields_dict.fields.grid.refresh();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` (
|
|||
|
||||
DROP TABLE IF EXISTS `tabSeries`;
|
||||
CREATE TABLE `tabSeries` (
|
||||
`name` varchar(100) DEFAULT NULL,
|
||||
`name` varchar(100),
|
||||
`current` int(10) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(`name`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
|
|
@ -42,6 +43,24 @@ class Dashboard(Document):
|
|||
except ValueError as error:
|
||||
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
if user == 'Administrator':
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return None
|
||||
|
||||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
|
||||
module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format(
|
||||
allowed_modules=','.join(allowed_modules))
|
||||
|
||||
return module_condition
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_charts(dashboard_name):
|
||||
permitted_charts = []
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ from frappe.utils.dateutils import\
|
|||
get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.boot import get_allowed_reports
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
|
|
@ -31,9 +31,11 @@ def get_permission_query_conditions(user):
|
|||
|
||||
doctype_condition = False
|
||||
report_condition = False
|
||||
module_condition = False
|
||||
|
||||
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
|
||||
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
|
||||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
|
||||
|
||||
if allowed_doctypes:
|
||||
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format(
|
||||
|
|
@ -41,18 +43,24 @@ def get_permission_query_conditions(user):
|
|||
if allowed_reports:
|
||||
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format(
|
||||
allowed_reports=','.join(allowed_reports))
|
||||
if allowed_modules:
|
||||
module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules})
|
||||
or `tabDashboard Chart`.`module` is NULL'''.format(
|
||||
allowed_modules=','.join(allowed_modules))
|
||||
|
||||
return '''
|
||||
(`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
|
||||
and {doctype_condition})
|
||||
or
|
||||
(`tabDashboard Chart`.`chart_type` = 'Report'
|
||||
and {report_condition})
|
||||
'''.format(
|
||||
doctype_condition=doctype_condition,
|
||||
report_condition=report_condition
|
||||
)
|
||||
|
||||
((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average')
|
||||
and {doctype_condition})
|
||||
or
|
||||
(`tabDashboard Chart`.`chart_type` = 'Report'
|
||||
and {report_condition}))
|
||||
and
|
||||
({module_condition})
|
||||
'''.format(
|
||||
doctype_condition=doctype_condition,
|
||||
report_condition=report_condition,
|
||||
module_condition=module_condition
|
||||
)
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
roles = frappe.get_roles(user)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from frappe.model.document import Document
|
|||
from frappe.utils import cint
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.modules.export_file import export_to_files
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
|
||||
class NumberCard(Document):
|
||||
def autoname(self):
|
||||
|
|
@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None):
|
|||
return None
|
||||
|
||||
doctype_condition = False
|
||||
module_condition = False
|
||||
|
||||
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()]
|
||||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()]
|
||||
|
||||
if allowed_doctypes:
|
||||
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format(
|
||||
allowed_doctypes=','.join(allowed_doctypes))
|
||||
if allowed_modules:
|
||||
module_condition = '''`tabNumber Card`.`module` in ({allowed_modules})
|
||||
or `tabNumber Card`.`module` is NULL'''.format(
|
||||
allowed_modules=','.join(allowed_modules))
|
||||
|
||||
return '''
|
||||
{doctype_condition}
|
||||
'''.format(doctype_condition=doctype_condition)
|
||||
{doctype_condition}
|
||||
and
|
||||
{module_condition}
|
||||
'''.format(doctype_condition=doctype_condition, module_condition=module_condition)
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
roles = frappe.get_roles(user)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
|
|
@ -8,6 +9,8 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"subject",
|
||||
"use_html",
|
||||
"response_html",
|
||||
"response",
|
||||
"owner",
|
||||
"section_break_4",
|
||||
|
|
@ -22,11 +25,12 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.use_html",
|
||||
"fieldname": "response",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Response",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:!doc.use_html"
|
||||
},
|
||||
{
|
||||
"default": "user",
|
||||
|
|
@ -45,10 +49,24 @@
|
|||
"fieldtype": "HTML",
|
||||
"label": "Email Reply Help",
|
||||
"options": "<h4>Email Reply Example</h4>\n\n<pre>Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>\n"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_html",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_html",
|
||||
"fieldname": "response_html",
|
||||
"fieldtype": "Code",
|
||||
"label": "Response ",
|
||||
"options": "HTML"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-comment",
|
||||
"modified": "2019-10-30 14:15:00.956347",
|
||||
"links": [],
|
||||
"modified": "2020-11-30 14:12:50.321633",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Template",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,29 @@ from six import string_types
|
|||
|
||||
class EmailTemplate(Document):
|
||||
def validate(self):
|
||||
validate_template(self.response)
|
||||
if self.use_html:
|
||||
validate_template(self.response_html)
|
||||
else:
|
||||
validate_template(self.response)
|
||||
|
||||
def get_formatted_subject(self, doc):
|
||||
return frappe.render_template(self.subject, doc)
|
||||
|
||||
def get_formatted_response(self, doc):
|
||||
if self.use_html:
|
||||
return frappe.render_template(self.response_html, doc)
|
||||
|
||||
return frappe.render_template(self.response, doc)
|
||||
|
||||
def get_formatted_email(self, doc):
|
||||
if isinstance(doc, string_types):
|
||||
doc = json.loads(doc)
|
||||
|
||||
return {
|
||||
"subject" : self.get_formatted_subject(doc),
|
||||
"message" : self.get_formatted_response(doc)
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_email_template(template_name, doc):
|
||||
|
|
@ -18,5 +40,4 @@ def get_email_template(template_name, doc):
|
|||
doc = json.loads(doc)
|
||||
|
||||
email_template = frappe.get_doc("Email Template", template_name)
|
||||
return {"subject" : frappe.render_template(email_template.subject, doc),
|
||||
"message" : frappe.render_template(email_template.response, doc)}
|
||||
return email_template.get_formatted_email(doc)
|
||||
|
|
@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator):
|
|||
self.db_set("scheduled_to_send", len(self.recipients))
|
||||
|
||||
def get_message(self):
|
||||
|
||||
if self.content_type == "HTML":
|
||||
return frappe.render_template(self.message_html, {"doc": self.as_dict()})
|
||||
return {
|
||||
'Rich Text': self.message,
|
||||
'Markdown': markdown(self.message_md),
|
||||
'HTML': self.message_html
|
||||
'Markdown': markdown(self.message_md)
|
||||
}[self.content_type or 'Rich Text']
|
||||
|
||||
def get_recipients(self):
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ permission_query_conditions = {
|
|||
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
|
||||
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
|
||||
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
|
||||
"Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions",
|
||||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
|
||||
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
|
||||
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
|
||||
|
|
|
|||
|
|
@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path):
|
|||
Returns:
|
||||
str: Path of the decompressed SQL file
|
||||
"""
|
||||
from frappe.utils import get_bench_relative_path
|
||||
sql_file_path = get_bench_relative_path(sql_file_path)
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
if not os.path.exists(sql_file_path):
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
if not os.path.exists(sql_file_path):
|
||||
print('Invalid path {0}'.format(sql_file_path[3:]))
|
||||
sys.exit(1)
|
||||
elif sql_file_path.startswith(os.sep):
|
||||
base_path = os.sep
|
||||
else:
|
||||
base_path = '.'
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
decompressed_file_name = extract_sql_gzip(sql_file_path)
|
||||
else:
|
||||
decompressed_file_name = sql_file_path
|
||||
|
||||
|
|
@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path):
|
|||
return decompressed_file
|
||||
|
||||
|
||||
def extract_files(site_name, file_path, folder_name):
|
||||
def extract_files(site_name, file_path):
|
||||
import shutil
|
||||
import subprocess
|
||||
from frappe.utils import get_bench_relative_path
|
||||
|
||||
file_path = get_bench_relative_path(file_path)
|
||||
|
||||
# Need to do frappe.init to maintain the site locals
|
||||
frappe.init(site=site_name)
|
||||
|
|
|
|||
|
|
@ -18,12 +18,9 @@
|
|||
"bucket",
|
||||
"endpoint_url",
|
||||
"column_break_13",
|
||||
"region",
|
||||
"backup_details_section",
|
||||
"frequency",
|
||||
"backup_files",
|
||||
"column_break_18",
|
||||
"backup_limit"
|
||||
"backup_files"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -42,7 +39,7 @@
|
|||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Note: By default emails for failed backups are sent.",
|
||||
"description": "By default, emails are only sent for failed backups.",
|
||||
"fieldname": "send_email_for_successful_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful Backup"
|
||||
|
|
@ -73,14 +70,7 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "us-east-1",
|
||||
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.",
|
||||
"fieldname": "region",
|
||||
"fieldtype": "Select",
|
||||
"label": "Region",
|
||||
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1"
|
||||
},
|
||||
{
|
||||
"default": "https://s3.amazonaws.com",
|
||||
"fieldname": "endpoint_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Endpoint URL"
|
||||
|
|
@ -92,14 +82,6 @@
|
|||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Set to 0 for no limit on the number of backups taken",
|
||||
"fieldname": "backup_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Backup Limit",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "api_access_section",
|
||||
|
|
@ -142,16 +124,12 @@
|
|||
"fieldname": "backup_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backup Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-27 17:27:21.400000",
|
||||
"modified": "2020-12-07 15:30:55.047689",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
|
|
@ -172,4 +150,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ class S3BackupSettings(Document):
|
|||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = 'https://s3.amazonaws.com'
|
||||
|
||||
conn = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=self.access_key_id,
|
||||
|
|
@ -31,25 +32,21 @@ class S3BackupSettings(Document):
|
|||
endpoint_url=self.endpoint_url
|
||||
)
|
||||
|
||||
bucket_lower = str(self.bucket)
|
||||
|
||||
try:
|
||||
conn.list_buckets()
|
||||
|
||||
except ClientError:
|
||||
frappe.throw(_("Invalid Access Key ID or Secret Access Key."))
|
||||
|
||||
try:
|
||||
# Head_bucket returns a 200 OK if the bucket exists and have access to it.
|
||||
conn.head_bucket(Bucket=bucket_lower)
|
||||
# Requires ListBucket permission
|
||||
conn.head_bucket(Bucket=self.bucket)
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
bucket_name = frappe.bold(self.bucket)
|
||||
if error_code == '403':
|
||||
frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower))
|
||||
else: # '400'-Bad request or '404'-Not Found return
|
||||
# try to create bucket
|
||||
conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={
|
||||
'LocationConstraint': self.region})
|
||||
msg = _("Do not have permission to access bucket {0}.").format(bucket_name)
|
||||
elif error_code == '404':
|
||||
msg = _("Bucket {0} not found.").format(bucket_name)
|
||||
else:
|
||||
msg = e.args[0]
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -70,11 +67,13 @@ def take_backups_weekly():
|
|||
def take_backups_monthly():
|
||||
take_backups_if("Monthly")
|
||||
|
||||
|
||||
def take_backups_if(freq):
|
||||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
|
||||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
|
||||
take_backups_s3()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backups_s3(retry_count=0):
|
||||
try:
|
||||
|
|
@ -146,42 +145,13 @@ def backup_to_s3():
|
|||
if files_filename:
|
||||
upload_file_to_s3(files_filename, folder, conn, bucket)
|
||||
|
||||
delete_old_backups(doc.backup_limit, bucket)
|
||||
|
||||
|
||||
def upload_file_to_s3(filename, folder, conn, bucket):
|
||||
destpath = os.path.join(folder, os.path.basename(filename))
|
||||
try:
|
||||
print("Uploading file:", filename)
|
||||
conn.upload_file(filename, bucket, destpath)
|
||||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error()
|
||||
print("Error uploading: %s" % (e))
|
||||
|
||||
|
||||
def delete_old_backups(limit, bucket):
|
||||
all_backups = []
|
||||
doc = frappe.get_single("S3 Backup Settings")
|
||||
backup_limit = int(limit)
|
||||
|
||||
s3 = boto3.resource(
|
||||
's3',
|
||||
aws_access_key_id=doc.access_key_id,
|
||||
aws_secret_access_key=doc.get_password('secret_access_key'),
|
||||
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com'
|
||||
)
|
||||
|
||||
bucket = s3.Bucket(bucket)
|
||||
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/')
|
||||
if objects:
|
||||
for obj in objects.get('CommonPrefixes'):
|
||||
all_backups.append(obj.get('Prefix'))
|
||||
|
||||
oldest_backup = sorted(all_backups)[0] if all_backups else ''
|
||||
|
||||
if len(all_backups) > backup_limit:
|
||||
print("Deleting Backup: {0}".format(oldest_backup))
|
||||
for obj in bucket.objects.filter(Prefix=oldest_backup):
|
||||
# delete all keys that are inside the oldest_backup
|
||||
s3.Object(bucket.name, obj.key).delete()
|
||||
|
|
|
|||
|
|
@ -802,12 +802,12 @@ class BaseDocument(object):
|
|||
if translated:
|
||||
val = _(val)
|
||||
|
||||
if absolute_value and isinstance(val, (int, float)):
|
||||
val = abs(self.get(fieldname))
|
||||
|
||||
if not doc:
|
||||
doc = getattr(self, "parent_doc", None) or self
|
||||
|
||||
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
|
||||
val = abs(self.get(fieldname))
|
||||
|
||||
return format_value(val, df=df, doc=doc, currency=currency)
|
||||
|
||||
def is_print_hide(self, fieldname, df=None, for_print=True):
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from frappe.client import check_parent_permission
|
|||
from frappe.model.utils.user_settings import get_user_settings, update_user_settings
|
||||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range
|
||||
from frappe.model.meta import get_table_columns
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
|
||||
class DatabaseQuery(object):
|
||||
def __init__(self, doctype, user=None):
|
||||
|
|
@ -683,15 +684,23 @@ class DatabaseQuery(object):
|
|||
self.match_filters.append(match_filters)
|
||||
|
||||
def get_permission_query_conditions(self):
|
||||
conditions = []
|
||||
condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, [])
|
||||
if condition_methods:
|
||||
conditions = []
|
||||
for method in condition_methods:
|
||||
c = frappe.call(frappe.get_attr(method), self.user)
|
||||
if c:
|
||||
conditions.append(c)
|
||||
|
||||
return " and ".join(conditions) if conditions else None
|
||||
permision_script_name = get_server_script_map().get("permission_query").get(self.doctype)
|
||||
if permision_script_name:
|
||||
script = frappe.get_doc("Server Script", permision_script_name)
|
||||
condition = script.get_permission_query_conditions(self.user)
|
||||
if condition:
|
||||
conditions.append(condition)
|
||||
|
||||
return " and ".join(conditions) if conditions else ""
|
||||
|
||||
|
||||
def run_custom_query(self, query):
|
||||
if '%(key)s' in query:
|
||||
|
|
|
|||
|
|
@ -209,7 +209,8 @@ class Meta(Document):
|
|||
'owner': _('Created By'),
|
||||
'modified_by': _('Modified By'),
|
||||
'creation': _('Created On'),
|
||||
'modified': _('Last Modified On')
|
||||
'modified': _('Last Modified On'),
|
||||
'_assign': _('Assigned To')
|
||||
}.get(fieldname) or _('No Label')
|
||||
return label
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow')
|
|||
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02
|
||||
execute:frappe.reload_doc('core', 'doctype', 'has_role')
|
||||
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02
|
||||
execute:frappe.reload_doc('core', 'doctype', 'server_script')
|
||||
frappe.patches.v11_0.replicate_old_user_permissions
|
||||
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
|
||||
frappe.patches.v7_1.rename_scheduler_log_to_error_log
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", {
|
|||
}
|
||||
frm.trigger('render_buttons');
|
||||
frm.toggle_display('standard', frappe.boot.developer_mode);
|
||||
frm.trigger('hide_absolute_value_field');
|
||||
},
|
||||
render_buttons: function (frm) {
|
||||
frm.page.clear_inner_toolbar();
|
||||
|
|
@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", {
|
|||
frm.set_value('show_section_headings', value);
|
||||
frm.set_value('line_breaks', value);
|
||||
frm.trigger('render_buttons');
|
||||
},
|
||||
doc_type: function (frm) {
|
||||
frm.trigger('hide_absolute_value_field');
|
||||
},
|
||||
hide_absolute_value_field: function (frm) {
|
||||
// TODO: make it work with frm.doc.doc_type
|
||||
// Problem: frm isn't updated in some random cases
|
||||
const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type;
|
||||
if (doctype) {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
const meta = frappe.get_meta(doctype);
|
||||
const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype));
|
||||
frm.toggle_display('absolute_value', has_int_float_currency_field.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"align_labels_right",
|
||||
"show_section_headings",
|
||||
"line_breaks",
|
||||
"absolute_value",
|
||||
"column_break_11",
|
||||
"font",
|
||||
"css_section",
|
||||
|
|
@ -196,13 +197,21 @@
|
|||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Print Format Builder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "doc_type",
|
||||
"description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive",
|
||||
"fieldname": "absolute_value",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Absolute Values"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-print",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-27 18:27:58.307070",
|
||||
"modified": "2020-12-14 11:38:49.132061",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Format",
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
|
|||
.replace('%H', 'HH')
|
||||
.replace('%M', 'mm')
|
||||
.replace('%S', 'ss')
|
||||
.replace('%b', 'Mon')
|
||||
: null;
|
||||
|
||||
let column_title = `<span class="indicator green">
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ frappe.Application = Class.extend({
|
|||
user: frappe.session.user
|
||||
},
|
||||
callback: function(r) {
|
||||
console.log(r);
|
||||
if(r.message.show_alert){
|
||||
frappe.show_alert({
|
||||
indicator: 'red',
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,31 @@ $.extend(frappe.model, {
|
|||
return docfield[0];
|
||||
},
|
||||
|
||||
get_from_localstorage: function(doctype) {
|
||||
if (localStorage["_doctype:" + doctype]) {
|
||||
return JSON.parse(localStorage["_doctype:" + doctype]);
|
||||
}
|
||||
},
|
||||
|
||||
set_in_localstorage: function(doctype, docs) {
|
||||
try {
|
||||
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
|
||||
} catch(e) {
|
||||
// if quota is exceeded, clear local storage and set item
|
||||
console.warn("localStorage quota exceeded, clearing doctype cache")
|
||||
frappe.model.clear_local_storage();
|
||||
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
|
||||
}
|
||||
},
|
||||
|
||||
clear_local_storage: function() {
|
||||
for(var key in localStorage) {
|
||||
if (key.startsWith("_doctype:")) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
with_doctype: function(doctype, callback, async) {
|
||||
if(locals.DocType[doctype]) {
|
||||
callback && callback();
|
||||
|
|
@ -110,13 +135,15 @@ $.extend(frappe.model, {
|
|||
let cached_timestamp = null;
|
||||
let cached_doc = null;
|
||||
|
||||
if(localStorage["_doctype:" + doctype]) {
|
||||
let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]);
|
||||
let cached_docs = frappe.model.get_from_localstorage(doctype)
|
||||
|
||||
if (cached_docs) {
|
||||
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
|
||||
if(cached_doc) {
|
||||
cached_timestamp = cached_doc.modified;
|
||||
}
|
||||
}
|
||||
|
||||
return frappe.call({
|
||||
method:'frappe.desk.form.load.getdoctype',
|
||||
type: "GET",
|
||||
|
|
@ -134,7 +161,7 @@ $.extend(frappe.model, {
|
|||
if(r.message=="use_cache") {
|
||||
frappe.model.sync(cached_doc);
|
||||
} else {
|
||||
localStorage["_doctype:" + doctype] = JSON.stringify(r.docs);
|
||||
frappe.model.set_in_localstorage(doctype, r.docs)
|
||||
}
|
||||
frappe.model.init_doctype(doctype);
|
||||
|
||||
|
|
|
|||
|
|
@ -979,17 +979,42 @@ Object.assign(frappe.utils, {
|
|||
return route;
|
||||
},
|
||||
|
||||
shorten_number: function (number, country) {
|
||||
country = (country == 'India') ? country : '';
|
||||
shorten_number: function (number, country, min_length=4, max_no_of_decimals=2) {
|
||||
/* returns the number as an abbreviated string
|
||||
* PARAMS
|
||||
* number - number to be shortened
|
||||
* country - country that determines the numnber system to be used
|
||||
* min_length - length below which the number will not be shortened
|
||||
* max_no_of_decimals - max number of decimals of the shortened number
|
||||
*/
|
||||
|
||||
// return number if total digits is lesser than min_length
|
||||
const len = String(number).match(/\d/g).length;
|
||||
if (len < min_length) return number.toString();
|
||||
|
||||
const number_system = this.get_number_system(country);
|
||||
let x = Math.abs(Math.round(number));
|
||||
for (const map of number_system) {
|
||||
const condition = map.condition ? map.condition(x) : x >= map.divisor;
|
||||
if (condition) {
|
||||
return (number/map.divisor).toFixed(2) + ' ' + map.symbol;
|
||||
if (x >= map.divisor) {
|
||||
let result = number/map.divisor;
|
||||
const no_of_decimals = this.get_number_of_decimals(result);
|
||||
/*
|
||||
If no_of_decimals is greater than max_no_of_decimals,
|
||||
round the number to max_no_of_decimals
|
||||
*/
|
||||
result = no_of_decimals > max_no_of_decimals
|
||||
? result.toFixed(max_no_of_decimals)
|
||||
: result;
|
||||
return result + ' ' + map.symbol;
|
||||
}
|
||||
}
|
||||
return number.toFixed();
|
||||
|
||||
return number.toFixed(max_no_of_decimals);
|
||||
},
|
||||
|
||||
get_number_of_decimals: function (number) {
|
||||
if (Math.floor(number) === number) return 0;
|
||||
return number.toString().split(".")[1].length || 0;
|
||||
},
|
||||
|
||||
get_number_system: function (country) {
|
||||
|
|
@ -1019,9 +1044,11 @@ Object.assign(frappe.utils, {
|
|||
{
|
||||
divisor: 1.0e+3,
|
||||
symbol: 'K',
|
||||
condition: (num) => num.toFixed().length > 5
|
||||
}]
|
||||
};
|
||||
|
||||
if (!Object.keys(number_system_map).includes(country)) country = '';
|
||||
|
||||
return number_system_map[country];
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1112,7 +1112,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
get_filter_values(raise) {
|
||||
const mandatory = this.filters.filter(f => f.df.reqd);
|
||||
|
||||
// check for mandatory property for filters added via UI
|
||||
const mandatory = this.filters.filter(f => (f.df.reqd || f.df.mandatory));
|
||||
const missing_mandatory = mandatory.filter(f => !f.get_value());
|
||||
if (raise && missing_mandatory.length > 0) {
|
||||
let message = __('Please set filters');
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export default class NumberCardWidget extends Widget {
|
|||
|
||||
get_formatted_number(df) {
|
||||
const default_country = frappe.sys_defaults.country;
|
||||
const shortened_number = frappe.utils.shorten_number(this.number, default_country);
|
||||
const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5);
|
||||
let number_parts = shortened_number.split(' ');
|
||||
|
||||
const symbol = number_parts[1] || '';
|
||||
|
|
|
|||
|
|
@ -137,9 +137,10 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
|
|||
{% elif df.fieldtype=="HTML" %}
|
||||
{{ frappe.render_template(df.options, {"doc":doc}) }}
|
||||
{% elif df.fieldtype=="Currency" %}
|
||||
{{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }}
|
||||
{% else %}
|
||||
{{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
|
||||
{% else %}
|
||||
{%- set parent = parent_doc or doc -%}
|
||||
{{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import glob
|
|||
import frappe
|
||||
import frappe.recorder
|
||||
from frappe.installer import add_to_installed_apps
|
||||
from frappe.utils import add_to_date, now
|
||||
from frappe.utils import add_to_date, get_bench_relative_path, now
|
||||
from frappe.utils.backups import fetch_latest_backups
|
||||
|
||||
|
||||
|
|
@ -364,3 +364,21 @@ class TestCommands(BaseTestCommands):
|
|||
else:
|
||||
installed_apps = set(frappe.get_installed_apps())
|
||||
self.assertSetEqual(list_apps, installed_apps)
|
||||
|
||||
def test_get_bench_relative_path(self):
|
||||
bench_path = frappe.utils.get_bench_path()
|
||||
test1_path = os.path.join(bench_path, "test1.txt")
|
||||
test2_path = os.path.join(bench_path, "sites", "test2.txt")
|
||||
|
||||
with open(test1_path, "w+") as test1:
|
||||
test1.write("asdf")
|
||||
with open(test2_path, "w+") as test2:
|
||||
test2.write("asdf")
|
||||
|
||||
self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
|
||||
self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
|
||||
with self.assertRaises(SystemExit):
|
||||
get_bench_relative_path("test3.txt")
|
||||
|
||||
os.remove(test1_path)
|
||||
os.remove(test2_path)
|
||||
|
|
|
|||
|
|
@ -734,3 +734,27 @@ def get_build_version():
|
|||
# .build can sometimes not exist
|
||||
# this is not a major problem so send fallback
|
||||
return frappe.utils.random_string(8)
|
||||
|
||||
def get_bench_relative_path(file_path):
|
||||
"""Fixes paths relative to the bench root directory if exists and returns the absolute path
|
||||
|
||||
Args:
|
||||
file_path (str, Path): Path of a file that exists on the file system
|
||||
|
||||
Returns:
|
||||
str: Absolute path of the file_path
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
base_path = '..'
|
||||
elif file_path.startswith(os.sep):
|
||||
base_path = os.sep
|
||||
else:
|
||||
base_path = '.'
|
||||
|
||||
file_path = os.path.join(base_path, file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print('Invalid path {0}'.format(file_path[3:]))
|
||||
sys.exit(1)
|
||||
|
||||
return os.path.abspath(file_path)
|
||||
|
|
|
|||
|
|
@ -369,6 +369,8 @@ def format_duration(seconds, hide_days=False):
|
|||
|
||||
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
|
||||
"""
|
||||
|
||||
seconds = cint(seconds)
|
||||
|
||||
total_duration = {
|
||||
'days': math.floor(seconds / (3600 * 24)),
|
||||
|
|
@ -1321,12 +1323,14 @@ def generate_hash(*args, **kwargs):
|
|||
|
||||
def guess_date_format(date_string):
|
||||
DATE_FORMATS = [
|
||||
r"%d/%b/%y",
|
||||
r"%d-%m-%Y",
|
||||
r"%m-%d-%Y",
|
||||
r"%Y-%m-%d",
|
||||
r"%d-%m-%y",
|
||||
r"%m-%d-%y",
|
||||
r"%y-%m-%d",
|
||||
r"%y-%b-%d",
|
||||
r"%d/%m/%Y",
|
||||
r"%m/%d/%Y",
|
||||
r"%Y/%m/%d",
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
|
|||
doc.print_section_headings = print_format.show_section_headings
|
||||
doc.print_line_breaks = print_format.line_breaks
|
||||
doc.align_labels_right = print_format.align_labels_right
|
||||
doc.absolute_value = print_format.absolute_value
|
||||
|
||||
def get_template_from_string():
|
||||
return jenv.from_string(get_print_format(doc.doctype,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"driver.js": "^0.9.8",
|
||||
"express": "^4.17.1",
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"frappe-charts": "^1.5.1",
|
||||
"frappe-charts": "^1.5.5",
|
||||
"frappe-datatable": "^1.15.3",
|
||||
"frappe-gantt": "^0.5.0",
|
||||
"fuse.js": "^3.4.6",
|
||||
|
|
|
|||
14
yarn.lock
14
yarn.lock
|
|
@ -2299,10 +2299,10 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
frappe-charts@^1.5.1:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b"
|
||||
integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw==
|
||||
frappe-charts@^1.5.5:
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.5.tgz#5f44a3639aecc6f8fc7d15752abc80bb68e26734"
|
||||
integrity sha512-L9pJTsrSuRobS/EaBKT8i1x+DVOjkXyUwT85cteZAPqynU/7K+uqjQOy4tMSTv5zsTWJNWFJ37ax68T73YdR3g==
|
||||
|
||||
frappe-datatable@^1.15.3:
|
||||
version "1.15.3"
|
||||
|
|
@ -2918,9 +2918,9 @@ inherits@2.0.3:
|
|||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
inquirer@^7.3.3:
|
||||
version "7.3.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue