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

This commit is contained in:
Suraj Shetty 2020-11-06 10:42:37 +05:30
commit b5d9c1e816
219 changed files with 3803 additions and 2311 deletions

View file

@ -1,9 +1,10 @@
name: Trigger Docker build on release
name: 'Trigger Docker build on release'
on:
release:
types: [released]
jobs:
curl:
name: 'Trigger Docker build on release'
runs-on: ubuntu-latest
container:
image: alpine:latest

View file

@ -1,10 +1,11 @@
name: 'Documentation Required'
name: 'Documentation Check'
on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
jobs:
build:
docs-required:
name: 'Documentation Required'
runs-on: ubuntu-latest
steps:

View file

@ -1,11 +1,12 @@
name: Build and Publish Assets for Development
name: 'Frappe Assets'
on:
push:
branches: [ develop ]
jobs:
build:
build-dev-and-publish:
name: 'Build and Publish Assets for Development'
runs-on: ubuntu-latest
steps:

View file

@ -1,4 +1,4 @@
name: Build and Publish Assets built for Releases
name: 'Frappe Assets'
on:
release:
@ -8,7 +8,8 @@ env:
GITHUB_TOKEN: ${{ github.token }}
jobs:
build:
build-release-and-publish:
name: 'Build and Publish Assets built for Releases'
runs-on: ubuntu-latest
steps:
@ -44,4 +45,3 @@ jobs:
asset_path: build/assets.tar.gz
asset_name: assets.tar.gz
asset_content_type: application/octet-stream

View file

@ -5,7 +5,7 @@ pull_request_rules:
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk (frappe)
- label!=don't-merge
- label!=dont-merge
- label!=squash
- "#approved-reviews-by>=1"
actions:
@ -17,7 +17,7 @@ pull_request_rules:
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk (frappe)
- label!=don't-merge
- label!=dont-merge
- label=squash
- "#approved-reviews-by>=1"
actions:

View file

@ -31,12 +31,12 @@ matrix:
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
script: bench --verbose --site test_site run-tests --coverage
script: bench --site test_site run-tests --coverage
- name: "Python 3.7 PostgreSQL"
python: 3.7
env: DB=postgres TYPE=server
script: bench --verbose --site test_site run-tests --coverage
script: bench --site test_site run-tests --coverage
- name: "Cypress"
python: 3.7

View file

@ -8,10 +8,10 @@ website/ @scmmishra
web_form/ @scmmishra
templates/ @scmmishra
www/ @scmmishra
integrations/ @Mangesh-Khairnar
integrations/ @nextchamp-saqib
patches/ @sahil28297
dashboard/ @prssanna
email/ @Thunderbottom
email/ @saurabh6790
event_streaming/ @ruchamahabal
data_import* @netchampfaris
core/ @surajshetty3416

View file

@ -312,7 +312,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -321,6 +321,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param title: [optional] Message title.
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
@ -346,16 +347,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
return
if as_table and type(msg) in (list, tuple):
table_rows = ''
for row in msg:
table_row_data = ''
for data in row:
table_row_data += '<td>{}</td>'.format(data)
table_rows += '<tr>{}</tr>'.format(table_row_data)
out.message = '''<table class="table table-bordered"
style="margin: 0;">{}</table>'''.format(table_rows)
out.as_table = 1
if as_list and type(msg) in (list, tuple) and len(msg) > 1:
out.as_list = 1
if flags.print_messages and out.message:
print(f"Message: {repr(out.message).encode('utf-8')}")
@ -405,12 +400,12 @@ def clear_last_message():
if len(local.message_log) > 0:
local.message_log = local.message_log[:-1]
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None):
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, as_list=False):
"""Throw execption and show message (`msgprint`).
:param msg: Message.
:param exc: Exception class. Default `frappe.ValidationError`"""
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide)
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
def emit_js(js, user=False, **kwargs):
if user == False:
@ -1159,6 +1154,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
'doctype_or_field': args.doctype_or_field,
'doc_type': doctype,
'field_name': args.fieldname,
'row_name': args.row_name,
'property': args.property,
'value': args.value,
'property_type': args.property_type or "Data",

View file

@ -160,6 +160,10 @@ def handle_exception(e):
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
if frappe.conf.get('developer_mode'):
# don't fail silently
print(frappe.get_traceback())
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
# handle ajax responses first
# if the request is ajax, send back the trace or error message

View file

@ -2,38 +2,70 @@
// For license information, please see license.txt
frappe.ui.form.on('Assignment Rule', {
onload: (frm) => {
frm.trigger('set_due_date_field_options');
},
refresh: function(frm) {
frm.trigger('setup_assignment_days_buttons');
frm.trigger('set_options');
// refresh description
frm.events.rule(frm);
},
document_type: function(frm) {
frm.trigger('set_options');
},
setup_assignment_days_buttons: function(frm) {
const labels = ['Weekends', 'Weekdays', 'All Days'];
let get_days = (label) => {
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const weekends = ['Saturday', 'Sunday'];
return {
'All Days': weekdays.concat(weekends),
'Weekdays': weekdays,
'Weekends': weekends,
}[label];
};
let set_days = (e) => {
frm.clear_table('assignment_days');
const label = $(e.currentTarget).text();
get_days(label).forEach((day) =>
frm.add_child('assignment_days', { day: day })
);
frm.refresh_field('assignment_days');
};
labels.forEach(label =>
frm.fields_dict['assignment_days'].grid.add_custom_button(
label,
set_days,
'top'
)
);
},
rule: function(frm) {
if (frm.doc.rule === 'Round Robin') {
frm.get_field('rule').set_description(__('Assign one by one, in sequence'));
} else {
frm.get_field('rule').set_description(__('Assign to the one who has the least assignments'));
}
const description_map = {
'Round Robin': __('Assign one by one, in sequence'),
'Load Balancing': __('Assign to the one who has the least assignments'),
'Based on Field': __('Assign to the user set in this field'),
};
frm.get_field('rule').set_description(description_map[frm.doc.rule]);
},
document_type: (frm) => {
frm.trigger('set_due_date_field_options');
},
set_due_date_field_options: (frm) => {
let doctype = frm.doc.document_type;
let datetime_fields = [];
set_options(frm) {
const doctype = frm.doc.document_type;
frm.set_fields_as_options(
'field',
doctype,
(df) => df.fieldtype == 'Link' && df.options == 'User',
[{ label: 'Owner', value: 'owner' }]
);
if (doctype) {
frappe.model.with_doctype(doctype, () => {
frappe.get_meta(doctype).fields.map((df) => {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
datetime_fields.push({ label: df.label, value: df.fieldname });
}
});
if (datetime_fields) {
frm.set_df_property('due_date_based_on', 'options', datetime_fields);
}
frm.set_df_property('due_date_based_on', 'hidden', !datetime_fields.length);
});
frm.set_fields_as_options(
'due_date_based_on',
doctype,
(df) => ['Date', 'Datetime'].includes(df.fieldtype)
).then(options => frm.set_df_property('due_date_based_on', 'hidden', !options.length));
}
}
},
});

View file

@ -24,6 +24,7 @@
"assignment_days",
"assign_to_users_section",
"rule",
"field",
"users",
"last_user"
],
@ -93,15 +94,16 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Rule",
"options": "Round Robin\nLoad Balancing",
"options": "Round Robin\nLoad Balancing\nBased on Field",
"reqd": 1
},
{
"depends_on": "eval: doc.rule !== 'Based on Field'",
"fieldname": "users",
"fieldtype": "Table MultiSelect",
"label": "Users",
"options": "Assignment Rule User",
"reqd": 1
"mandatory_depends_on": "eval: doc.rule !== 'Based on Field'",
"options": "Assignment Rule User"
},
{
"fieldname": "last_user",
@ -134,15 +136,22 @@
},
{
"depends_on": "document_type",
"description": "Value from this field will be set as the due date in the ToDo",
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"description": "Value from this field will be set as the due date in the ToDo"
"label": "Due Date Based On"
},
{
"depends_on": "eval: doc.rule == 'Based on Field'",
"fieldname": "field",
"fieldtype": "Select",
"label": "Field",
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-13 06:48:54.019735",
"modified": "2020-10-20 14:47:20.662954",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",

View file

@ -38,27 +38,30 @@ class AssignmentRule(Document):
def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc):
self.do_assignment(doc)
return True
return self.do_assignment(doc)
def do_assignment(self, doc):
# clear existing assignment, to reassign
assign_to.clear(doc.get('doctype'), doc.get('name'))
user = self.get_user()
user = self.get_user(doc)
assign_to.add(dict(
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
))
if user:
assign_to.add(dict(
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
))
# set for reference in round robin
self.db_set('last_user', user)
# set for reference in round robin
self.db_set('last_user', user)
return True
return False
def clear_assignment(self, doc):
'''Clear assignments'''
@ -70,7 +73,7 @@ class AssignmentRule(Document):
if self.safe_eval('close_condition', doc):
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name'))
def get_user(self):
def get_user(self, doc):
'''
Get the next user for assignment
'''
@ -78,6 +81,8 @@ class AssignmentRule(Document):
return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
return self.get_user_load_balancing()
elif self.rule == 'Based on Field':
return doc.get(self.field)
def get_user_round_robin(self):
'''

View file

@ -88,6 +88,30 @@ class TestAutoAssign(unittest.TestCase):
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
def test_based_on_field(self):
self.assignment_rule.rule = 'Based on Field'
self.assignment_rule.field = 'owner'
self.assignment_rule.save()
frappe.set_user('test1@example.com')
note = make_note(dict(public=1))
# check if auto assigned to doc owner, test1@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test1@example.com')
frappe.set_user('test2@example.com')
note = make_note(dict(public=1))
# check if auto assigned to doc owner, test2@example.com
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test2@example.com')
frappe.set_user('Administrator')
def test_assign_condition(self):
# check condition
@ -287,4 +311,4 @@ def make_note(values=None):
note.insert()
return note
return note

View file

@ -1,76 +1,34 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"allow_read": 1,
"creation": "2019-02-27 11:41:46.602400",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"index_web_pages_for_search": 1,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-27 17:16:41.399261",
"links": [],
"modified": "2020-09-29 20:12:14.456785",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule User",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -615,8 +615,10 @@ def browse(context, site):
@click.command('start-recording')
@pass_context
def start_recording(context):
import frappe.recorder
for site in context.sites:
frappe.init(site=site)
frappe.set_user("Administrator")
frappe.recorder.start()
if not context.sites:
raise SiteNotSpecifiedError
@ -625,8 +627,10 @@ def start_recording(context):
@click.command('stop-recording')
@pass_context
def stop_recording(context):
import frappe.recorder
for site in context.sites:
frappe.init(site=site)
frappe.set_user("Administrator")
frappe.recorder.stop()
if not context.sites:
raise SiteNotSpecifiedError

View file

@ -460,11 +460,21 @@ def console(context):
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
import IPython
all_apps = frappe.get_installed_apps()
failed_to_import = []
for app in all_apps:
locals()[app] = __import__(app)
try:
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
IPython.embed(display_banner="", header="", colors="neutral")

View file

@ -75,7 +75,7 @@
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
"label": "State/Province"
},
{
"fieldname": "country",
@ -148,7 +148,7 @@
"icon": "fa fa-map-marker",
"idx": 5,
"links": [],
"modified": "2020-10-14 17:38:08.971776",
"modified": "2020-10-21 16:14:37.284830",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Address",

View file

@ -259,10 +259,8 @@ class Communication(Document):
# Timeline Links
def set_timeline_links(self):
contacts = []
if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \
frappe.flags.in_test:
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled)
for contact_name in contacts:
self.add_link('Contact', contact_name)
@ -341,7 +339,7 @@ def get_permission_query_conditions_for_communication(user):
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
def get_contacts(email_strings):
def get_contacts(email_strings, auto_create_contact=False):
email_addrs = []
for email_string in email_strings:
@ -356,7 +354,7 @@ def get_contacts(email_strings):
email = get_email_without_link(email)
contact_name = get_contact_name(email)
if not contact_name and email:
if not contact_name and email and auto_create_contact:
email_parts = email.split("@")
first_name = frappe.unscrub(email_parts[0])

View file

@ -55,7 +55,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
comm = frappe.get_doc({
"doctype":"Communication",
"subject": subject,
"content": frappe.utils.sanitize_html(content),
"content": content,
"sender": sender,
"sender_full_name":sender_full_name,
"recipients": recipients,

View file

@ -1,2 +1,2 @@
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
Test 26 ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
Title,Description,Number,another_number,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
Test 26,test description,1,2,"",child title,child description,child title,14-08-2019,4,child title again,22-09-2020,5,7

Can't render this file because it contains an unexpected character in line 2 and column 56.

View file

@ -616,7 +616,9 @@ class Row:
id_field = get_id_field(doctype)
id_value = doc.get(id_field.fieldname)
if id_value and frappe.db.exists(doctype, id_value):
doc = frappe.get_doc(doctype, id_value)
existing_doc = frappe.get_doc(doctype, id_value)
existing_doc.update(doc)
doc = existing_doc
else:
# for table rows being inserted in update
# create a new doc with defaults set

View file

@ -5,12 +5,14 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.core.doctype.data_import.importer import Importer
from frappe.utils import getdate, format_duration
doctype_name = 'DocType for Import'
class TestImporter(unittest.TestCase):
def setUp(self):
@classmethod
def setUpClass(cls):
create_doctype_if_not_exists(doctype_name)
def test_data_import_from_file(self):
@ -71,19 +73,28 @@ class TestImporter(unittest.TestCase):
self.assertEqual(warnings[2]['message'], "<b>Title</b> is a mandatory field")
def test_data_import_update(self):
if not frappe.db.exists(doctype_name, 'Test 26'):
frappe.get_doc(
doctype=doctype_name,
title='Test 26'
).insert()
existing_doc = frappe.get_doc(
doctype=doctype_name,
title=frappe.generate_hash(doctype_name, 8),
table_field_1=[{'child_title': 'child title to update'}]
)
existing_doc.save()
frappe.db.commit()
import_file = get_import_file('sample_import_file_for_update')
data_import = self.get_importer(doctype_name, import_file, update=True)
data_import.start_import()
i = Importer(data_import.reference_doctype, data_import=data_import)
updated_doc = frappe.get_doc(doctype_name, 'Test 26')
# update child table id in template date
i.import_file.raw_data[1][4] = existing_doc.table_field_1[0].name
i.import_file.raw_data[1][0] = existing_doc.name
i.import_file.parse_data_from_template()
i.import_data()
updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
self.assertEqual(updated_doc.description, 'test description')
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title')
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description')
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again')

View file

@ -13,6 +13,7 @@
"fieldname",
"precision",
"length",
"non_negative",
"hide_days",
"hide_seconds",
"reqd",
@ -473,13 +474,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-28 11:28:21.252853",
"modified": "2020-10-29 06:09:26.454990",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -5,6 +5,7 @@
from __future__ import unicode_literals
import re, copy, os, shutil
import json
from frappe.cache_manager import clear_user_cache
# imports - third party imports
import six
@ -103,6 +104,10 @@ class DocType(Document):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def after_insert(self):
# clear user cache so that on the next reload this doctype is included in boot
clear_user_cache(frappe.session.user)
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -747,8 +752,8 @@ def validate_fields(meta):
def check_illegal_default(d):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
if d.fieldtype == "Check" and d.default not in ('0', '1'):
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
if d.fieldtype == "Check" and cint(d.default) not in (0, 1):
frappe.throw(_("Default for 'Check' type of field {0} must be either '0' or '1'").format(frappe.bold(d.fieldname)))
if d.fieldtype == "Select" and d.default:
if not d.options:
frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname)))

View file

@ -12,41 +12,22 @@ from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMan
class TestDocType(unittest.TestCase):
def new_doctype(self, name, unique=0, depends_on=''):
return frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [{
"label": "Some Field",
"fieldname": "some_fieldname",
"fieldtype": "Data",
"unique": unique,
"depends_on": depends_on,
}],
"permissions": [{
"role": "System Manager",
"read": 1,
}],
"name": name
})
def test_validate_name(self):
self.assertRaises(frappe.NameError, self.new_doctype("_Some DocType").insert)
self.assertRaises(frappe.NameError, self.new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, self.new_doctype("Some (DocType)").insert)
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
for name in ("Some DocType", "Some_DocType"):
if frappe.db.exists("DocType", name):
frappe.delete_doc("DocType", name)
doc = self.new_doctype(name).insert()
doc = new_doctype(name).insert()
doc.delete()
def test_doctype_unique_constraint_dropped(self):
if frappe.db.exists("DocType", "With_Unique"):
frappe.delete_doc("DocType", "With_Unique")
dt = self.new_doctype("With_Unique", unique=1)
dt = new_doctype("With_Unique", unique=1)
dt.insert()
doc1 = frappe.new_doc("With_Unique")
@ -67,7 +48,7 @@ class TestDocType(unittest.TestCase):
doc2.delete()
def test_validate_search_fields(self):
doc = self.new_doctype("Test Search Fields")
doc = new_doctype("Test Search Fields")
doc.search_fields = "some_fieldname"
doc.insert()
self.assertEqual(doc.name, "Test Search Fields")
@ -85,7 +66,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(frappe.ValidationError, doc.save)
def test_depends_on_fields(self):
doc = self.new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0")
doc = new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0")
doc.insert()
# check if the assignment operation is allowed in depends_on
@ -261,7 +242,7 @@ class TestDocType(unittest.TestCase):
frappe.flags.allow_doctype_export = 0
def test_unique_field_name_for_two_fields(self):
doc = self.new_doctype('Test Unique Field')
doc = new_doctype('Test Unique Field')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
@ -273,7 +254,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(UniqueFieldnameError, doc.insert)
def test_fieldname_is_not_name(self):
doc = self.new_doctype('Test Name Field')
doc = new_doctype('Test Name Field')
field_1 = doc.append('fields', {})
field_1.label = 'Name'
field_1.fieldtype = 'Data'
@ -283,7 +264,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(InvalidFieldNameError, doc.save)
def test_illegal_mandatory_validation(self):
doc = self.new_doctype('Test Illegal mandatory')
doc = new_doctype('Test Illegal mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Section Break'
@ -292,7 +273,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(IllegalMandatoryError, doc.insert)
def test_link_with_wrong_and_no_options(self):
doc = self.new_doctype('Test link')
doc = new_doctype('Test link')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Link'
@ -304,7 +285,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
def test_hidden_and_mandatory_without_default(self):
doc = self.new_doctype('Test hidden and mandatory')
doc = new_doctype('Test hidden and mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
@ -314,7 +295,7 @@ class TestDocType(unittest.TestCase):
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
def test_field_can_not_be_indexed_validation(self):
doc = self.new_doctype('Test index')
doc = new_doctype('Test index')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Long Text'
@ -327,14 +308,14 @@ class TestDocType(unittest.TestCase):
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create doctype
link_doc = self.new_doctype('Test Linked Doctype')
link_doc = new_doctype('Test Linked Doctype')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
doc = self.new_doctype('Test Doctype')
doc = new_doctype('Test Doctype')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype'
@ -377,12 +358,12 @@ class TestDocType(unittest.TestCase):
doc.delete()
frappe.db.commit()
def test_ignore_cancelation_of_linked_doctype_during_cancell(self):
def test_ignore_cancelation_of_linked_doctype_during_cancel(self):
import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create linked doctype
link_doc = self.new_doctype('Test Linked Doctype 1')
link_doc = new_doctype('Test Linked Doctype 1')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
@ -390,7 +371,7 @@ class TestDocType(unittest.TestCase):
link_doc.insert()
#create first parent doctype
test_doc_1 = self.new_doctype('Test Doctype 1')
test_doc_1 = new_doctype('Test Doctype 1')
test_doc_1.is_submittable = 1
field_2 = test_doc_1.append('fields', {})
@ -405,7 +386,7 @@ class TestDocType(unittest.TestCase):
test_doc_1.insert()
#crete second parent doctype
doc = self.new_doctype('Test Doctype 2')
doc = new_doctype('Test Doctype 2')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
@ -469,3 +450,28 @@ class TestDocType(unittest.TestCase):
doc.delete()
test_doc_1.delete()
frappe.db.commit()
def new_doctype(name, unique=0, depends_on='', fields=None):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [{
"label": "Some Field",
"fieldname": "some_fieldname",
"fieldtype": "Data",
"unique": unique,
"depends_on": depends_on,
}],
"permissions": [{
"role": "System Manager",
"read": 1,
}],
"name": name
})
if fields:
for f in fields:
doc.append('fields', f)
return doc

View file

@ -9,7 +9,8 @@
"action_type",
"action",
"group",
"hidden"
"hidden",
"custom"
],
"fields": [
{
@ -48,12 +49,19 @@
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "custom",
"fieldtype": "Check",
"hidden": 1,
"label": "Custom"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-21 14:44:03.845315",
"modified": "2020-09-24 14:19:05.549835",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Action",

View file

@ -7,7 +7,9 @@
"field_order": [
"link_doctype",
"link_fieldname",
"group"
"group",
"hidden",
"custom"
],
"fields": [
{
@ -30,10 +32,25 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "custom",
"fieldtype": "Check",
"hidden": 1,
"label": "Custom"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-09-24 11:41:25.291377",
"links": [],
"modified": "2020-09-24 14:19:25.189511",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",

View file

@ -34,7 +34,8 @@
"fieldname": "prefix",
"fieldtype": "Data",
"label": "Prefix",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"",
"reqd": 1
},
{
"fieldname": "counter",
@ -48,7 +49,8 @@
"fieldname": "prefix_digits",
"fieldtype": "Int",
"label": "Digits",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\"",
"reqd": 1
},
{
"fieldname": "naming_section",
@ -69,7 +71,7 @@
"options": "Document Naming Rule Condition"
},
{
"description": "Rules with higher priority will be applied first.",
"description": "Rules with higher priority number will be applied first.",
"fieldname": "priority",
"fieldtype": "Int",
"label": "Priority"
@ -77,7 +79,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-21 10:23:34.401539",
"modified": "2020-11-04 14:38:14.836056",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",

View file

@ -13,7 +13,7 @@ class DocumentNamingRule(Document):
Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
if self.conditions:
if not evaluate_filters(doc, [(d.field, d.condition, d.value) for d in self.conditions]):
if not evaluate_filters(doc, [(self.document_type, d.field, d.condition, d.value) for d in self.conditions]):
return
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0

View file

@ -2,7 +2,6 @@
"actions": [],
"creation": "2020-08-01 23:38:41.783206",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_label",
@ -30,6 +29,7 @@
"in_list_view": 1,
"label": "Item Type",
"options": "Route\nAction\nSeparator",
"read_only_depends_on": "eval:doc.is_standard",
"show_days": 1,
"show_seconds": 1
},
@ -59,6 +59,7 @@
"in_list_view": 1,
"label": "Route",
"mandatory_depends_on": "eval:doc.item_type == 'Route'",
"read_only_depends_on": "eval:doc.is_standard",
"show_days": 1,
"show_seconds": 1
},
@ -68,13 +69,14 @@
"fieldtype": "Data",
"label": "Action",
"mandatory_depends_on": "eval:doc.item_type == 'Action'",
"read_only_depends_on": "eval:doc.is_standard",
"show_days": 1,
"show_seconds": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-06 16:32:49.597060",
"modified": "2020-11-02 10:57:37.709262",
"modified_by": "Administrator",
"module": "Core",
"name": "Navbar Item",

View file

@ -2,7 +2,9 @@ frappe.ui.form.on('Report', {
refresh: function(frm) {
if (frm.doc.is_standard === "Yes" && !frappe.boot.developer_mode) {
// make the document read-only
frm.set_read_only();
frm.disable_form();
} else {
frm.enable_save();
}
let doc = frm.doc;
@ -32,8 +34,6 @@ frappe.ui.form.on('Report', {
});
}, doc.disabled ? "fa fa-check" : "fa fa-off");
}
frm.events.report_type(frm);
},
ref_doctype: function(frm) {

View file

@ -49,7 +49,9 @@ class Report(Document):
self.export_doc()
def on_trash(self):
if self.is_standard == 'Yes' and not cint(getattr(frappe.local.conf, 'developer_mode',0)):
if (self.is_standard == 'Yes'
and not cint(getattr(frappe.local.conf, 'developer_mode', 0))
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)

View file

@ -4,6 +4,8 @@
from __future__ import unicode_literals
import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
test_records = frappe.get_test_records('Report')
test_dependencies = ['User']
@ -27,7 +29,57 @@ class TestReport(unittest.TestCase):
columns, data = report.get_data(filters={'user': 'Administrator', 'doctype': 'DocType'})
self.assertEqual(columns[0].get('label'), 'Name')
self.assertEqual(columns[1].get('label'), 'Module')
self.assertTrue('User' in [d[0] for d in data])
self.assertTrue('User' in [d.get('name') for d in data])
def test_custom_report(self):
reset_customization('User')
custom_report_name = save_report(
'Permitted Documents For User',
'Permitted Documents For User Custom',
json.dumps([{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}]))
custom_report = frappe.get_doc('Report', custom_report_name)
columns, result = custom_report.run_query_report(
filters={
'user': 'Administrator',
'doctype': 'User'
}, user=frappe.session.user)
self.assertListEqual(['email'], [column.get('fieldname') for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator')
self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict)
def test_report_with_custom_column(self):
reset_customization('User')
response = run('Permitted Documents For User',
filters={'user': 'Administrator', 'doctype': 'User'},
custom_columns=[{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}])
result = response.get('result')
columns = response.get('columns')
self.assertListEqual(['name', 'email', 'user_type'], [column.get('fieldname') for column in columns])
admin_dict = frappe.core.utils.find(result, lambda d: d['name'] == 'Administrator')
self.assertDictEqual({'name': 'Administrator', 'user_type': 'System User', 'email': 'admin@example.com'}, admin_dict)
def test_report_permissions(self):
frappe.set_user('test@example.com')

View file

@ -36,7 +36,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.queue==='All'",
"depends_on": "eval:doc.frequency==='All'",
"fieldname": "create_log",
"fieldtype": "Check",
"label": "Create Log"
@ -49,7 +49,7 @@
},
{
"allow_in_quick_entry": 1,
"depends_on": "eval:doc.queue==='Cron'",
"depends_on": "eval:doc.frequency==='Cron'",
"fieldname": "cron_format",
"fieldtype": "Data",
"label": "Cron Format",
@ -81,7 +81,7 @@
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2020-04-05 17:27:33.480562",
"modified": "2020-10-07 10:39:24.519460",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",

View file

@ -20,9 +20,9 @@ class ScheduledJobType(Document):
# force logging for all events other than continuous ones (ALL)
self.create_log = 1
def enqueue(self):
def enqueue(self, force=False):
# enqueue event if last execution is done
if self.is_event_due():
if self.is_event_due() or force:
if frappe.flags.enqueued_jobs:
frappe.flags.enqueued_jobs.append(self.method)
@ -114,7 +114,7 @@ class ScheduledJobType(Document):
def execute_event(doc):
frappe.only_for('System Manager')
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue()
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue(force=True)
def run_scheduled_job(job_type):

View file

@ -47,7 +47,7 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user = filters.get("user")
user_perms = frappe.utils.user.UserPermissions(user)
user_perms.build_permissions()
can_read = user_perms.can_read
can_read = user_perms.can_read # Does not include child tables
single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})]

View file

@ -30,6 +30,7 @@
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"read_only",
@ -403,13 +404,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-28 11:28:44.377753",
"modified": "2020-10-29 06:14:43.073329",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -1,20 +0,0 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Custom Link', {
refresh: function(frm) {
frm.set_query("document_type", function () {
return {
filters: {
custom: 0,
istable: 0,
module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
}
};
});
frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() {
frappe.set_route('List', frm.doc.document_type);
});
}
});

View file

@ -5,6 +5,7 @@ frappe.provide("frappe.customize_form");
frappe.ui.form.on("Customize Form", {
onload: function(frm) {
frm.disable_save();
frm.set_query("doc_type", function() {
return {
translate_values: false,
@ -27,7 +28,7 @@ frappe.ui.form.on("Customize Form", {
});
$(frm.wrapper).on("grid-row-render", function(e, grid_row) {
if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
if (grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
$(grid_row.row).css({"font-weight": "bold"});
}
});
@ -40,19 +41,25 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_sortable");
});
if (localStorage['customize_doctype']) {
// set default value from customize form
frm.set_value('doc_type', localStorage['customize_doctype']);
}
},
doc_type: function(frm) {
if(frm.doc.doc_type) {
if (frm.doc.doc_type) {
return frm.call({
method: "fetch_to_customize",
doc: frm.doc,
freeze: true,
callback: function(r) {
if(r) {
if(r._server_messages && r._server_messages.length) {
if (r) {
if (r._server_messages && r._server_messages.length) {
frm.set_value("doc_type", "");
} else {
localStorage['customize_doctype'] = frm.doc.doc_type;
frm.refresh();
frm.trigger("setup_sortable");
}
@ -69,7 +76,7 @@ frappe.ui.form.on("Customize Form", {
frm.doc.fields.forEach(function(f, i) {
var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row');
if(f.is_custom_field) {
if (f.is_custom_field) {
data_row.addClass("highlight");
} else {
f._sortable = false;
@ -82,26 +89,26 @@ frappe.ui.form.on("Customize Form", {
frm.disable_save();
frm.page.clear_icons();
if(frm.doc.doc_type) {
if (frm.doc.doc_type) {
frappe.customize_form.set_primary_action(frm);
frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() {
frappe.set_route('List', frm.doc.doc_type);
});
}, __('Actions'));
frm.add_custom_button(__('Refresh Form'), function() {
frm.add_custom_button(__('Reload'), function() {
frm.script_manager.trigger("doc_type");
}, "fa fa-refresh", "btn-default");
}, __('Actions'));
frm.add_custom_button(__('Reset to defaults'), function() {
frappe.customize_form.confirm(__('Remove all customizations?'), frm);
}, "fa fa-eraser", "btn-default");
}, __('Actions'));
frm.add_custom_button(__('Set Permissions'), function() {
frappe.set_route('permission-manager', frm.doc.doc_type);
}, "fa fa-lock", "btn-default");
}, __('Actions'));
if(frappe.boot.developer_mode) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(__('Export Customizations'), function() {
frappe.prompt(
[
@ -124,34 +131,36 @@ frappe.ui.form.on("Customize Form", {
});
},
__("Select Module"));
});
}, __('Actions'));
}
}
// sort order select
if(frm.doc.doc_type) {
if (frm.doc.doc_type) {
var fields = $.map(frm.doc.fields,
function(df) { return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; });
function(df) {
return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null;
});
fields = ["", "name", "modified"].concat(fields);
frm.set_df_property("sort_field", "options", fields);
}
if(frappe.route_options && frappe.route_options.doc_type) {
if (frappe.route_options && frappe.route_options.doc_type) {
setTimeout(function() {
frm.set_value("doc_type", frappe.route_options.doc_type);
frappe.route_options = null;
}, 1000);
}
}
});
// can't delete standard fields
frappe.ui.form.on("Customize Form Field", {
before_fields_remove: function(frm, doctype, name) {
var row = frappe.get_doc(doctype, name);
if(!(row.is_custom_field || row.__islocal)) {
if (!(row.is_custom_field || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
throw "cannot delete custom field";
throw "cannot delete standard field";
}
},
fields_add: function(frm, cdt, cdn) {
@ -160,16 +169,46 @@ frappe.ui.form.on("Customize Form Field", {
}
});
// can't delete standard links
frappe.ui.form.on("DocType Link", {
before_links_remove: function(frm, doctype, name) {
let row = frappe.get_doc(doctype, name);
if (!(row.custom || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard link. You can hide it if you want"));
throw "cannot delete standard link";
}
},
links_add: function(frm, cdt, cdn) {
let f = frappe.model.get_doc(cdt, cdn);
f.custom = 1;
}
});
// can't delete standard actions
frappe.ui.form.on("DocType Action", {
before_actions_remove: function(frm, doctype, name) {
let row = frappe.get_doc(doctype, name);
if (!(row.custom || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard action. You can hide it if you want"));
throw "cannot delete standard action";
}
},
actions_add: function(frm, cdt, cdn) {
let f = frappe.model.get_doc(cdt, cdn);
f.custom = 1;
}
});
frappe.customize_form.set_primary_action = function(frm) {
frm.page.set_primary_action(__("Update"), function() {
if(frm.doc.doc_type) {
if (frm.doc.doc_type) {
return frm.call({
doc: frm.doc,
freeze: true,
btn: frm.page.btn_primary,
method: "save_customization",
callback: function(r) {
if(!r.exc) {
if (!r.exc) {
frappe.customize_form.clear_locals_and_refresh(frm);
frm.script_manager.trigger("doc_type");
}
@ -180,7 +219,7 @@ frappe.customize_form.set_primary_action = function(frm) {
};
frappe.customize_form.confirm = function(msg, frm) {
if(!frm.doc.doc_type) return;
if (!frm.doc.doc_type) return;
var d = new frappe.ui.Dialog({
title: 'Reset To Defaults',
@ -192,7 +231,7 @@ frappe.customize_form.confirm = function(msg, frm) {
doc: frm.doc,
method: "reset_to_defaults",
callback: function(r) {
if(r.exc) {
if (r.exc) {
frappe.msgprint(r.exc);
} else {
d.hide();

View file

@ -10,8 +10,9 @@
"doc_type",
"properties",
"label",
"default_print_format",
"max_attachments",
"search_fields",
"column_break_5",
"allow_copy",
"istable",
"editable_grid",
@ -20,22 +21,27 @@
"track_views",
"allow_auto_repeat",
"allow_import",
"show_preview_popup",
"image_view",
"column_break_5",
"fields_section_break",
"fields",
"view_settings_section",
"title_field",
"image_field",
"search_fields",
"section_break_8",
"sort_field",
"column_break_10",
"sort_order",
"section_break_23",
"default_print_format",
"column_break_29",
"show_preview_popup",
"image_view",
"email_settings_section",
"email_append_to",
"sender_field",
"subject_field",
"fields_section_break",
"fields"
"document_actions_section",
"actions",
"document_links_section",
"links",
"section_break_8",
"sort_field",
"column_break_10",
"sort_order"
],
"fields": [
{
@ -130,9 +136,11 @@
"label": "Search Fields"
},
{
"collapsible": 1,
"depends_on": "doc_type",
"fieldname": "section_break_8",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "List Settings"
},
{
"fieldname": "sort_field",
@ -161,7 +169,8 @@
"fieldname": "fields",
"fieldtype": "Table",
"label": "Fields",
"options": "Customize Form Field"
"options": "Customize Form Field",
"reqd": 1
},
{
"default": "0",
@ -200,24 +209,67 @@
"fieldtype": "Check",
"label": "Allow document creation via Email"
},
{
"depends_on": "doc_type",
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
},
{
"collapsible": 1,
"depends_on": "doc_type",
"fieldname": "view_settings_section",
"fieldtype": "Section Break",
"label": "View Settings"
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"collapsible_depends_on": "email_append_to",
"depends_on": "doc_type",
"fieldname": "email_settings_section",
"fieldtype": "Section Break",
"label": "Email Settings"
},
{
"collapsible": 1,
"collapsible_depends_on": "links",
"depends_on": "doc_type",
"fieldname": "document_links_section",
"fieldtype": "Section Break",
"label": "Document Links"
},
{
"fieldname": "links",
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
},
{
"collapsible": 1,
"collapsible_depends_on": "actions",
"depends_on": "doc_type",
"fieldname": "document_actions_section",
"fieldtype": "Section Break",
"label": "Document Actions"
},
{
"fieldname": "actions",
"fieldtype": "Table",
"label": "Actions",
"options": "DocType Action"
}
],
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-04-10 12:16:01.320411",
"modified": "2020-09-24 14:16:49.594012",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -6,6 +6,7 @@ from __future__ import unicode_literals
Customize Form is a Single DocType used to mask the Property Setter
Thus providing a better UI from user perspective
"""
import json
import frappe
import frappe.translate
from frappe import _
@ -14,80 +15,9 @@ from frappe.model.document import Document
from frappe.model import no_value_fields, core_doctypes_list
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter
from frappe.model.docfield import supports_translation
doctype_properties = {
'search_fields': 'Data',
'title_field': 'Data',
'image_field': 'Data',
'sort_field': 'Data',
'sort_order': 'Data',
'default_print_format': 'Data',
'allow_copy': 'Check',
'istable': 'Check',
'quick_entry': 'Check',
'editable_grid': 'Check',
'max_attachments': 'Int',
'track_changes': 'Check',
'track_views': 'Check',
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data'
}
docfield_properties = {
'idx': 'Int',
'label': 'Data',
'fieldtype': 'Select',
'options': 'Text',
'fetch_from': 'Small Text',
'fetch_if_empty': 'Check',
'permlevel': 'Int',
'width': 'Data',
'print_width': 'Data',
'reqd': 'Check',
'unique': 'Check',
'ignore_user_permissions': 'Check',
'in_list_view': 'Check',
'in_standard_filter': 'Check',
'in_global_search': 'Check',
'in_preview': 'Check',
'bold': 'Check',
'hidden': 'Check',
'collapsible': 'Check',
'collapsible_depends_on': 'Data',
'print_hide': 'Check',
'print_hide_if_no_value': 'Check',
'report_hide': 'Check',
'allow_on_submit': 'Check',
'translatable': 'Check',
'mandatory_depends_on': 'Data',
'read_only_depends_on': 'Data',
'depends_on': 'Data',
'description': 'Text',
'default': 'Text',
'precision': 'Select',
'read_only': 'Check',
'length': 'Int',
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check',
'hide_border': 'Check',
'hide_days': 'Check',
'hide_seconds': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), ('Data', 'Select'),
('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'), ('Table', 'Table MultiSelect'))
allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data')
class CustomizeForm(Document):
def on_update(self):
frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
@ -100,37 +30,64 @@ class CustomizeForm(Document):
meta = frappe.get_meta(self.doc_type)
if self.doc_type in core_doctypes_list:
return frappe.msgprint(_("Core DocTypes cannot be customized."))
self.validate_doctype(meta)
if meta.issingle:
return frappe.msgprint(_("Single DocTypes cannot be customized."))
if meta.custom:
return frappe.msgprint(_("Only standard DocTypes are allowed to be customized from Customize Form."))
# doctype properties
for property in doctype_properties:
self.set(property, meta.get(property))
for d in meta.get("fields"):
new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name}
for property in docfield_properties:
new_d[property] = d.get(property)
self.append("fields", new_d)
# load the meta properties on the customize (self) object
self.load_properties(meta)
# load custom translation
translation = self.get_name_translation()
self.label = translation.translated_text if translation else ''
#If allow_auto_repeat is set, add auto_repeat custom field.
self.create_auto_repeat_custom_field_if_requried(meta)
# NOTE doc (self) is sent to clientside by run_method
def validate_doctype(self, meta):
'''
Check if the doctype is allowed to be customized.
'''
if self.doc_type in core_doctypes_list:
frappe.throw(_("Core DocTypes cannot be customized."))
if meta.issingle:
frappe.throw(_("Single DocTypes cannot be customized."))
if meta.custom:
frappe.throw(_("Only standard DocTypes are allowed to be customized from Customize Form."))
def load_properties(self, meta):
'''
Load the customize object (this) with the metadata properties
'''
# doctype properties
for prop in doctype_properties:
self.set(prop, meta.get(prop))
for d in meta.get("fields"):
new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name}
for prop in docfield_properties:
new_d[prop] = d.get(prop)
self.append("fields", new_d)
for fieldname in ('links', 'actions'):
for d in meta.get(fieldname):
self.append(fieldname, d)
def create_auto_repeat_custom_field_if_requried(self, meta):
if self.allow_auto_repeat:
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}):
if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat',
'dt': self.doc_type}):
insert_after = self.fields[len(self.fields) - 1].fieldname
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
df = dict(
fieldname='auto_repeat',
label='Auto Repeat',
fieldtype='Link',
options='Auto Repeat',
insert_after=insert_after,
read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.doc_type, df)
# NOTE doc is sent to clientside by run_method
def get_name_translation(self):
'''Get translation object if exists of current doctype name in the default language'''
@ -195,72 +152,142 @@ class CustomizeForm(Document):
def set_property_setters(self):
meta = frappe.get_meta(self.doc_type)
# doctype property setters
for property in doctype_properties:
if self.get(property) != meta.get(property):
self.make_property_setter(property=property, value=self.get(property),
property_type=doctype_properties[property])
# doctype
self.set_property_setters_for_doctype(meta)
# docfield
for df in self.get("fields"):
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not meta_df or meta_df[0].get("is_custom_field"):
continue
self.set_property_setters_for_docfield(meta, df, meta_df)
for property in docfield_properties:
if property != "idx" and (df.get(property) or '') != (meta_df[0].get(property) or ''):
if property == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
# action and links
self.set_property_setters_for_actions_and_links(meta)
elif property == "allow_on_submit" and df.get(property):
if not frappe.db.get_value("DocField",
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
.format(df.idx))
continue
def set_property_setters_for_doctype(self, meta):
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)
elif property == "reqd" and \
((frappe.db.get_value("DocField",
{"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \
and (df.get(property) == 0)):
frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\
.format(df.idx))
continue
def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or '') != (meta_df[0].get(prop) or ''):
if not self.allow_property_change(prop, meta_df, df):
continue
elif property == "in_list_view" and df.get(property) \
and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields:
frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}")
.format(df.fieldtype, df.idx))
continue
self.make_property_setter(prop, df.get(prop), prop_type,
fieldname=df.fieldname)
elif property == "precision" and cint(df.get("precision")) > 6 \
and cint(df.get("precision")) > cint(meta_df[0].get("precision")):
self.flags.update_db = True
def allow_property_change(self, prop, meta_df, df):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
elif property == "unique":
self.flags.update_db = True
elif prop == "allow_on_submit" and df.get(prop):
if not frappe.db.get_value("DocField",
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
.format(df.idx))
return False
elif (property == "read_only" and cint(df.get("read_only"))==0
and frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "read_only")==1):
# if docfield has read_only checked and user is trying to make it editable, don't allow it
frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label))
continue
elif prop == "reqd" and \
((frappe.db.get_value("DocField",
{"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \
and (df.get(prop) == 0)):
frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\
.format(df.idx))
return False
elif property == "options" and df.get("fieldtype") not in allowed_fieldtype_for_options_change:
frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label))
continue
elif prop == "in_list_view" and df.get(prop) \
and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields:
frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}")
.format(df.fieldtype, df.idx))
return False
elif property == 'translatable' and not supports_translation(df.get('fieldtype')):
frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label))
continue
elif prop == "precision" and cint(df.get("precision")) > 6 \
and cint(df.get("precision")) > cint(meta_df[0].get("precision")):
self.flags.update_db = True
elif (property == 'in_global_search' and
df.in_global_search != meta_df[0].get("in_global_search")):
self.flags.rebuild_doctype_for_global_search = True
elif prop == "unique":
self.flags.update_db = True
self.make_property_setter(property=property, value=df.get(property),
property_type=docfield_properties[property], fieldname=df.fieldname)
elif (prop == "read_only" and cint(df.get("read_only"))==0
and frappe.db.get_value("DocField", {"parent": self.doc_type,
"fieldname": df.fieldname}, "read_only")==1):
# if docfield has read_only checked and user is trying to make it editable, don't allow it
frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label))
return False
elif prop == "options" and df.get("fieldtype") not in ALLOWED_OPTIONS_CHANGE:
frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label))
return False
elif prop == 'translatable' and not supports_translation(df.get('fieldtype')):
frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label))
return False
elif (prop == 'in_global_search' and
df.in_global_search != meta_df[0].get("in_global_search")):
self.flags.rebuild_doctype_for_global_search = True
return True
def set_property_setters_for_actions_and_links(self, meta):
'''
Apply property setters or create custom records for DocType Action and DocType Link
'''
for doctype, fieldname, field_map in (
('DocType Link', 'links', doctype_link_properties),
('DocType Action', 'actions', doctype_action_properties)
):
has_custom = False
items = []
for i, d in enumerate(self.get(fieldname) or []):
d.idx = i
if frappe.db.exists(doctype, d.name) and not d.custom:
# check property and apply property setter
original = frappe.get_doc(doctype, d.name)
for prop, prop_type in field_map.items():
if d.get(prop) != original.get(prop):
self.make_property_setter(prop, d.get(prop), prop_type,
apply_on=doctype, row_name=d.name)
items.append(d.name)
else:
# custom - just insert/update
d.parent = self.doc_type
d.custom = 1
d.save(ignore_permissions=True)
has_custom = True
items.append(d.name)
self.update_order_property_setter(has_custom, fieldname)
self.clear_removed_items(doctype, items)
def update_order_property_setter(self, has_custom, fieldname):
'''
We need to maintain the order of the link/actions if the user has shuffled them.
So we create a new property (ex `links_order`) to keep a list of items.
'''
property_name = '{}_order'.format(fieldname)
if has_custom:
# save the order of the actions and links
self.make_property_setter(property_name,
json.dumps([d.name for d in self.get(fieldname)]), 'Small Text')
else:
frappe.db.delete('Property Setter', dict(property=property_name,
doc_type=self.doc_type))
def clear_removed_items(self, doctype, items):
'''
Clear rows that do not appear in `items`. These have been removed by the user.
'''
if items:
frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1,
name=('not in', items)))
else:
frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1))
def update_custom_fields(self):
for i, df in enumerate(self.get("fields")):
@ -278,8 +305,8 @@ class CustomizeForm(Document):
d.dt = self.doc_type
for property in docfield_properties:
d.set(property, df.get(property))
for prop in docfield_properties:
d.set(prop, df.get(prop))
if i!=0:
d.insert_after = self.fields[i-1].fieldname
@ -297,12 +324,12 @@ class CustomizeForm(Document):
custom_field = frappe.get_doc("Custom Field", meta_df[0].name)
changed = False
for property in docfield_properties:
if df.get(property) != custom_field.get(property):
if property == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
for prop in docfield_properties:
if df.get(prop) != custom_field.get(prop):
if prop == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop))
custom_field.set(property, df.get(property))
custom_field.set(prop, df.get(prop))
changed = True
# check and update `insert_after` property
@ -328,32 +355,28 @@ class CustomizeForm(Document):
if df.get("is_custom_field"):
frappe.delete_doc("Custom Field", df.name)
def make_property_setter(self, property, value, property_type, fieldname=None):
self.delete_existing_property_setter(property, fieldname)
def make_property_setter(self, prop, value, property_type, fieldname=None,
apply_on=None, row_name = None):
delete_property_setter(self.doc_type, prop, fieldname)
property_value = self.get_existing_property_value(property, fieldname)
property_value = self.get_existing_property_value(prop, fieldname)
if property_value==value:
return
if not apply_on:
apply_on = "DocField" if fieldname else "DocType"
# create a new property setter
# ignore validation becuase it will be done at end
frappe.make_property_setter({
"doctype": self.doc_type,
"doctype_or_field": "DocField" if fieldname else "DocType",
"doctype_or_field": apply_on,
"fieldname": fieldname,
"property": property,
"row_name": row_name,
"property": prop,
"value": value,
"property_type": property_type
}, ignore_validate=True)
def delete_existing_property_setter(self, property, fieldname=None):
# first delete existing property setter
existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.doc_type,
"property": property, "field_name['']": fieldname or ''})
if existing_property_setter:
frappe.db.sql("delete from `tabProperty Setter` where name=%s", existing_property_setter)
})
def get_existing_property_value(self, property_name, fieldname=None):
# check if there is any need to make property setter!
@ -361,20 +384,17 @@ class CustomizeForm(Document):
property_value = frappe.db.get_value("DocField", {"parent": self.doc_type,
"fieldname": fieldname}, property_name)
else:
try:
if frappe.db.has_column("DocType", property_name):
property_value = frappe.db.get_value("DocType", self.doc_type, property_name)
except Exception as e:
if frappe.db.is_column_missing(e):
property_value = None
else:
raise
else:
property_value = None
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
allowed = False
self.check_length_for_fieldtypes = []
for allowed_changes in allowed_fieldtype_change:
for allowed_changes in ALLOWED_FIELDTYPE_CHANGE:
if (old_value in allowed_changes and new_value in allowed_changes):
allowed = True
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
@ -425,8 +445,109 @@ class CustomizeForm(Document):
if not self.doc_type:
return
frappe.db.sql("""DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
reset_customization(self.doc_type)
self.fetch_to_customize()
def reset_customization(doctype):
frappe.db.sql("""
DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'
""", doctype)
frappe.clear_cache(doctype=doctype)
doctype_properties = {
'search_fields': 'Data',
'title_field': 'Data',
'image_field': 'Data',
'sort_field': 'Data',
'sort_order': 'Data',
'default_print_format': 'Data',
'allow_copy': 'Check',
'istable': 'Check',
'quick_entry': 'Check',
'editable_grid': 'Check',
'max_attachments': 'Int',
'track_changes': 'Check',
'track_views': 'Check',
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data'
}
docfield_properties = {
'idx': 'Int',
'label': 'Data',
'fieldtype': 'Select',
'options': 'Text',
'fetch_from': 'Small Text',
'fetch_if_empty': 'Check',
'permlevel': 'Int',
'width': 'Data',
'print_width': 'Data',
'non_negative': 'Check',
'reqd': 'Check',
'unique': 'Check',
'ignore_user_permissions': 'Check',
'in_list_view': 'Check',
'in_standard_filter': 'Check',
'in_global_search': 'Check',
'in_preview': 'Check',
'bold': 'Check',
'hidden': 'Check',
'collapsible': 'Check',
'collapsible_depends_on': 'Data',
'print_hide': 'Check',
'print_hide_if_no_value': 'Check',
'report_hide': 'Check',
'allow_on_submit': 'Check',
'translatable': 'Check',
'mandatory_depends_on': 'Data',
'read_only_depends_on': 'Data',
'depends_on': 'Data',
'description': 'Text',
'default': 'Text',
'precision': 'Select',
'read_only': 'Check',
'length': 'Int',
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check',
'hide_border': 'Check',
'hide_days': 'Check',
'hide_seconds': 'Check'
}
doctype_link_properties = {
'link_doctype': 'Link',
'link_fieldname': 'Data',
'group': 'Data',
'hidden': 'Check'
}
doctype_action_properties = {
'label': 'Link',
'action_type': 'Select',
'action': 'Small Text',
'group': 'Data',
'hidden': 'Check'
}
ALLOWED_FIELDTYPE_CHANGE = (
('Currency', 'Float', 'Percent'),
('Small Text', 'Data'),
('Text', 'Data'),
('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'),
('Data', 'Select'),
('Text', 'Small Text'),
('Text', 'Data', 'Barcode'),
('Code', 'Geolocation'),
('Table', 'Table MultiSelect'))
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data')

View file

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe, unittest, json
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.doctype.doctype import InvalidFieldNameError
from frappe.core.doctype.doctype.test_doctype import new_doctype
test_dependencies = ["Custom Field", "Property Setter"]
class TestCustomizeForm(unittest.TestCase):
@ -24,6 +25,7 @@ class TestCustomizeForm(unittest.TestCase):
def setUp(self):
self.insert_custom_field()
frappe.db.delete('Property Setter', dict(doc_type='Event'))
frappe.db.commit()
frappe.clear_cache(doctype="Event")
@ -185,9 +187,75 @@ class TestCustomizeForm(unittest.TestCase):
d.run_method("save_customization")
def test_core_doctype_customization(self):
d = self.get_customize_form('User')
e = self.get_customize_form('Custom Field')
self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User')
# core doctype is invalid, hence no attributes are set
self.assertEquals(d.get("fields"), [])
self.assertEquals(e.get("fields"), [])
def test_custom_link(self):
try:
# create a dummy doctype linked to Event
testdt_name = 'Test Link for Event'
testdt = new_doctype(testdt_name, fields=[
dict(fieldtype='Link', fieldname='event', options='Event')
]).insert()
testdt_name1 = 'Test Link for Event 1'
testdt1 = new_doctype(testdt_name1, fields=[
dict(fieldtype='Link', fieldname='event', options='Event')
]).insert()
# add a custom link
d = self.get_customize_form("Event")
d.append('links', dict(link_doctype=testdt_name, link_fieldname='event', group='Tests'))
d.append('links', dict(link_doctype=testdt_name1, link_fieldname='event', group='Tests'))
d.run_method("save_customization")
frappe.clear_cache()
event = frappe.get_meta('Event')
# check links exist
self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name])
self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name1])
# check order
order = json.loads(event.links_order)
self.assertListEqual(order, [d.name for d in event.links])
# remove the link
d = self.get_customize_form("Event")
d.links = []
d.run_method("save_customization")
frappe.clear_cache()
event = frappe.get_meta('Event')
self.assertFalse([d.name for d in (event.links or []) if d.link_doctype == testdt_name])
finally:
testdt.delete()
testdt1.delete()
def test_custom_action(self):
test_route = '#List/DocType'
# create a dummy action (route)
d = self.get_customize_form("Event")
d.append('actions', dict(label='Test Action', action_type='Route', action=test_route))
d.run_method("save_customization")
frappe.clear_cache()
event = frappe.get_meta('Event')
# check if added to meta
action = [d for d in event.actions if d.label=='Test Action']
self.assertEqual(len(action), 1)
self.assertEqual(action[0].action, test_route)
# clear the action
d = self.get_customize_form("Event")
d.actions = []
d.run_method("save_customization")
frappe.clear_cache()
event = frappe.get_meta('Event')
action = [d for d in event.actions if d.label=='Test Action']
self.assertEqual(len(action), 0)

View file

@ -11,8 +11,7 @@
"label",
"fieldtype",
"fieldname",
"hide_seconds",
"hide_days",
"non_negative",
"reqd",
"unique",
"in_list_view",
@ -23,6 +22,7 @@
"allow_in_quick_entry",
"translatable",
"column_break_7",
"default",
"precision",
"length",
"options",
@ -47,8 +47,9 @@
"column_break_33",
"read_only_depends_on",
"display",
"default",
"in_filter",
"hide_seconds",
"hide_days",
"column_break_21",
"description",
"print_hide",
@ -100,6 +101,7 @@
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
@ -283,7 +285,7 @@
},
{
"fieldname": "default",
"fieldtype": "Text",
"fieldtype": "Small Text",
"label": "Default",
"oldfieldname": "default",
"oldfieldtype": "Text"
@ -413,13 +415,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-28 11:28:59.084060",
"modified": "2020-10-29 06:11:57.661039",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -1,358 +1,133 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-01-10 16:34:04",
"custom": 0,
"description": "Property Setter overrides a standard DocType or Field property",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"creation": "2013-01-10 16:34:04",
"description": "Property Setter overrides a standard DocType or Field property",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"help",
"sb0",
"doctype_or_field",
"doc_type",
"field_name",
"row_name",
"column_break0",
"property",
"property_type",
"value",
"default_value"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "help",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Help",
"length": 0,
"no_copy": 0,
"options": "<div class=\"alert\">Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!</div>",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "help",
"fieldtype": "HTML",
"label": "Help",
"options": "<div class=\"alert\">Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!</div>"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sb0",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "sb0",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.__islocal",
"fieldname": "doctype_or_field",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "DocType or Field",
"length": 0,
"no_copy": 0,
"options": "\nDocField\nDocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "doctype_or_field",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applied On",
"options": "\nDocField\nDocType\nDocType Link\nDocType Action",
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "New value to be set",
"fieldname": "value",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Set Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "New value to be set",
"fieldname": "value",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Set Value"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break0",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break0",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "doc_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"fieldname": "doc_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.doctype_or_field=='DocField'",
"description": "ID (name) of the entity whose property is to be set",
"fieldname": "field_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Field Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"depends_on": "eval:doc.doctype_or_field=='DocField'",
"description": "ID (name) of the entity whose property is to be set",
"fieldname": "field_name",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Field Name",
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "property",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Property",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
"fieldname": "property",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Property",
"reqd": 1,
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "property_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Property Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "property_type",
"fieldtype": "Data",
"label": "Property Type"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "default_value",
"fieldtype": "Data",
"label": "Default Value"
},
{
"description": "For DocType Link / DocType Action",
"fieldname": "row_name",
"fieldtype": "Data",
"label": "Row Name"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-glass",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:39:50.172883",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
"owner": "Administrator",
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-24 14:42:38.599684",
"modified_by": "Administrator",
"module": "Custom",
"name": "Property Setter",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "doc_type,property",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"search_fields": "doc_type,property",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -11,13 +11,16 @@ not_allowed_fieldtype_change = ['naming_series']
class PropertySetter(Document):
def autoname(self):
self.name = self.doc_type + "-" \
+ (self.field_name and (self.field_name + "-") or "") \
+ self.property
self.name = '{doctype}-{field}-{property}'.format(
doctype = self.doc_type,
field = self.field_name or self.row_name or 'main',
property = self.property
)
def validate(self):
self.validate_fieldtype_change()
self.delete_property_setter()
if self.is_new():
delete_property_setter(self.doc_type, self.property, self.field_name)
# clear cache
frappe.clear_cache(doctype = self.doc_type)
@ -27,15 +30,6 @@ class PropertySetter(Document):
self.property == 'fieldtype':
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))
def delete_property_setter(self):
"""delete other property setters on this, if this is new"""
if self.get('__islocal'):
frappe.db.sql("""delete from `tabProperty Setter` where
doctype_or_field = %(doctype_or_field)s
and doc_type = %(doc_type)s
and coalesce(field_name,'') = coalesce(%(field_name)s, '')
and property = %(property)s""", self.get_valid_dict())
def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
@ -89,3 +83,12 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
property_setter.flags.validate_fields_for_doctype = validate_fields_for_doctype
property_setter.insert()
return property_setter
def delete_property_setter(doc_type, property, field_name=None):
"""delete other property setters on this, if this is new"""
filters = dict(doc_type = doc_type, property=property)
if field_name:
filters['field_name'] = field_name
frappe.db.delete('Property Setter', filters)

View file

@ -319,8 +319,7 @@ class Database(object):
nres.append(nr)
return nres
@staticmethod
def build_conditions(filters):
def build_conditions(self, filters):
"""Convert filters sent as dict, lists to SQL conditions. filter's key
is passed by map function, build conditions like:
@ -341,18 +340,12 @@ class Database(object):
value = filters.get(key)
values[key] = value
if isinstance(value, (list, tuple)):
# value is a tuble like ("!=", 0)
# value is a tuple like ("!=", 0)
_operator = value[0]
values[key] = value[1]
if isinstance(value[1], (tuple, list)):
# value is a list in tuple ("in", ("A", "B"))
inner_list = []
for i, v in enumerate(value[1]):
inner_key = "{0}_{1}".format(key, i)
values[inner_key] = v
inner_list.append("%({0})s".format(inner_key))
_rhs = " ({0})".format(", ".join(inner_list))
_rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]]))
del values[key]
if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
@ -787,6 +780,9 @@ class Database(object):
"""Returns True if table for given doctype exists."""
return ("tab" + doctype) in self.get_tables()
def has_table(self, doctype):
return self.table_exists(doctype)
def get_tables(self):
tables = frappe.cache().get_value('db_tables')
if not tables:
@ -959,13 +955,13 @@ class Database(object):
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
def delete(self, doctype, conditions):
def delete(self, doctype, conditions, debug=False):
if conditions:
conditions, values = self.build_conditions(conditions)
return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format(
doctype=doctype,
conditions=conditions
), values)
), values, debug=debug)
else:
frappe.throw(_('No conditions provided'))

View file

@ -140,11 +140,11 @@ class PostgresDatabase(Database):
@staticmethod
def is_table_missing(e):
return e.pgcode == '42P01'
return getattr(e, 'pgcode', None) == '42P01'
@staticmethod
def is_missing_column(e):
return e.pgcode == '42703'
return getattr(e, 'pgcode', None) == '42703'
@staticmethod
def is_access_denied(e):

View file

@ -186,7 +186,7 @@ class DbColumn:
column_def += ' not null default {0}'.format(default_value)
elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \
and not self.default.startswith(":") and column_def not in ('text', 'longtext'):
and not cstr(self.default).startswith(":") and column_def not in ('text', 'longtext'):
column_def += " default {}".format(frappe.db.escape(self.default))
if self.unique and (column_def not in ('text', 'longtext')):

View file

@ -378,7 +378,7 @@ def get_desk_sidebar_items(flatten=False, cache=True):
# pages sorted based on pinned to top and then by name
order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
all_pages = frappe.get_all("Desk Page", fields=["name", "category", "icon"], filters=filters, order_by=order_by, ignore_permissions=True)
all_pages = frappe.get_all("Desk Page", fields=["name", "category", "icon", "module"]], filters=filters, order_by=order_by, ignore_permissions=True)
pages = []
# Filter Page based on Permission

View file

@ -21,8 +21,10 @@ frappe.ui.form.on('Dashboard Chart', {
refresh: function(frm) {
frm.chart_filters = null;
frm.is_disabled = !frappe.boot.developer_mode && frm.doc.is_standard;
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
if (frm.is_disabled) {
!frm.doc.custom_options && frm.set_df_property('chart_options_section', 'hidden', 1);
frm.disable_form();
}
@ -333,6 +335,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
table.on('click', () => {
frm.is_disabled && frappe.throw(__('Cannot edit filters for standard charts'));
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),

View file

@ -61,7 +61,7 @@ def make_notification_logs(doc, users):
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
for user in users:
if frappe.db.exists('User', user):
if frappe.db.exists('User', {"name": user, "enabled": 1}):
if is_notifications_enabled(user):
if doc.type == 'Energy Point' and not is_energy_point_enabled():
return

View file

@ -2,12 +2,19 @@
// For license information, please see license.txt
frappe.ui.form.on('Notification Settings', {
onload: () => {
onload: (frm) => {
frappe.breadcrumbs.add({
label: __('Settings'),
route: '#modules/Settings',
type: 'Custom'
});
frm.set_query('subscribed_documents', () => {
return {
filters: {
istable: 0
}
};
});
},
refresh: (frm) => {

View file

@ -22,68 +22,52 @@
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled",
"show_days": 1,
"show_seconds": 1
"label": "Enabled"
},
{
"fieldname": "subscribed_documents",
"fieldtype": "Table MultiSelect",
"label": "Subscribed Documents",
"options": "Notification Subscribed Document",
"show_days": 1,
"show_seconds": 1
"label": "Open Documents",
"options": "Notification Subscribed Document"
},
{
"fieldname": "column_break_3",
"fieldtype": "Section Break",
"label": "Email Settings",
"show_days": 1,
"show_seconds": 1
"label": "Email Settings"
},
{
"default": "1",
"fieldname": "enable_email_notifications",
"fieldtype": "Check",
"label": "Enable Email Notifications",
"show_days": 1,
"show_seconds": 1
"label": "Enable Email Notifications"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_mention",
"fieldtype": "Check",
"label": "Mentions",
"show_days": 1,
"show_seconds": 1
"label": "Mentions"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_assignment",
"fieldtype": "Check",
"label": "Assignments",
"show_days": 1,
"show_seconds": 1
"label": "Assignments"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_energy_point",
"fieldtype": "Check",
"label": "Energy Points",
"show_days": 1,
"show_seconds": 1
"label": "Energy Points"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_share",
"fieldtype": "Check",
"label": "Document Share",
"show_days": 1,
"show_seconds": 1
"label": "Document Share"
},
{
"default": "__user",
@ -92,23 +76,20 @@
"hidden": 1,
"label": "User",
"options": "User",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"label": "Seen",
"show_days": 1,
"show_seconds": 1
"label": "Seen"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-05-31 22:16:40.798019",
"modified": "2020-11-04 12:54:57.989317",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",

View file

@ -168,8 +168,8 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
"""
if not (assigned_by and owner and doc_type and doc_name): return
# self assignment / closing - no message
if assigned_by==owner:
# return if self assigned or user disabled
if assigned_by == owner or not frappe.db.get_value('User', owner, 'enabled'):
return
# Search for email address in description -- i.e. assignee
@ -177,7 +177,7 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
title = get_title(doc_type, doc_name)
description_html = "<div>{0}</div>".format(description) if description else None
if action=='CLOSE':
if action == 'CLOSE':
subject = _('Your assignment on {0} {1} has been removed by {2}')\
.format(frappe.bold(doc_type), get_title_html(title), frappe.bold(user_name))
else:

View file

@ -1,17 +1,23 @@
import frappe
from datetime import datetime
from frappe.utils import getdate
@frappe.whitelist()
def get_energy_points_heatmap_data(user, date):
try:
date = getdate(date)
except Exception:
date = getdate()
return dict(frappe.db.sql("""select unix_timestamp(date(creation)), sum(points)
from `tabEnergy Point Log`
where
date(creation) > subdate('{date}', interval 1 year) and
date(creation) < subdate('{date}', interval -1 year) and
user = '{user}' and
user = %s and
type != 'Review'
group by date(creation)
order by creation asc""".format(user = user, date = date)))
order by creation asc""".format(date = date), user))
@frappe.whitelist()

View file

@ -12,6 +12,7 @@ from frappe.modules import scrub, get_module_path
from frappe.utils import (
flt,
cint,
cstr,
get_html_format,
get_url_to_form,
gzip_decompress,
@ -74,23 +75,27 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
res = report.execute_script_report(filters)
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
columns = [get_column_as_dict(col) for col in columns]
report_column_names = [col["fieldname"] for col in columns]
# convert to list of dicts
result = normalize_result(result, columns)
if report.custom_columns:
# Original query columns, needed to reorder data as per custom columns
query_columns = columns
# Reordered columns
# saved columns (with custom columns / with different column order)
columns = json.loads(report.custom_columns)
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
# unsaved custom_columns
if custom_columns:
result = add_data_to_custom_columns(custom_columns, result)
for custom_column in custom_columns:
columns.insert(custom_column["insert_after_index"] + 1, custom_column)
# all columns which are not in original report
report_custom_columns = [column for column in columns if column["fieldname"] not in report_column_names]
if report_custom_columns:
result = add_custom_column_data(report_custom_columns, result)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
@ -109,6 +114,20 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
or 0,
}
def normalize_result(result, columns):
# Converts to list of dicts from list of lists/tuples
data = []
column_names = [column["fieldname"] for column in columns]
if result and isinstance(result[0], (list, tuple)):
for row in result:
row_obj = {}
for idx, column_name in enumerate(column_names):
row_obj[column_name] = row[idx]
data.append(row_obj)
else:
data = result
return data
@frappe.whitelist()
def background_enqueue_run(report_name, filters=None, user=None):
@ -177,14 +196,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
def run(
report_name,
filters=None,
user=None,
ignore_prepared_report=False,
custom_columns=None,
):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
report = get_report_doc(report_name)
if not user:
user = frappe.session.user
@ -221,69 +233,20 @@ def run(
return result
def add_data_to_custom_columns(columns, result):
custom_fields_data = get_data_for_custom_report(columns)
def add_custom_column_data(custom_columns, result):
custom_column_data = get_data_for_custom_report(custom_columns)
data = []
for row in result:
row_obj = {}
if isinstance(row, tuple):
row = list(row)
for column in custom_columns:
key = (column.get('doctype'), column.get('fieldname'))
if key in custom_column_data:
for row in result:
row_reference = row.get(column.get('link_field'))
# possible if the row is empty
if not row_reference:
continue
row[column.get('fieldname')] = custom_column_data.get(key).get(row_reference)
if isinstance(row, list):
for idx, column in enumerate(columns):
if column.get("link_field"):
row_obj[column["fieldname"]] = None
row.insert(idx, None)
else:
row_obj[column["fieldname"]] = row[idx]
data.append(row_obj)
else:
data.append(row)
for row in data:
for column in columns:
if column.get("link_field"):
fieldname = column["fieldname"]
key = (column["doctype"], fieldname)
link_field = column["link_field"]
row[fieldname] = custom_fields_data.get(key, {}).get(
row.get(link_field)
)
return data
def reorder_data_for_custom_columns(custom_columns, columns, result):
if not result:
return []
columns = [get_column_as_dict(col) for col in columns]
if isinstance(result[0], list) or isinstance(result[0], tuple):
# If the result is a list of lists
custom_column_names = [col["label"] for col in custom_columns]
original_column_names = [col["label"] for col in columns]
return get_columns_from_list(custom_column_names, original_column_names, result)
else:
# columns do not need to be reordered if result is a list of dicts
return result
def get_columns_from_list(columns, target_columns, result):
reordered_result = []
for res in result:
r = []
for col_name in columns:
try:
idx = target_columns.index(col_name)
r.append(res[idx])
except ValueError:
pass
reordered_result.append(r)
return reordered_result
return result
def get_prepared_report_result(report, filters, dn="", user=None):
@ -343,31 +306,27 @@ def get_prepared_report_result(report, filters, dn="", user=None):
@frappe.whitelist()
def export_query():
"""export from query reports"""
data = frappe._dict(frappe.local.form_dict)
del data["cmd"]
if "csrf_token" in data:
del data["csrf_token"]
data.pop("cmd", None)
data.pop("csrf_token", None)
if isinstance(data.get("filters"), string_types):
filters = json.loads(data["filters"])
if isinstance(data.get("report_name"), string_types):
if data.get("report_name"):
report_name = data["report_name"]
frappe.permissions.can_export(
frappe.get_cached_value("Report", report_name, "ref_doctype"),
raise_exception=True,
)
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
custom_columns = frappe.parse_json(data["custom_columns"])
file_format_type = data.get("file_format_type")
custom_columns = frappe.parse_json(data.get("custom_columns", "[]"))
include_indentation = data.get("include_indentation")
visible_idx = data.get("visible_idx")
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
else:
visible_idx = None
if isinstance(visible_idx, string_types):
visible_idx = json.loads(visible_idx)
if file_format_type == "Excel":
data = run(report_name, filters, custom_columns=custom_columns)
@ -386,8 +345,8 @@ def export_query():
data["result"] = handle_duration_fieldtype_values(
data.get("result"), data.get("columns")
)
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
frappe.response["filename"] = report_name + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
@ -421,34 +380,38 @@ def handle_duration_fieldtype_values(result, columns):
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
column_widths = []
# add column headings
for idx in range(len(data.columns)):
if not columns[idx].get("hidden"):
result[0].append(columns[idx]["label"])
for column in data.columns:
if column.get("hidden"):
continue
result[0].append(column["label"])
column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl
column_width /= 10
column_widths.append(column_width)
# build table from result
for i, row in enumerate(data.result):
for row_idx, row in enumerate(data.result):
# only pick up rows that are visible in the report
if i in visible_idx:
if row_idx in visible_idx:
row_data = []
if isinstance(row, dict) and row:
for idx in range(len(data.columns)):
# check if column is not hidden
if not columns[idx].get("hidden"):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and "indent" in row and idx == 0:
cell_value = (" " * cint(row["indent"])) + cell_value
row_data.append(cell_value)
else:
if isinstance(row, dict):
for col_idx, column in enumerate(data.columns):
if column.get("hidden"):
continue
label = column.get("label")
fieldname = column.get("fieldname")
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and "indent" in row and col_idx == 0:
cell_value = (" " * cint(row["indent"])) + cstr(cell_value)
row_data.append(cell_value)
elif row:
row_data = row
result.append(row_data)
return result
return result, column_widths
def add_total_row(result, columns, meta=None):
@ -755,6 +718,8 @@ def get_column_as_dict(col):
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
if len(col) == 3:
col_dict["width"] = col[2]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])

View file

@ -141,7 +141,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=doctype))
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
# In order_by, `idx` gets second priority, because it stores link count

View file

@ -25,7 +25,11 @@ from frappe.core.doctype.communication.email import set_incoming_outgoing_accoun
from frappe.utils.html_utils import clean_email_html
from frappe.email.utils import get_port
class SentEmailInInbox(Exception): pass
class SentEmailInInbox(Exception):
pass
class InvalidEmailCredentials(frappe.ValidationError):
pass
class EmailAccount(Document):
def autoname(self):
@ -148,7 +152,7 @@ class EmailAccount(Document):
return None
args = frappe._dict({
"email_account":self.name,
"email_account": self.name,
"host": self.email_server,
"use_ssl": self.use_ssl,
"username": getattr(self, "login_id", None) or self.email_id,
@ -166,21 +170,45 @@ class EmailAccount(Document):
frappe.throw(_("{0} is required").format("Email Server"))
email_server = EmailServer(frappe._dict(args))
self.check_email_server_connection(email_server, in_receive)
if not in_receive and self.use_imap:
email_server.imap.logout()
# reset failed attempts count
self.set_failed_attempts_count(0)
return email_server
def check_email_server_connection(self, email_server, in_receive):
# tries to connect to email server and handles failure
try:
email_server.connect()
except (error_proto, imaplib.IMAP4.error) as e:
e = cstr(e)
message = e.lower().replace(" ","")
if in_receive and any(map(lambda t: t in message, ['authenticationfailed', 'loginviayourwebbrowser', #abbreviated to work with both failure and failed
'loginfailed', 'err[auth]', 'errtemporaryerror'])): #temporary error to deal with godaddy
# if called via self.receive and it leads to authentication error, disable incoming
# and send email to system manager
self.handle_incoming_connect_error(
description=_('Authentication failed while receiving emails from Email Account {0}. Message from server: {1}').format(self.name, e)
)
message = cstr(e).lower().replace(" ","")
auth_error_codes = [
'authenticationfailed',
'loginfailed',
]
other_error_codes = [
'err[auth]',
'errtemporaryerror',
'loginviayourwebbrowser'
]
all_error_codes = auth_error_codes + other_error_codes
if in_receive and any(map(lambda t: t in message, all_error_codes)):
# if called via self.receive and it leads to authentication error,
# disable incoming and send email to System Manager
error_message = _("Authentication failed while receiving emails from Email Account: {0}.").format(self.name)
error_message += "<br>" + _("Message from server: {0}").format(cstr(e))
self.handle_incoming_connect_error(description=error_message)
return None
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
self.throw_invalid_credentials_exception()
else:
frappe.throw(e)
@ -195,16 +223,16 @@ class EmailAccount(Document):
else:
frappe.cache().set_value("workers:no-internet", True)
return None
else:
raise
if not in_receive:
if self.use_imap:
email_server.imap.logout()
# reset failed attempts count
self.set_failed_attempts_count(0)
return email_server
@classmethod
def throw_invalid_credentials_exception(cls):
frappe.throw(
_("Incorrect email or password. Please check your login credentials."),
exc=InvalidEmailCredentials,
title=_("Invalid Credentials")
)
def handle_incoming_connect_error(self, description):
if test_internet():

View file

@ -3,11 +3,6 @@
frappe.ui.form.on("Email Group", "refresh", function(frm) {
if(!frm.is_new()) {
frm.add_custom_button(__("View Subscribers"), function() {
frappe.route_options = {"email_group": frm.doc.name};
frappe.set_route("List", "Email Group Member");
}, __("View"));
frm.add_custom_button(__("Import Subscribers"), function() {
frappe.prompt({fieldtype:"Select", options: frm.doc.__onload.import_types,
label:__("Import Email From"), fieldname:"doctype", reqd:1},

View file

@ -5,6 +5,7 @@
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"title",
"total_subscribers",
@ -41,8 +42,15 @@
"options": "Email Template"
}
],
"links": [],
"modified": "2020-02-21 14:12:48.884738",
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "Email Group Member",
"link_fieldname": "email_group"
}
],
"modified": "2020-09-24 16:41:55.286377",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",

View file

@ -198,12 +198,15 @@ class EMail:
def set_message_id(self, message_id, is_notification=False):
if message_id:
self.msg_root["Message-Id"] = '<' + message_id + '>'
message_id = '<' + message_id + '>'
else:
self.msg_root["Message-Id"] = get_message_id()
self.msg_root["isnotification"] = '<notification>'
message_id = get_message_id()
self.set_header('isnotification', '<notification>')
if is_notification:
self.msg_root["isnotification"] = '<notification>'
self.set_header('isnotification', '<notification>')
self.set_header('Message-Id', message_id)
def set_in_reply_to(self, in_reply_to):
"""Used to send the Message-Id of a received email back as In-Reply-To"""

View file

@ -59,10 +59,6 @@ class EmailServer:
frappe.msgprint(_('Invalid Mail Server. Please rectify and try again.'))
raise
except Exception as e:
frappe.msgprint(_('Cannot connect: {0}').format(str(e)))
raise
def connect_pop(self):
#this method return pop connection
try:
@ -540,6 +536,8 @@ class Email:
except MaxFileSizeReachedError:
# WARNING: bypass max file size exception
pass
except frappe.FileAlreadyAttachedException:
pass
except frappe.DuplicateEntryError:
# same file attached twice??
pass

View file

@ -2,7 +2,6 @@
# MIT License. See license.txt
from __future__ import unicode_literals
from six import reraise as raise_
import frappe
import smtplib
import email.utils
@ -242,16 +241,17 @@ class SMTPServer:
return self._sess
except smtplib.SMTPAuthenticationError as e:
from frappe.email.doctype.email_account.email_account import EmailAccount
EmailAccount.throw_invalid_credentials_exception()
except _socket.error as e:
# Invalid mail server -- due to refusing connection
frappe.msgprint(_('Invalid Outgoing Mail Server or Port'))
traceback = sys.exc_info()[2]
raise_(frappe.ValidationError, e, traceback)
except smtplib.SMTPAuthenticationError as e:
frappe.msgprint(_("Invalid login or password"))
traceback = sys.exc_info()[2]
raise_(frappe.ValidationError, e, traceback)
frappe.throw(
_("Invalid Outgoing Mail Server or Port"),
exc=frappe.ValidationError,
title=_("Incorrect Configuration")
)
except smtplib.SMTPException:
frappe.msgprint(_('Unable to send emails at this time'))

View file

@ -13,7 +13,6 @@
"api_secret",
"column_break_6",
"user",
"last_update",
"incoming_change"
],
"fields": [
@ -25,12 +24,6 @@
"reqd": 1,
"unique": 1
},
{
"fieldname": "last_update",
"fieldtype": "Data",
"label": "Last Update",
"read_only": 1
},
{
"description": "API Key of the user(Event Subscriber) on the producer site",
"fieldname": "api_key",
@ -77,7 +70,7 @@
}
],
"links": [],
"modified": "2020-09-08 18:50:57.687979",
"modified": "2020-10-26 13:00:15.361316",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Producer",

View file

@ -79,10 +79,24 @@ class EventProducer(Document):
)
if response:
response = json.loads(response)
self.last_update = response['last_update']
self.set_last_update(response['last_update'])
else:
frappe.throw(_('Failed to create an Event Consumer or an Event Consumer for the current site is already registered.'))
def set_last_update(self, last_update):
last_update_doc_name = frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name))
if not last_update_doc_name:
frappe.get_doc(dict(
doctype = 'Event Producer Last Update',
event_producer = self.producer_url,
last_update = last_update
)).insert(ignore_permissions=True)
else:
frappe.db.set_value('Event Producer Last Update', last_update_doc_name, 'last_update', last_update)
def get_last_update(self):
return frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name), 'last_update')
def get_request_data(self):
consumer_doctypes = []
for entry in self.producer_doctypes:
@ -184,7 +198,7 @@ def pull_from_node(event_producer):
"""pull all updates after the last update timestamp from event producer site"""
event_producer = frappe.get_doc('Event Producer', event_producer)
producer_site = get_producer_site(event_producer.producer_url)
last_update = event_producer.last_update
last_update = event_producer.get_last_update()
(doctypes, mapping_config, naming_config) = get_config(event_producer.producer_doctypes)
@ -239,7 +253,7 @@ def sync(update, producer_site, event_producer, in_retry=False):
return 'Failed'
log_event_sync(update, event_producer.name, 'Failed', frappe.get_traceback())
event_producer.db_set('last_update', update.creation)
event_producer.set_last_update(update.creation)
frappe.db.commit()

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Event Producer Last Update', {
// refresh: function(frm) {
// }
});

View file

@ -1,36 +1,36 @@
{
"actions": [],
"autoname": "field:document_type",
"creation": "2020-04-08 15:16:44.342509",
"autoname": "field:event_producer",
"creation": "2020-10-26 12:53:11.940177",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"links"
"event_producer",
"last_update"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"fieldname": "event_producer",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"label": "Event Producer",
"reqd": 1,
"unique": 1
},
{
"fieldname": "links",
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
"fieldname": "last_update",
"fieldtype": "Data",
"label": "Last Update"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-04-08 16:42:59.402671",
"modified": "2020-10-26 13:22:27.056599",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Link",
"module": "Event Streaming",
"name": "Event Producer Last Update",
"owner": "Administrator",
"permissions": [
{
@ -46,6 +46,7 @@
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CustomLink(Document):
class EventProducerLastUpdate(Document):
pass

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestCustomLink(unittest.TestCase):
class TestEventProducerLastUpdate(unittest.TestCase):
pass

View file

@ -76,6 +76,7 @@ class UnknownDomainError(Exception): pass
class MappingMismatchError(ValidationError): pass
class InvalidStatusError(ValidationError): pass
class MandatoryError(ValidationError): pass
class NonNegativeError(ValidationError): pass
class InvalidSignatureError(ValidationError): pass
class RateLimitExceededError(ValidationError): pass
class CannotChangeConstantError(ValidationError): pass

View file

@ -1,345 +1,113 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:currency_name",
"beta": 0,
"creation": "2013-01-28 10:06:02",
"custom": 0,
"description": "**Currency** Master",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"currency_name",
"enabled",
"fraction",
"fraction_units",
"smallest_currency_fraction_value",
"symbol",
"number_format"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "currency_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Currency Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "currency_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Enabled"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Sub-currency. For e.g. \"Cent\"",
"fieldname": "fraction",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Fraction"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent",
"fieldname": "fraction_units",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction Units",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Fraction Units"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01",
"fieldname": "smallest_currency_fraction_value",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Smallest Currency Fraction Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"non_negative": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "A symbol for this currency. For e.g. $",
"fieldname": "symbol",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Symbol",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Symbol"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "How should this currency be formatted? If not set, will use system defaults",
"fieldname": "number_format",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Number Format",
"length": 0,
"no_copy": 0,
"options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bitcoin",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-29 06:37:19.908254",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-29 06:33:12.879978",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"role": "Accounts User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"role": "Sales User"
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"role": "Purchase User"
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View file

@ -138,7 +138,6 @@ doc_events = {
"frappe.core.doctype.activity_log.feed.update_feed",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone",
"frappe.core.doctype.file.file.attach_files_to_document",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers",
"frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
@ -154,7 +153,8 @@ doc_events = {
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
]
},
"Event": {

View file

@ -175,7 +175,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
for doctype in set(drop_doctypes):
print("* dropping Table for '{0}'...".format(doctype))
frappe.db.sql("drop table `tab{0}`".format(doctype))
frappe.db.sql_ddl("drop table `tab{0}`".format(doctype))
frappe.db.commit()
click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")

View file

@ -10,7 +10,7 @@ import copy
import frappe
import frappe.defaults
from frappe.model import data_fieldtypes
from frappe.utils import nowdate, nowtime, now_datetime
from frappe.utils import nowdate, nowtime, now_datetime, cstr
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
from frappe.permissions import filter_allowed_docs_for_doctype
@ -99,7 +99,7 @@ def get_static_default_value(df, doctype_user_permissions, allowed_records):
elif df.default == "Today":
return nowdate()
elif not df.default.startswith(":"):
elif not cstr(df.default).startswith(":"):
# a simple default value
is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions)
or (df.default in allowed_records))
@ -116,7 +116,7 @@ def set_dynamic_default_values(doc, parent_doc, parentfield):
for df in frappe.get_meta(doc["doctype"]).get("fields"):
if df.get("default"):
if df.default.startswith(":"):
if cstr(df.default).startswith(":"):
default_value = get_default_based_on_another_field(df, user_permissions, parent_doc)
if default_value is not None and not doc.get(df.fieldname):
doc[df.fieldname] = default_value

View file

@ -38,7 +38,7 @@ class DatabaseQuery(object):
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
return_query=False, strict=True, pluck=None):
return_query=False, strict=True, pluck=None, ignore_ddl=False):
if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@ -86,6 +86,7 @@ class DatabaseQuery(object):
self.user_settings_fields = copy.deepcopy(self.fields)
self.return_query = return_query
self.strict = strict
self.ignore_ddl = ignore_ddl
# for contextual user permission check
# to determine which user permission is applicable on link field of specific doctype
@ -94,6 +95,11 @@ class DatabaseQuery(object):
if user_settings:
self.user_settings = json.loads(user_settings)
self.columns = self.get_table_columns()
# no table & ignore_ddl, return
if not self.columns: return []
if query:
result = self.run_custom_query(query)
else:
@ -134,7 +140,8 @@ class DatabaseQuery(object):
if self.return_query:
return query
else:
return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, update=self.update)
return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug,
update=self.update, ignore_ddl=self.ignore_ddl)
def prepare_args(self):
self.parse_args()
@ -323,15 +330,22 @@ class DatabaseQuery(object):
if '.' not in field and not _in_standard_sql_methods(field):
self.fields[idx] = '{0}.{1}'.format(self.tables[0], field)
def get_table_columns(self):
try:
return get_table_columns(self.doctype)
except frappe.db.TableMissingError:
if self.ignore_ddl:
return None
else:
raise
def set_optional_columns(self):
"""Removes optional columns like `_user_tags`, `_comments` etc. if not in table"""
columns = get_table_columns(self.doctype)
# remove from fields
to_remove = []
for fld in self.fields:
for f in optional_fields:
if f in fld and not f in columns:
if f in fld and not f in self.columns:
to_remove.append(fld)
for fld in to_remove:
@ -344,7 +358,7 @@ class DatabaseQuery(object):
each = [each]
for element in each:
if element in optional_fields and element not in columns:
if element in optional_fields and element not in self.columns:
to_remove.append(each)
for each in to_remove:

View file

@ -493,6 +493,7 @@ class Document(BaseDocument):
self._validate_mandatory()
self._validate_data_fields()
self._validate_selects()
self._validate_non_negative()
self._validate_length()
self._extract_images_from_text_editor()
self._sanitize_content()
@ -503,6 +504,7 @@ class Document(BaseDocument):
for d in children:
d._validate_data_fields()
d._validate_selects()
d._validate_non_negative()
d._validate_length()
d._extract_images_from_text_editor()
d._sanitize_content()
@ -514,6 +516,21 @@ class Document(BaseDocument):
else:
self.validate_set_only_once()
def _validate_non_negative(self):
def get_msg(df):
if self.parentfield:
return "{} {} #{}: {} {}".format(frappe.bold(_(self.doctype)),
_("Row"), self.idx, _("Value cannot be negative for"), frappe.bold(_(df.label)))
else:
return _("Value cannot be negative for {0}: {1}").format(_(df.parent), frappe.bold(_(df.label)))
for df in self.meta.get('fields', {'non_negative': ('=', 1),
'fieldtype': ('in', ['Int', 'Float', 'Currency'])}):
if flt(self.get(df.fieldname)) < 0:
msg = get_msg(df)
frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value"))
def validate_workflow(self):
"""Validate if the workflow transition is valid"""
if frappe.flags.in_install == 'frappe': return

View file

@ -42,9 +42,12 @@ def get_dynamic_link_map(for_delete=False):
# always check in Single DocTypes
dynamic_link_map.setdefault(meta.name, []).append(df)
else:
links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df))
for doctype in links:
dynamic_link_map.setdefault(doctype, []).append(df)
try:
links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df))
for doctype in links:
dynamic_link_map.setdefault(doctype, []).append(df)
except frappe.db.TableMissingError: # noqa: E722
pass
frappe.local.dynamic_link_map = dynamic_link_map
return frappe.local.dynamic_link_map

View file

@ -19,7 +19,7 @@ from __future__ import unicode_literals, print_function
from datetime import datetime
from six.moves import range
import frappe, json, os
from frappe.utils import cstr, cint
from frappe.utils import cstr, cint, cast_fieldtype
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields
from frappe.model.document import Document
from frappe.model.base_document import BaseDocument
@ -103,6 +103,7 @@ class Meta(Document):
self.sort_fields()
self.get_valid_columns()
self.set_custom_permissions()
self.add_custom_links_and_actions()
def as_dict(self, no_nulls = False):
def serialize(doc):
@ -305,6 +306,11 @@ class Meta(Document):
self.extend("fields", custom_fields)
def apply_property_setters(self):
"""
Property Setters are set via Customize Form. They override standard properties
of the doctype or its child properties like fields, links etc. This method
applies the customized properties over the standard meta object
"""
if not frappe.db.table_exists('Property Setter'):
return
@ -313,26 +319,52 @@ class Meta(Document):
if not property_setters: return
integer_docfield_properties = [d.fieldname for d in frappe.get_meta('DocField').fields
if d.fieldtype in ('Int', 'Check')]
for ps in property_setters:
if ps.doctype_or_field=='DocType':
if ps.property_type in ('Int', 'Check'):
ps.value = cint(ps.value)
self.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
self.set(ps.property, ps.value)
else:
docfield = self.get("fields", {"fieldname":ps.field_name}, limit=1)
if docfield:
docfield = docfield[0]
else:
continue
elif ps.doctype_or_field=='DocField':
for d in self.fields:
if d.fieldname == ps.field_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
break
if ps.property in integer_docfield_properties:
ps.value = cint(ps.value)
elif ps.doctype_or_field=='DocType Link':
for d in self.links:
if d.name == ps.row_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
break
docfield.set(ps.property, ps.value)
elif ps.doctype_or_field=='DocType Action':
for d in self.actions:
if d.name == ps.row_name:
d.set(ps.property, cast_fieldtype(ps.property_type, ps.value))
break
def add_custom_links_and_actions(self):
for doctype, fieldname in (('DocType Link', 'links'), ('DocType Action', 'actions')):
# ignore_ddl because the `custom` column was added later via a patch
for d in frappe.get_all(doctype, fields='*', filters=dict(parent=self.name, custom=1), ignore_ddl=True):
self.append(fieldname, d)
# set the fields in order if specified
# order is saved as `links_order`
order = json.loads(self.get('{}_order'.format(fieldname)) or '[]')
if order:
name_map = {d.name:d for d in self.get(fieldname)}
new_list = []
for name in order:
if name in name_map:
new_list.append(name_map[name])
# add the missing items that have not be added
# maybe these items were added to the standard product
# after the customization was done
for d in self.get(fieldname):
if d not in new_list:
new_list.append(d)
self.set(fieldname, new_list)
def sort_fields(self):
"""sort on basis of insert_after"""
@ -448,9 +480,6 @@ class Meta(Document):
if hasattr(self, 'links') and self.links:
dashboard_links.extend(self.links)
if frappe.get_all("Custom Link", {"document_type": self.name}):
dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links)
if not data.transactions:
# init groups
data.transactions = []
@ -458,6 +487,9 @@ class Meta(Document):
for link in dashboard_links:
link.added = False
if link.hidden:
continue
for group in data.transactions:
group = frappe._dict(group)
# group found

View file

@ -93,15 +93,12 @@ def set_naming_from_document_naming_rule(doc):
if doc.doctype in log_types:
return
try:
for d in frappe.get_all('Document Naming Rule',
dict(document_type=doc.doctype, disabled=0), order_by='priority desc'):
frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc)
if doc.name:
break
except frappe.db.TableMissingError: # noqa: E722
# not yet bootstrapped
pass
# ignore_ddl if naming is not yet bootstrapped
for d in frappe.get_all('Document Naming Rule',
dict(document_type=doc.doctype, disabled=0), order_by='priority desc', ignore_ddl=True):
frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc)
if doc.name:
break
def set_name_by_naming_series(doc):
"""Sets name by the `naming_series` property"""

View file

@ -7,7 +7,7 @@ frappe.patches.v7_0.update_auth
frappe.patches.v8_0.drop_in_dialog #2017-09-22
frappe.patches.v7_2.remove_in_filter
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20
frappe.patches.v11_0.drop_column_apply_user_permissions
@ -315,3 +315,6 @@ frappe.patches.v13_0.update_newsletter_content_type
execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'})
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
frappe.patches.v13_0.web_template_set_module #2020-10-05
frappe.patches.v13_0.remove_custom_link
execute:frappe.delete_doc("DocType", "Footer Item")
frappe.patches.v13_0.replace_field_target_with_open_in_new_tab

View file

@ -0,0 +1,15 @@
import frappe
def execute():
'''
Remove the doctype "Custom Link" that was used to add Custom Links to the
Dashboard since this is now managed by Customize Form.
Update `parent` property to the DocType and delte the doctype
'''
frappe.reload_doctype('DocType Link')
if frappe.db.has_table('Custom Link'):
for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']):
frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s',
(custom_link.document_type, custom_link.name))
frappe.delete_doc('DocType', 'Custom Link')

View file

@ -0,0 +1,11 @@
import frappe
def execute():
doctype = "Top Bar Item"
if not frappe.db.table_exists(doctype) \
or not frappe.db.has_column(doctype, "target"):
return
frappe.reload_doc("website", "doctype", "top_bar_item")
frappe.db.set_value(doctype, {"target": 'target = "_blank"'}, 'open_in_new_tab', 1)

View file

@ -8,10 +8,11 @@
"field_order": [
"doc_type",
"module",
"disabled",
"default_print_language",
"column_break_3",
"standard",
"custom_format",
"disabled",
"section_break_6",
"print_format_type",
"raw_printing",
@ -22,7 +23,6 @@
"show_section_headings",
"line_breaks",
"column_break_11",
"default_print_language",
"font",
"css_section",
"css",
@ -202,7 +202,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-22 14:58:03.261063",
"modified": "2020-10-27 18:27:58.307070",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",

View file

@ -10,6 +10,7 @@ body {
html,
body {
overflow-x: hidden;
overflow-y: overlay;
}
@media (max-width: 991px) {
.intro-area,

View file

@ -214,6 +214,7 @@ frappe.Application = Class.extend({
'reqd': 1
},
{
"fieldname": "submit",
"fieldtype": "Button",
"label": __("Submit")
}

View file

@ -263,7 +263,7 @@ frappe.ui.form.Form = class FrappeForm {
cur_frm = this;
if(this.docname) { // document to show
this.save_disabled = false;
// set the doc
this.doc = frappe.get_doc(this.doctype, this.docname);
@ -1270,17 +1270,17 @@ frappe.ui.form.Form = class FrappeForm {
set_df_property(fieldname, property, value, docname, table_field) {
var df;
if (!docname && !table_field) {
if (!docname || !table_field) {
df = this.get_docfield(fieldname);
} else {
var grid = this.fields_dict[table_field].grid,
fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': fieldname});
var grid = this.fields_dict[fieldname].grid,
fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field});
if (fname && fname.length)
df = frappe.meta.get_docfield(fname[0].parent, fieldname, docname);
df = frappe.meta.get_docfield(fname[0].parent, table_field, docname);
}
if (df && df[property] != value) {
df[property] = value;
refresh_field(fieldname, table_field);
this.refresh_field(fieldname);
}
}
@ -1690,6 +1690,21 @@ frappe.ui.form.Form = class FrappeForm {
this.timeline && this.timeline.refresh();
});
}
// Filters fields from the reference doctype and sets them as options for a Select field
set_fields_as_options(fieldname, reference_doctype, filter_function, default_options=[], table_fieldname) {
if (!reference_doctype) return;
let options = default_options;
return new Promise(resolve => {
frappe.model.with_doctype(reference_doctype, () => {
frappe.get_meta(reference_doctype).fields.map(df => {
filter_function(df) && options.push({ label: df.label, value: df.fieldname });
});
options && this.set_df_property(fieldname, 'options', options, this.doc.name, table_fieldname);
resolve(options);
});
});
}
};
frappe.validated = 0;

View file

@ -106,7 +106,7 @@ frappe.form.formatters = {
if(frappe.form.link_formatters[doctype]) {
// don't apply formatters in case of composite (parent field of same type)
if (doc && doctype !== doc.doctype) {
value = frappe.form.link_formatters[doctype](value, doc);
value = frappe.form.link_formatters[doctype](value, doc, docfield);
}
}
@ -305,7 +305,7 @@ frappe.format = function(value, df, options, doc) {
formatted = frappe.dom.remove_script_and_style(formatted);
return formatted;
}
};
frappe.get_format_helper = function(doc) {
var helper = {
@ -317,4 +317,9 @@ frappe.get_format_helper = function(doc) {
};
$.extend(helper, doc);
return helper;
}
};
frappe.form.link_formatters['User'] = function(value, doc, docfield) {
let full_name = doc && (doc.full_name || (docfield && doc[`${docfield.fieldname}_full_name`]));
return full_name || value;
};

View file

@ -53,6 +53,7 @@ export default class Grid {
let template = `
<label class="control-label">${__(this.df.label || '')}</label>
<p class="text-muted small grid-description"></p>
<div class="grid-custom-buttons grid-field"></div>
<div class="form-grid">
<div class="grid-heading-row"></div>
<div class="grid-body">
@ -117,6 +118,7 @@ export default class Grid {
this.custom_buttons = {};
this.grid_buttons = this.wrapper.find('.grid-buttons');
this.grid_custom_buttons = this.wrapper.find('.grid-custom-buttons');
this.remove_rows_button = this.grid_buttons.find('.grid-remove-rows');
this.remove_all_rows_button = this.grid_buttons.find('.grid-remove-all-rows');
@ -873,18 +875,19 @@ export default class Grid {
});
}
add_custom_button(label, click) {
add_custom_button(label, click, position='bottom') {
// add / unhide a custom button
var btn = this.custom_buttons[label];
if (!btn) {
btn = $('<button class="btn btn-secondary btn-xs btn-custom">' + label + '</button>')
.css('margin-right', '4px')
.prependTo(this.grid_buttons)
const $wrapper = position === 'top' ? this.grid_custom_buttons : this.grid_buttons;
let $btn = this.custom_buttons[label];
if (!$btn) {
$btn = $(`<button class="btn btn-default btn-xs btn-custom">${__(label)}</button>`)
.prependTo($wrapper)
.on('click', click);
this.custom_buttons[label] = btn;
this.custom_buttons[label] = $btn;
} else {
btn.removeClass('hidden');
$btn.removeClass('hidden');
}
return $btn;
}
clear_custom_buttons() {

View file

@ -491,6 +491,7 @@ frappe.ui.form.Layout = Class.extend({
},
set_dependant_property: function(condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;
if (this.frm) {
@ -499,10 +500,10 @@ frappe.ui.form.Layout = Class.extend({
form_obj = this;
}
if (form_obj) {
if (set_property) {
form_obj.set_df_property(fieldname, property, 1);
if (this.doc && this.doc.parent) {
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
} else {
form_obj.set_df_property(fieldname, property, 0);
form_obj.set_df_property(fieldname, property, value);
}
}
},

View file

@ -334,16 +334,12 @@ frappe.views.BaseList = class BaseList {
`<div class="list-paging-area level">
<div class="level-left">
<div class="btn-group">
${paging_values
.map(
(value) => `
${paging_values.map((value) => `
<button type="button" class="btn btn-default btn-sm btn-paging"
data-value="${value}">
${value}
</button>
`
)
.join("")}
`).join("")}
</div>
</div>
<div class="level-right">
@ -373,8 +369,8 @@ frappe.views.BaseList = class BaseList {
this.refresh();
} else if ($this.is(".btn-more")) {
this.start = this.start + this.page_length;
this.refresh();
}
this.refresh();
});
}

View file

@ -12,7 +12,6 @@ frappe.views.ListSidebar = class ListSidebar {
constructor(opts) {
$.extend(this, opts);
this.make();
this.cat_tags = [];
}
make() {
@ -95,32 +94,6 @@ frappe.views.ListSidebar = class ListSidebar {
this.sidebar.find(".list-stats-dropdown .stat-result").html(tag_list);
}
set_fieldtype(df) {
// scrub
if (df.fieldname == "docstatus") {
df.fieldtype = "Select",
df.options = [
{ value: 0, label: "Draft" },
{ value: 1, label: "Submitted" },
{ value: 2, label: "Cancelled" },
];
} else if (df.fieldtype == 'Check') {
df.fieldtype = 'Select';
df.options = [{ value: 0, label: 'No' },
{ value: 1, label: 'Yes' }
];
} else if (['Text', 'Small Text', 'Text Editor', 'Code', 'Tag', 'Comments',
'Dynamic Link', 'Read Only', 'Assign'
].indexOf(df.fieldtype) != -1) {
df.fieldtype = 'Data';
} else if (df.fieldtype == 'Link' && this.$w.find('.condition').val() != "=") {
df.fieldtype = 'Data';
}
if (df.fieldtype === "Data" && (df.options || "").toLowerCase() === "email") {
df.options = null;
}
}
reload_stats() {
this.sidebar.find(".stat-link").remove();
this.sidebar.find(".stat-no-records").remove();

View file

@ -98,6 +98,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
});
}
if (this.view_name == 'List') this.toggle_paging = true;
this.patch_refresh_and_load_lib();
return this.get_list_view_settings();
}
@ -526,6 +528,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
sort_by: this.sort_selector.sort_by,
sort_order: this.sort_selector.sort_order,
});
this.toggle_paging && this.$paging_area.toggle(false);
}
after_render() {
@ -536,6 +539,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
`);
this.setup_new_doc_event();
this.list_sidebar && this.list_sidebar.reload_stats();
this.toggle_paging && this.$paging_area.toggle(true);
}
render() {
@ -575,9 +579,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const subject_field = this.columns[0].df;
let subject_html = `
<input class="level-item list-check-all hidden-xs" type="checkbox" title="${__(
"Select All"
)}">
<input class="level-item list-check-all hidden-xs" type="checkbox"
title="${__("Select All")}">
<span class="level-item list-liked-by-me">
<span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span>
</span>
@ -594,12 +597,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return `
<div class="${classes}">
${
col.type === "Subject"
? subject_html
: `
<span>${__((col.df && col.df.label) || col.type)}</span>`
}
${col.type === "Subject" ? subject_html : `
<span>${__((col.df && col.df.label) || col.type)}</span>`}
</div>
`;
})
@ -619,9 +618,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
</div>
<div class="level-left checkbox-actions">
<div class="level list-subject">
<input class="level-item list-check-all hidden-xs" type="checkbox" title="${__(
"Select All"
)}">
<input class="level-item list-check-all hidden-xs" type="checkbox"
title="${__("Select All")}">
<span class="level-item list-header-meta"></span>
</div>
</div>
@ -728,10 +726,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
if (df.fieldtype === "Image") {
html = df.options
? `<img src="${
doc[df.options]
}" style="max-height: 30px; max-width: 100%;">`
html = df.options ? `<img src="${doc[df.options]}"
style="max-height: 30px; max-width: 100%;">`
: `<div class="missing-image small">
<span class="octicon octicon-circle-slash"></span>
</div>`;
@ -796,7 +792,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (tag) {
return `<div class="tag-pill ellipsis" title="${tag}">${tag}</div>`;
}
}
};
return user_tags.split(',').slice(1, 3).map(get_tag_html).join('');
}
@ -871,7 +867,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return this.settings.get_form_link(doc);
}
const docname = doc.name.match(/[%'"]/)
const docname = doc.name.match(/[%'"\s]/)
? encodeURIComponent(doc.name)
: doc.name;
@ -903,7 +899,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
get_subject_html(doc) {
let user = frappe.session.user;
let subject_field = this.columns[0].df;
let value = doc[subject_field.fieldname] || doc.name;
let subject = strip_html(value.toString());
@ -912,19 +907,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const seen = this.get_seen_class(doc);
let subject_html = `
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" data-name="${escape(
doc.name
)}">
<input class="level-item list-row-checkbox hidden-xs" type="checkbox"
data-name="${escape(doc.name)}">
<span class="level-item" style="margin-bottom: 1px;">
${this.get_like_html(doc)}
</span>
<span class="level-item ${seen} ellipsis" title="${escaped_subject}">
<a class="ellipsis" href="${this.get_form_link(
doc
)}" title="${escaped_subject}" data-doctype="${
this.doctype
}" data-name="${doc.name}">
${subject}
<a class="ellipsis"
href="${this.get_form_link(doc)}"
title="${escaped_subject}"
data-doctype="${this.doctype}"
data-name="${doc.name}">
${subject}
</a>
</span>
`;

View file

@ -82,6 +82,9 @@ frappe.render_template = function(name, data) {
if(data===undefined) {
data = {};
}
if (!template) {
frappe.throw(`Template <b>${name}</b> not found.`);
}
return frappe.render(template, data, name);
}
frappe.render_grid = function(opts) {
@ -160,4 +163,4 @@ frappe.render_pdf = function(html, opts = {}) {
}
};
xhr.send(formData);
}
}

View file

@ -444,7 +444,7 @@ frappe.request.report_error = function(xhr, request_opts) {
var communication_composer = new frappe.views.CommunicationComposer({
subject: 'Error Report [' + frappe.datetime.nowdate() + ']',
recipients: error_report_email,
message: frappe.utils.xss_sanitise(error_report_message),
message: error_report_message,
doc: {
doctype: "User",
name: frappe.session.user

View file

@ -118,6 +118,19 @@ frappe.msgprint = function(msg, title, is_minimizable) {
data.indicator = 'blue';
}
if (data.as_list) {
const list_rows = data.message.map(m => `<li>${m}</li>`).join('');
data.message = `<ul style="padding-left: 20px">${list_rows}</ul>`;
}
if (data.as_table) {
const rows = data.message.map(row => {
const cols = row.map(col => `<td>${col}</td>`).join('');
return `<tr>${cols}</tr>`;
}).join('');
data.message = `<table class="table table-bordered" style="margin: 0;">${rows}</table>`;
}
if(data.message instanceof Array) {
data.message.forEach(function(m) {
frappe.msgprint(m);

View file

@ -252,33 +252,38 @@ Object.assign(frappe.utils, {
</a></p>');
return content.html();
},
scroll_to: function(element, animate, additional_offset, element_to_be_scrolled) {
scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) {
element_to_be_scrolled = element_to_be_scrolled || $("html, body");
var y = 0;
if (element && typeof element==="number") {
y = element;
} else if (element) {
var header_offset = $(".navbar").height() + $(".page-head").height();
var y = $(element).offset().top - header_offset - cint(additional_offset);
let scroll_top = 0;
if (element) {
// If a number is passed, just subtract the offset,
// otherwise calculate scroll position from element
scroll_top = typeof element == "number"
? element - cint(additional_offset)
: this.get_scroll_position(element, additional_offset);
}
if (y < 0) {
y = 0;
if (scroll_top < 0) {
scroll_top = 0;
}
// already there
if (y == element_to_be_scrolled.scrollTop()) {
if (scroll_top == element_to_be_scrolled.scrollTop()) {
return;
}
if (animate !== false) {
element_to_be_scrolled.animate({ scrollTop: y });
if (animate) {
element_to_be_scrolled.animate({ scrollTop: scroll_top });
} else {
element_to_be_scrolled.scrollTop(y);
element_to_be_scrolled.scrollTop(scroll_top);
}
},
get_scroll_position: function(element, additional_offset) {
let header_offset = $(".navbar").height() + $(".page-head").height();
let scroll_top = $(element).offset().top - header_offset - cint(additional_offset);
return scroll_top;
},
filter_dict: function(dict, filters) {
var ret = [];
if (typeof filters=='string') {

View file

@ -10,7 +10,12 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
.then(() => {
this.page_title = this.page_title + ' ' + __('Gantt');
this.calendar_settings = frappe.views.calendar[this.doctype] || {};
if(this.calendar_settings.order_by) {
if (typeof this.calendar_settings.gantt == 'object') {
Object.assign(this.calendar_settings, this.calendar_settings.gantt);
}
if (this.calendar_settings.order_by) {
this.sort_by = this.calendar_settings.order_by;
this.sort_order = 'asc';
} else {

View file

@ -39,7 +39,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
this.save_kanban_board_filters();
}
});
this.toggle_paging = true;
return this.get_board();
});
}
@ -78,6 +78,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
this.save_view_user_settings({
last_kanban_board: this.board_name
});
this.toggle_paging && this.$paging_area.toggle(false);
}
render_list() {

View file

@ -1061,7 +1061,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
// applied to Float, Currency fields, needed only for currency formatting.
// make first data column have value 'Total'
let index = 1;
if (this.datatable && this.datatable.options.checkboxColumn) index = 2;
if (this.report_settings.get_datatable_options) {
let datatable = this.report_settings.get_datatable_options({});
if (datatable && datatable.checkboxColumn) index = 2;
}
if (column.colIndex === index && !value) {
value = "Total";

Some files were not shown because too many files have changed in this diff Show more