Merge branch 'develop' of https://github.com/frappe/frappe into rebrand-ui

This commit is contained in:
Suraj Shetty 2020-12-11 13:02:36 +05:30
commit 25dc9ee222
33 changed files with 417 additions and 90 deletions

View file

@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', {
// auto repeat schedule
frappe.auto_repeat.render_schedule(frm);
frm.trigger('toggle_submit_on_creation');
},
reference_doctype: function(frm) {
frm.trigger('toggle_submit_on_creation');
},
toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
},
template: function(frm) {

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:AUT-AR-{#####}",
@ -12,6 +13,7 @@
"section_break_3",
"reference_doctype",
"reference_document",
"submit_on_creation",
"column_break_5",
"start_date",
"end_date",
@ -186,9 +188,16 @@
"fieldname": "repeat_on_last_day",
"fieldtype": "Check",
"label": "Repeat on Last Day of the Month"
},
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
}
],
"modified": "2019-07-17 11:30:51.412317",
"links": [],
"modified": "2020-12-10 10:43:13.449172",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",

View file

@ -21,6 +21,7 @@ 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.set_dates()
@ -60,6 +61,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
@ -150,6 +156,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 +169,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))

View file

@ -111,6 +111,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,6 +137,7 @@ 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 "",
@ -128,3 +148,34 @@ def make_auto_repeat(**args):
}).insert(ignore_permissions=True)
return doc
def create_submittable_doctype(doctype):
if frappe.db.exists('DocType', doctype):
return
else:
doc = frappe.get_doc({
'doctype': 'DocType',
'__newname': doctype,
'module': 'Custom',
'custom': 1,
'is_submittable': 1,
'fields': [{
'fieldname': 'test',
'label': 'Test',
'fieldtype': 'Data'
}],
'permissions': [{
'role': 'System Manager',
'read': 1,
'write': 1,
'create': 1,
'delete': 1,
'submit': 1,
'cancel': 1,
'amend': 1
}]
}).insert()
doc.allow_auto_repeat = 1
doc.save()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@
"align_labels_right",
"show_section_headings",
"line_breaks",
"absolute_value",
"column_break_11",
"font",
"css_section",
@ -196,13 +197,21 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Print Format Builder"
},
{
"default": "0",
"depends_on": "doc_type",
"description": "If checked, negative numberic 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-10 18:58:55.598269",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",

View file

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

View file

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

View file

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

View file

@ -1033,19 +1033,6 @@ Object.assign(frappe.utils, {
return duration_options;
},
shorten_number: function (number, country) {
country = (country == 'India') ? country : '';
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;
}
}
return number.toFixed();
},
get_number_system: function (country) {
let number_system_map = {
'India':
@ -1073,9 +1060,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];
},
@ -1125,7 +1114,7 @@ Object.assign(frappe.utils, {
route = strip(item.link, "#");
} else if (type === "doctype") {
let doctype_slug = frappe.router.slug(item.doctype);
if (frappe.model.is_single(item.doctype)) {
route = doctype_slug;
} else {
@ -1136,7 +1125,7 @@ Object.assign(frappe.utils, {
item.doc_view = "List";
}
}
switch (item.doc_view) {
case "List":
if (item.filters) {
@ -1173,11 +1162,11 @@ Object.assign(frappe.utils, {
} else if (type === "dashboard") {
route = "dashboard/" + item.name;
}
} else {
route = item.route;
}
if (item.route_options) {
route +=
"?" +
@ -1187,7 +1176,7 @@ Object.assign(frappe.utils, {
);
}).join("&");
}
// if(type==="page" || type==="help" || type==="report" ||
// (item.doctype && frappe.model.can_read(item.doctype))) {
// item.shown = true;
@ -1195,6 +1184,43 @@ Object.assign(frappe.utils, {
return `/app/${route}`;
},
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) {
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(max_no_of_decimals);
},
get_number_of_decimals: function (number) {
if (Math.floor(number) === number) return 0;
return number.toString().split(".")[1].length || 0;
},
build_summary_item(summary) {
if (summary.type == "separator") {
return $(`<div class="summary-separator">
@ -1203,7 +1229,6 @@ Object.assign(frappe.utils, {
}
let df = { fieldtype: summary.datatype };
let doc = null;
if (summary.datatype == "Currency") {
df.options = "currency";
doc = { currency: summary.currency };

View file

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

View file

@ -136,10 +136,9 @@ 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, doc, translated=df.translatable) }}
{% else %}
{{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }}
{%- set parent = parent_doc or doc -%}
{{ doc.get_formatted(df.fieldname, parent, translated=df.translatable, absolute_value=parent.absolute_value) }}
{% endif %}
{%- endmacro %}

View file

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

View file

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

View file

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

View file

@ -110,6 +110,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,