diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index 4b1147e79f..510e7c7678 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -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
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index cdf676dd67..90453cd1b4 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -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:
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
index ee633ef039..2a934a6795 100644
--- a/.github/workflows/publish-assets-develop.yml
+++ b/.github/workflows/publish-assets-develop.yml
@@ -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:
diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml
index 5c412ea1b0..e86f884f35 100644
--- a/.github/workflows/publish-assets-releases.yml
+++ b/.github/workflows/publish-assets-releases.yml
@@ -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
-
diff --git a/.mergify.yml b/.mergify.yml
index 582bbc2ee5..5b0ec71b1c 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -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:
diff --git a/.travis.yml b/.travis.yml
index 2331217363..63895675ea 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,12 +31,12 @@ matrix:
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
- script: bench --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
diff --git a/CODEOWNERS b/CODEOWNERS
index b23f98b034..5753d85cfa 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 554f1f9747..fac0927428 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -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):
+ out.as_table = 1
- table_rows = ''
- for row in msg:
- table_row_data = ''
- for data in row:
- table_row_data += '
{} | '.format(data)
- table_rows += '{}
'.format(table_row_data)
-
- out.message = ''''''.format(table_rows)
+ 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:
@@ -801,11 +796,17 @@ def get_doc(*args, **kwargs):
return doc
-def get_last_doc(doctype):
+def get_last_doc(doctype, filters=None, order_by="creation desc"):
"""Get last created document of this type."""
- d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1)
+ d = get_all(
+ doctype,
+ filters=filters,
+ limit_page_length=1,
+ order_by=order_by,
+ pluck="name"
+ )
if d:
- return get_doc(doctype, d[0].name)
+ return get_doc(doctype, d[0])
else:
raise DoesNotExistError
@@ -1159,6 +1160,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",
diff --git a/frappe/app.py b/frappe/app.py
index c4d6a0235a..82471c4e32 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -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
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js
index e8d17527bf..774befc15e 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.js
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js
@@ -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));
}
- }
+ },
});
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.json b/frappe/automation/doctype/assignment_rule/assignment_rule.json
index 858ad8aac4..0a57e06da6 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.json
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.json
@@ -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",
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py
index cd70799361..c85cb149ea 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py
@@ -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):
'''
diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
index dab842ad83..cb1e0ff8f4 100644
--- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py
@@ -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
\ No newline at end of file
+ return note
diff --git a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json
index f529772c8e..5a159c8267 100644
--- a/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json
+++ b/frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json
@@ -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
}
\ No newline at end of file
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 1f4642658f..f8ff07db1d 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -265,14 +265,12 @@ def disable_user(context, email):
user.save(ignore_permissions=True)
frappe.db.commit()
-
@click.command('migrate')
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
- import compileall
import re
from frappe.migrate import migrate
@@ -291,9 +289,6 @@ def migrate(context, skip_failing=False, skip_search_index=False):
if not context.sites:
raise SiteNotSpecifiedError
- print("Compiling Python files...")
- compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
-
@click.command('migrate-to')
@click.argument('frappe_provider')
@pass_context
@@ -615,8 +610,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 +622,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
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 4a69486c1b..31b84ee98a 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -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")
diff --git a/frappe/config/customization.py b/frappe/config/customization.py
index 3d587e6839..95fa5d355c 100644
--- a/frappe/config/customization.py
+++ b/frappe/config/customization.py
@@ -54,12 +54,6 @@ def get_data():
"label": _("Custom Translations"),
"name": "Translation",
"description": _("Add your own translations")
- },
- {
- "type": "doctype",
- "label": _("Package"),
- "name": "Package",
- "description": _("Import and Export Packages.")
}
]
}
diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json
index ae7ab464b8..e85a89ff1a 100644
--- a/frappe/contacts/doctype/address/address.json
+++ b/frappe/contacts/doctype/address/address.json
@@ -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",
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index d12bdce8b8..5ebf714645 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -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])
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index c56a950fbd..4c531fbac6 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -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,
diff --git a/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv b/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv
index 656985b519..e48208ea72 100644
--- a/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv
+++ b/frappe/core/doctype/data_import/fixtures/sample_import_file_for_update.csv
@@ -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
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 5271690527..7880648b6f 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -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
diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py
index 249451fd4d..b083b9eaaa 100644
--- a/frappe/core/doctype/data_import/test_importer.py
+++ b/frappe/core/doctype/data_import/test_importer.py
@@ -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'], "Title 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')
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index e420d3b775..ca134665b8 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -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",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 9d37849746..8a9c130fbe 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -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)))
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 00e80ce4e7..6f4a400577 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -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
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json
index 0f9da802eb..080755c479 100644
--- a/frappe/core/doctype/doctype_action/doctype_action.json
+++ b/frappe/core/doctype/doctype_action/doctype_action.json
@@ -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",
diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json
index 752b4bb5da..0453894467 100644
--- a/frappe/core/doctype/doctype_link/doctype_link.json
+++ b/frappe/core/doctype/doctype_link/doctype_link.json
@@ -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",
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.json b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
index 79eebdbe64..4a88e3be6e 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.json
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.json
@@ -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",
diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
index 2de7552dc1..3ff47facc3 100644
--- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py
+++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py
@@ -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
diff --git a/frappe/core/doctype/navbar_item/navbar_item.json b/frappe/core/doctype/navbar_item/navbar_item.json
index 3bfea52558..541d785710 100644
--- a/frappe/core/doctype/navbar_item/navbar_item.json
+++ b/frappe/core/doctype/navbar_item/navbar_item.json
@@ -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",
diff --git a/frappe/core/doctype/report/report.js b/frappe/core/doctype/report/report.js
index 14e9b3a901..f78fd3e812 100644
--- a/frappe/core/doctype/report/report.js
+++ b/frappe/core/doctype/report/report.js
@@ -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) {
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 8634ad1084..9d30409a2a 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -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)
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 805b903300..d76a1470e4 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -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')
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
index 2a9c1a4573..d4d79b21fb 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
@@ -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",
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index fa854f579e..0d6aa3d7d1 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -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):
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 97209cd8ea..c928939119 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -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})]
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 3946568bb6..2f0819ab68 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -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",
diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js
deleted file mode 100644
index 8662724b1a..0000000000
--- a/frappe/custom/doctype/custom_link/custom_link.js
+++ /dev/null
@@ -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);
- });
- }
-});
diff --git a/frappe/custom/doctype/custom_link/custom_link.py b/frappe/custom/doctype/custom_link/custom_link.py
deleted file mode 100644
index 11316d5751..0000000000
--- a/frappe/custom/doctype/custom_link/custom_link.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-# import frappe
-from frappe.model.document import Document
-
-class CustomLink(Document):
- pass
diff --git a/frappe/custom/doctype/custom_link/test_custom_link.py b/frappe/custom/doctype/custom_link/test_custom_link.py
deleted file mode 100644
index a292f73ad0..0000000000
--- a/frappe/custom/doctype/custom_link/test_custom_link.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
-import unittest
-
-class TestCustomLink(unittest.TestCase):
- pass
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index b1743a96a5..2d220b864c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -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();
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index cd57aa23fe..ff102b3c08 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -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",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index d4eeba3f93..9ce602906c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -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')
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index cace25a03d..46a2f2f9df 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -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)
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 1c7349ef01..227114137c 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -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",
diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/custom/doctype/package_document_type/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json
deleted file mode 100644
index 6d011bd4e4..0000000000
--- a/frappe/custom/doctype/package_document_type/package_document_type.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-14 16:45:47.196395",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "document_type",
- "column_break_2",
- "attachments",
- "overwrite",
- "section_break_4",
- "filters_json"
- ],
- "fields": [
- {
- "fieldname": "document_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Document Type",
- "options": "DocType",
- "reqd": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "attachments",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Include Attachments"
- },
- {
- "default": "0",
- "fieldname": "overwrite",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Overwrite"
- },
- {
- "fieldname": "section_break_4",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "filters_json",
- "fieldtype": "Code",
- "label": "Filters",
- "options": "JSON"
- }
- ],
- "istable": 1,
- "links": [],
- "modified": "2020-05-14 16:45:47.196395",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Document Type",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_target/__init__.py b/frappe/custom/doctype/package_publish_target/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json
deleted file mode 100644
index baeb7cb8bc..0000000000
--- a/frappe/custom/doctype/package_publish_target/package_publish_target.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-13 16:04:32.724663",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "instance_url",
- "username",
- "password"
- ],
- "fields": [
- {
- "fieldname": "instance_url",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Site URL",
- "reqd": 1
- },
- {
- "fieldname": "username",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Username",
- "reqd": 1
- },
- {
- "fieldname": "password",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "Password",
- "reqd": 1
- }
- ],
- "istable": 1,
- "links": [],
- "modified": "2020-05-15 17:35:16.282235",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Publish Target",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/custom/doctype/package_publish_target/package_publish_target.py
deleted file mode 100644
index 34eee02562..0000000000
--- a/frappe/custom/doctype/package_publish_target/package_publish_target.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-# import frappe
-from frappe.model.document import Document
-
-class PackagePublishTarget(Document):
- pass
diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
deleted file mode 100644
index a0190a8d8c..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Package Publish Tool', {
- refresh: function(frm) {
- frm.set_query("document_type", "package_details", function () {
- return {
- filters: {
- "istable": 0,
- }
- };
- });
-
- frappe.realtime.on("package", (data) => {
- frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
- if ((data.progress+1) != data.total) {
- frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
- } else {
- frm.dashboard.hide_progress();
- }
- });
-
- frm.trigger("show_instructions");
- frm.trigger("last_deployed_on");
- frm.trigger("set_dirty_trigger");
- frm.trigger("set_deploy_primary_action");
- },
- last_deployed_on: function(frm) {
- if (frm.doc.last_deployed_on) {
- frm.trigger("show_indicator");
- }
- },
- show_indicator: function(frm) {
- let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on);
- frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue");
- },
- set_dirty_trigger: function(frm) {
- $(frm.wrapper).on("dirty", function() {
- frm.page.set_primary_action(__('Save'), () => frm.save());
- });
- },
- set_deploy_primary_action: function(frm) {
- if (frm.doc.package_details.length && frm.doc.instances.length) {
- frm.page.set_primary_action(__("Publish"), function () {
- frappe.show_alert({
- message: __("Publishing documents..."),
- indicator: "green"
- });
-
- frappe.call({
- method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package",
- callback: function() {
- frm.reload_doc();
- frappe.msgprint(__("Documents have been published."));
- }
- });
- });
- }
- },
- show_instructions: function(frm) {
- let field = frm.get_field("html_info");
- field.html(`
-
- Package Publish Tool let's you copy documents from your site to any other remote site.
- Follow the steps below to publish.
-
-
- - Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
- - Add the Sites URL where you want to copy these documents, and enter the Username and Password.
- - Click on Save. Now, you can click on Publish and the documents will be copied.
-
- `);
- }
-});
-
-frappe.ui.form.on('Package Document Type', {
- form_render: function (frm, cdt, cdn) {
- function _show_filters(filters, table) {
- table.find('tbody').empty();
-
- if (filters.length > 0) {
- filters.forEach(filter => {
- const filter_row =
- $(`
- | ${filter[1]} |
- ${filter[2] || ""} |
- ${filter[3]} |
-
`);
-
- table.find('tbody').append(filter_row);
- });
- } else {
- const filter_row = $(`|
- ${__("Click to Set Filters")} |
`);
- table.find('tbody').append(filter_row);
- }
- }
-
- let row = frappe.get_doc(cdt, cdn);
-
- let wrapper = $(`[data-fieldname="filters_json"]`).empty();
- let table = $(`
-
-
- | ${__('Filter')} |
- ${__('Condition')} |
- ${__('Value')} |
-
-
-
-
-
`).appendTo(wrapper);
- $(`${__("Click table to edit")}
`).appendTo(wrapper);
-
- let filters = JSON.parse(row.filters_json || '[]');
- _show_filters(filters, table);
-
- table.on('click', () => {
- if (!row.document_type) {
- frappe.msgprint(__("Select Document Type."));
- return;
- }
-
- frappe.model.with_doctype(row.document_type, function() {
- let dialog = new frappe.ui.Dialog({
- title: __('Set Filters'),
- fields: [
- {
- fieldtype: 'HTML',
- label: 'Filters',
- fieldname: 'filter_area',
- }
- ],
- primary_action: function() {
- let values = filter_group.get_filters();
- let flt = [];
- if (values) {
- values.forEach(function(value) {
- flt.push([value[0], value[1], value[2], value[3]]);
- });
- }
- row.filters_json = JSON.stringify(flt);
- _show_filters(flt, table);
- dialog.hide();
- },
- primary_action_label: "Set"
- });
-
- let filter_group = new frappe.ui.FilterGroup({
- parent: dialog.get_field('filter_area').$wrapper,
- doctype: row.document_type,
- on_change: () => {},
- });
- filter_group.add_filters_to_filter_group(filters);
- dialog.show();
- });
- });
- },
-});
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
deleted file mode 100644
index 0f85ae0348..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-13 15:54:38.082657",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "html_info",
- "sb_00",
- "package_details",
- "sb_01",
- "instances",
- "last_deployed_on"
- ],
- "fields": [
- {
- "description": "Click on the row for accessing filters.",
- "fieldname": "package_details",
- "fieldtype": "Table",
- "label": "Document Types",
- "options": "Package Document Type",
- "reqd": 1
- },
- {
- "fieldname": "instances",
- "fieldtype": "Table",
- "label": "Sites",
- "options": "Package Publish Target",
- "reqd": 1
- },
- {
- "fieldname": "html_info",
- "fieldtype": "HTML"
- },
- {
- "fieldname": "last_deployed_on",
- "fieldtype": "Datetime",
- "hidden": 1,
- "label": "Last Deployed On",
- "read_only": 1
- },
- {
- "fieldname": "sb_00",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "sb_01",
- "fieldtype": "Section Break"
- }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2020-05-15 17:31:37.060199",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Publish Tool",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "All",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
deleted file mode 100644
index b73f93a628..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-import json
-import datetime
-import base64
-from frappe.model.document import Document
-from frappe.utils.file_manager import save_file, get_file
-from frappe import _
-from six import string_types
-from frappe.frappeclient import FrappeClient
-from frappe.utils import get_datetime_str, get_datetime
-from frappe.utils.password import get_decrypted_password
-
-class PackagePublishTool(Document):
- pass
-
-@frappe.whitelist()
-def deploy_package():
- package, doc = export_package()
-
- file_name = "Package-" + get_datetime_str(get_datetime())
-
- length = len(doc.instances)
- for idx, instance in enumerate(doc.instances):
- frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")},
- user=frappe.session.user)
-
- install_package_to_remote(package, instance)
-
- frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime())
-
-def install_package_to_remote(package, instance):
- try:
- connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name))
- except Exception:
- frappe.log_error(frappe.get_traceback())
- frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url))
-
- try:
- connection.post_request({
- "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package",
- "package": json.dumps(package)
- })
- except Exception:
- frappe.log_error(frappe.get_traceback())
- frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url))
-
-@frappe.whitelist()
-def export_package():
- """Export package as JSON."""
- package_doc = frappe.get_single("Package Publish Tool")
- package = []
-
- for doctype in package_doc.package_details:
- filters = []
-
- if doctype.get("filters_json"):
- filters = json.loads(doctype.get("filters_json"))
-
- docs = frappe.get_all(doctype.get("document_type"), filters=filters)
- length = len(docs)
-
- for idx, doc in enumerate(docs):
- frappe.publish_realtime("package", {
- "progress":idx, "total":length,
- "message":doctype.get("document_type"),
- "prefix": _("Exporting")
- },
- user=frappe.session.user)
-
- document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict()
- attachments = []
-
- if doctype.attachments:
- filters = {
- "attached_to_doctype": document.get("doctype"),
- "attached_to_name": document.get("name")
- }
-
- for f in frappe.get_list("File", filters=filters):
- fname, fcontents = get_file(f.name)
- attachments.append({
- "fname": fname,
- "content": base64.b64encode(fcontents).decode('ascii')
- })
-
- document.update({
- "__attachments": attachments,
- "__overwrite": True if doctype.overwrite else False
- })
-
- package.append(document)
-
- return post_process(package), package_doc
-
-@frappe.whitelist()
-def import_package(package=None):
- """Import package from JSON."""
- frappe.only_for("System Manager")
- if isinstance(package, string_types):
- package = json.loads(package)
-
- for doc in package:
- modified = doc.pop("modified")
- overwrite = doc.pop("__overwrite")
- attachments = doc.pop("__attachments")
- exists = frappe.db.exists(doc.get("doctype"), doc.get("name"))
-
- if not exists:
- d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True)
- if attachments:
- add_attachment(attachments, d)
- else:
- docname = doc.pop("name")
- document = frappe.get_doc(doc.get("doctype"), docname)
-
- if overwrite:
- update_document(document, doc, attachments)
-
- else:
- if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified):
- update_document(document, doc, attachments)
-
-def update_document(document, doc, attachments):
- document.update(doc)
- document.save()
- if attachments:
- add_attachment(attachments, document)
-
-def add_attachment(attachments, doc):
- for attachment in attachments:
- save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name"))
-
-def post_process(package):
- """Remove the keys from Document and Child Document. Convert datetime, date, time to str."""
- del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus')
- child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name')
-
- for doc in package:
- for key in del_keys:
- if key in doc:
- del doc[key]
-
- for key, value in doc.items():
- stringified_value = get_stringified_value(value)
- if stringified_value:
- doc[key] = stringified_value
-
- if not isinstance(value, list):
- continue
-
- for child in value:
- for child_key in child_del_keys:
- if child_key in child:
- del child[child_key]
-
- for child_key, child_value in child.items():
- stringified_value = get_stringified_value(child_value)
- if stringified_value:
- child[child_key] = stringified_value
-
- return package
-
-def get_stringified_value(value):
- if isinstance(value, datetime.datetime):
- return frappe.utils.get_datetime_str(value)
-
- if isinstance(value, datetime.date):
- return frappe.utils.get_date_str(value)
-
- if isinstance(value, datetime.timedelta):
- return frappe.utils.get_time_str(value)
-
- return None
diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json
index 5888e11969..b318d92c5a 100644
--- a/frappe/custom/doctype/property_setter/property_setter.json
+++ b/frappe/custom/doctype/property_setter/property_setter.json
@@ -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": "Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
",
- "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": "Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
"
+ },
{
- "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
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index d8ab5ede73..56e5829271 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -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)
+
diff --git a/frappe/database/database.py b/frappe/database/database.py
index d9755abd33..616dd3c3ec 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -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'))
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 3d997864e4..4faea78551 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -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):
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 52dc2ba917..daabbaa61c 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -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')):
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index a7b623ce22..4d5c904f56 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -378,7 +378,8 @@ 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
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index d763ce5009..f5d1ee0df5 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -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'),
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index c4c6077e85..a6126f1f9b 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -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
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js
index b8b7f37a4f..88dc145be2 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.js
+++ b/frappe/desk/doctype/notification_settings/notification_settings.js
@@ -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) => {
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.json b/frappe/desk/doctype/notification_settings/notification_settings.json
index 85f93e156e..fc12022e89 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.json
+++ b/frappe/desk/doctype/notification_settings/notification_settings.json
@@ -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",
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 26b2bd2835..aee7a8e52a 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -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 = "{0}
".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:
diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py
index 06928f9855..73df6d78cb 100644
--- a/frappe/desk/page/user_profile/user_profile.py
+++ b/frappe/desk/page/user_profile/user_profile.py
@@ -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()
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 5a9aae8435..3008cf0e61 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -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])
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 798e499bb9..f249c36746 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -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
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 14a3cfd9f1..343141c66d 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -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 += "
" + _("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():
diff --git a/frappe/email/doctype/email_group/email_group.js b/frappe/email/doctype/email_group/email_group.js
index 63c3832b47..404600c97d 100644
--- a/frappe/email/doctype/email_group/email_group.js
+++ b/frappe/email/doctype/email_group/email_group.js
@@ -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},
diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json
index 0d784d409a..c49de841e6 100644
--- a/frappe/email/doctype/email_group/email_group.json
+++ b/frappe/email/doctype/email_group/email_group.json
@@ -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",
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 5bb654abf3..8ac071fa61 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -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"] = ''
+ message_id = get_message_id()
+ self.set_header('isnotification', '')
+
if is_notification:
- self.msg_root["isnotification"] = ''
+ self.set_header('isnotification', '')
+
+ 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"""
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 9ba080bfda..cf6c13ee76 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -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
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index aa025465e5..f53b835757 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -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'))
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.json b/frappe/event_streaming/doctype/event_producer/event_producer.json
index 8fafdc3bb2..d868f6c123 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.json
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.json
@@ -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",
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index b0ec998ab9..d458f3c24b 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -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()
diff --git a/frappe/custom/doctype/custom_link/__init__.py b/frappe/event_streaming/doctype/event_producer_last_update/__init__.py
similarity index 100%
rename from frappe/custom/doctype/custom_link/__init__.py
rename to frappe/event_streaming/doctype/event_producer_last_update/__init__.py
diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js
new file mode 100644
index 0000000000..15730e4c5f
--- /dev/null
+++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js
@@ -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) {
+
+ // }
+});
diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json
similarity index 53%
rename from frappe/custom/doctype/custom_link/custom_link.json
rename to frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json
index 350e6b1c2d..27f8ed2f81 100644
--- a/frappe/custom/doctype/custom_link/custom_link.json
+++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json
@@ -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
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
similarity index 85%
rename from frappe/custom/doctype/package_document_type/package_document_type.py
rename to frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
index 6e166eecbd..02e297bdd5 100644
--- a/frappe/custom/doctype/package_document_type/package_document_type.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
-class PackageDocumentType(Document):
+class EventProducerLastUpdate(Document):
pass
diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
similarity index 77%
rename from frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
rename to frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
index 8332240543..0311cb2df9 100644
--- a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
+++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestPackagePublishTool(unittest.TestCase):
+class TestEventProducerLastUpdate(unittest.TestCase):
pass
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 88428b875c..60c17f6d5c 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -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
diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json
index bb9abb7ce8..db3fa5a19f 100644
--- a/frappe/geo/doctype/currency/currency.json
+++ b/frappe/geo/doctype/currency/currency.json
@@ -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
}
\ No newline at end of file
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 490c689090..17d022465e 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -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": {
diff --git a/frappe/installer.py b/frappe/installer.py
index c6549f16ee..df767a3294 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -3,7 +3,7 @@
import json
import os
-
+from frappe.defaults import _clear_cache
import frappe
@@ -111,8 +111,8 @@ def remove_from_installed_apps(app_name):
installed_apps = frappe.get_installed_apps()
if app_name in installed_apps:
installed_apps.remove(app_name)
- frappe.db.set_global("installed_apps", json.dumps(installed_apps))
- frappe.get_single("Installed Applications").update_versions()
+ frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
+ _clear_cache("__global")
frappe.db.commit()
if frappe.flags.in_install:
post_install()
@@ -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")
diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py
index fcf648e718..e0087a9e40 100644
--- a/frappe/model/create_new.py
+++ b/frappe/model/create_new.py
@@ -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
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 596f69d2dd..ace9b04cec 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -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:
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 53fcd99f78..3789e20b19 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -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
diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py
index e5ce9102e2..7404ba407e 100644
--- a/frappe/model/dynamic_links.py
+++ b/frappe/model/dynamic_links.py
@@ -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
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 1cc3abba5b..8c17a5b19b 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -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
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index 9ea5fc0ca4..c2e074990e 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -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"""
diff --git a/frappe/patches.txt b/frappe/patches.txt
index dd8654c81b..7d53f5a89d 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -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
diff --git a/frappe/patches/v13_0/remove_custom_link.py b/frappe/patches/v13_0/remove_custom_link.py
new file mode 100644
index 0000000000..f38bb642f0
--- /dev/null
+++ b/frappe/patches/v13_0/remove_custom_link.py
@@ -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')
\ No newline at end of file
diff --git a/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py
new file mode 100644
index 0000000000..21b2d8ef03
--- /dev/null
+++ b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py
@@ -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)
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 1c6f9d9dc5..c67c05f063 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -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",
diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css
index 7f922ffd1d..1cf8bb011a 100644
--- a/frappe/public/css/mobile.css
+++ b/frappe/public/css/mobile.css
@@ -10,6 +10,7 @@ body {
html,
body {
overflow-x: hidden;
+ overflow-y: overlay;
}
@media (max-width: 991px) {
.intro-area,
diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg
index ccdc2ce899..09a3d99b1e 100644
--- a/frappe/public/icons/timeless/symbol-defs.svg
+++ b/frappe/public/icons/timeless/symbol-defs.svg
@@ -648,4 +648,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 2dd8be86e0..0e425abf25 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -214,6 +214,7 @@ frappe.Application = Class.extend({
'reqd': 1
},
{
+ "fieldname": "submit",
"fieldtype": "Button",
"label": __("Submit")
}
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 3525fd0bd0..471825f193 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -134,18 +134,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
me.parse_validate_and_set_in_model(me.get_input_value(), e);
});
},
- bind_focusout: function() {
- // on touchscreen devices, scroll to top
- // so that static navbar and page head don't overlap the input
- if (frappe.dom.is_touchscreen()) {
- var me = this;
- this.$input && this.$input.on("focusout", function() {
- if (frappe.dom.is_touchscreen()) {
- frappe.utils.scroll_to(me.$wrapper);
- }
- });
- }
- },
set_label: function(label) {
if(label) this.df.label = label;
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index bbf9a89072..4db2553bd1 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -21,7 +21,6 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
this.input = this.$input.get(0);
this.has_input = true;
this.bind_change_event();
- this.bind_focusout();
this.setup_autoname_check();
if (this.df.options == 'Phone') {
this.setup_phone();
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 3c41b027a5..27de748c95 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -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;
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 7fea364522..bebe4e4065 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -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;
+};
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index 87c8106bd8..c05868964e 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -53,6 +53,7 @@ export default class Grid {
let template = `
+