Merge branch 'develop' of github.com:frappe/frappe into skip-backup-tables

This commit is contained in:
Gavin D'souza 2020-11-09 14:53:59 +05:30
commit fb0d1fbac1
292 changed files with 8247 additions and 5623 deletions

View file

@ -3,7 +3,10 @@ import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
f_string_pattern = re.compile(r"_\(f[\"']")
starts_with_f_pattern = re.compile(r"_\(f")
# skip first argument
files = sys.argv[1:]
@ -14,9 +17,25 @@ for _file in files_to_scan:
print(f'Checking: {_file}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
if 'frappe-lint: disable-translate' in line:
continue
start_matches = start_pattern.search(line)
if start_matches:
starts_with_f = starts_with_f_pattern.search(line)
if starts_with_f:
has_f_string = f_string_pattern.search(line)
if has_f_string:
errors_encounter += 1
print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
continue
else:
continue
match = pattern.search(line)
error_found = False
if not match and line.endswith(',\n'):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1:])
@ -24,11 +43,18 @@ for _file in files_to_scan:
match = pattern.match(line)
if not match:
error_found = True
print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
if not error_found and not words_pattern.search(line):
error_found = True
print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
if error_found:
errors_encounter += 1
print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}')
if errors_encounter > 0:
print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
sys.exit(1)
else:
print('\nGood To Go!')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
## Contributing
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Pull-Request-Guidelines)
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com)
### Website

View file

@ -20,10 +20,14 @@ context('FileUploader', () => {
open_upload_dialog();
cy.fixture('example.json').then(fileContent => {
cy.get_open_dialog().find('.file-upload-area').upload(
{ fileContent, fileName: 'example.json', mimeType: 'application/json' },
{ subjectType: 'drag-n-drop' },
);
cy.get_open_dialog().find('.file-upload-area').upload({
fileContent,
fileName: 'example.json',
mimeType: 'application/json'
}, {
subjectType: 'drag-n-drop',
force: true
});
cy.get_open_dialog().find('.file-info').should('contain', 'example.json');
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');

View file

@ -312,7 +312,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -321,6 +321,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param title: [optional] Message title.
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
:param primary_action: [optional] Bind a primary server/client side action.
:param is_minimizable: [optional] Allow users to minimize the modal
:param wide: [optional] Show wide modal
@ -346,16 +347,10 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
return
if as_table and type(msg) in (list, tuple):
out.as_table = 1
table_rows = ''
for row in msg:
table_row_data = ''
for data in row:
table_row_data += '<td>{}</td>'.format(data)
table_rows += '<tr>{}</tr>'.format(table_row_data)
out.message = '''<table class="table table-bordered"
style="margin: 0;">{}</table>'''.format(table_rows)
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",

View file

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

View file

@ -3,14 +3,69 @@
frappe.ui.form.on('Assignment Rule', {
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]);
},
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) {
frm.set_fields_as_options(
'due_date_based_on',
doctype,
(df) => ['Date', 'Datetime'].includes(df.fieldtype)
).then(options => frm.set_df_property('due_date_based_on', 'hidden', !options.length));
}
}
},
});

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2019-02-28 17:12:18.815830",
@ -8,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"document_type",
"due_date_based_on",
"priority",
"disabled",
"column_break_4",
@ -22,6 +24,7 @@
"assignment_days",
"assign_to_users_section",
"rule",
"field",
"users",
"last_user"
],
@ -91,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",
@ -129,9 +133,25 @@
"label": "Assignment Days",
"options": "Assignment Rule Day",
"reqd": 1
},
{
"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"
},
{
"depends_on": "eval: doc.rule == 'Based on Field'",
"fieldname": "field",
"fieldtype": "Select",
"label": "Field",
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
}
],
"modified": "2019-09-25 14:52:12.214514",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-20 14:47:20.662954",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",

View file

@ -19,14 +19,14 @@ class AssignmentRule(Document):
repeated_days = get_repeated(assignment_days)
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
def on_update(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_update(self):
clear_assignment_rule_cache(self)
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def after_rename(self, old, new, merge):
clear_assignment_rule_cache(self)
def on_trash(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_trash(self):
clear_assignment_rule_cache(self)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -38,26 +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
))
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'''
@ -69,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
'''
@ -77,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):
'''
@ -188,7 +194,7 @@ def apply(doc, method=None, doctype=None, name=None):
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.get('name')))
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
if not assignment_rule_docs:
return
@ -237,6 +243,40 @@ def apply(doc, method=None, doctype=None, name=None):
break
assignment_rule.close_assignments(doc)
def update_due_date(doc, state=None):
# called from hook
if (frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
return
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
document_type = doc.doctype,
disabled = 0,
due_date_based_on = ['is', 'set']
))
for rule in assignment_rules:
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
due_date_field = rule_doc.due_date_based_on
if doc.meta.has_field(due_date_field) and \
doc.has_value_changed(due_date_field) and rule.get('name'):
assignment_todos = frappe.get_all('ToDo', {
'assignment_rule': rule.get('name'),
'status': 'Open',
'reference_type': doc.doctype,
'reference_name': doc.name
})
for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule',
'docname': rule.get('name'),
'label': _('via Assignment Rule')
}
todo_doc.save(ignore_permissions=True)
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
@ -250,3 +290,7 @@ def get_repeated(values):
if value not in diff:
diff.append(str(value))
return " ".join(diff)
def clear_assignment_rule_cache(rule):
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)

View file

@ -20,6 +20,7 @@ class TestAutoAssign(unittest.TestCase):
dict(day = 'Friday'),
dict(day = 'Saturday'),
]
self.days = days
self.assignment_rule = get_assignment_rule([days, days])
clear_assignments()
@ -87,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
@ -180,6 +205,55 @@ class TestAutoAssign(unittest.TestCase):
status = 'Open'
), 'owner'), ['test3@example.com'])
def test_assignment_rule_condition(self):
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
# Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
create_custom_field('Note', df)
assignment_rule = frappe.get_doc(dict(
name = 'Assignment with Due Date',
doctype = 'Assignment Rule',
document_type = 'Note',
assign_condition = 'public == 0',
due_date_based_on = 'expiry_date',
assignment_days = self.days,
users = [
dict(user = 'test@example.com'),
]
)).insert()
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
note1 = make_note({'expiry_date': expiry_date})
note2 = make_note({'expiry_date': expiry_date})
note1_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note1.name,
status = 'Open'
))[0]
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name)
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)
# due date should be updated if the reference doc's date is updated.
note1.expiry_date = frappe.utils.add_days(expiry_date, 2)
note1.save()
note1_todo_doc.reload()
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)
# saving one note's expiry should not update other note todo's due date
note2_todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note2.name,
status = 'Open'
), fields=['name', 'date'])[0]
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
assignment_rule.delete()
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
@ -237,4 +311,4 @@ def make_note(values=None):
note.insert()
return note
return note

View file

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

View file

@ -40,6 +40,7 @@ def build_missing_files():
# check which files dont exist yet from the build.json and tell build.js to build only those!
missing_assets = []
current_asset_files = []
frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json")
for type in ["css", "js"]:
current_asset_files.extend(
@ -49,7 +50,7 @@ def build_missing_files():
]
)
with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f:
with open(frappe_build) as f:
all_asset_files = json.load(f).keys()
for asset in all_asset_files:
@ -111,13 +112,21 @@ def download_frappe_assets(verbose=True):
if assets_archive:
import tarfile
directories_created = set()
click.secho("\nExtracting assets...\n", fg="yellow")
with tarfile.open(assets_archive) as tar:
for file in tar:
if not file.isdir():
dest = "." + file.name.replace("./frappe-bench/sites", "")
asset_directory = os.path.dirname(dest)
show = dest.replace("./assets/", "")
if asset_directory not in directories_created:
if not os.path.exists(asset_directory):
os.makedirs(asset_directory, exist_ok=True)
directories_created.add(asset_directory)
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))

View file

@ -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
@ -636,8 +631,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
@ -646,8 +643,10 @@ def start_recording(context):
@click.command('stop-recording')
@pass_context
def stop_recording(context):
import frappe.recorder
for site in context.sites:
frappe.init(site=site)
frappe.set_user("Administrator")
frappe.recorder.stop()
if not context.sites:
raise SiteNotSpecifiedError

View file

@ -460,11 +460,21 @@ def console(context):
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
import IPython
all_apps = frappe.get_installed_apps()
failed_to_import = []
for app in all_apps:
locals()[app] = __import__(app)
try:
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
IPython.embed(display_banner="", header="", colors="neutral")
@ -554,10 +564,24 @@ def run_ui_tests(context, app, headless=False):
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
os.chdir(app_base_path)
node_bin = subprocess.getoutput("npm bin")
cypress_path = "{0}/cypress".format(node_bin)
plugin_path = "{0}/cypress-file-upload".format(node_bin)
# check if cypress in path...if not, install it.
if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)):
# install cypress
click.secho("Installing Cypress...", fg="yellow")
frappe.commands.popen("yarn add cypress@3 cypress-file-upload@^3.1 --no-lockfile")
# run for headless mode
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)

View file

@ -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.")
}
]
}

View file

@ -23,6 +23,11 @@ def get_data():
"description": _("Company, Fiscal Year and Currency defaults"),
"hide_count": True
},
{
"type": "doctype",
"name": "Log Settings",
"description": _("Log cleanup and notification configuration")
},
{
"type": "doctype",
"name": "Error Log",

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:32",
@ -24,7 +25,6 @@
"is_shipping_address",
"disabled",
"linked_with",
"is_your_company_address",
"links"
],
"fields": [
@ -75,7 +75,7 @@
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
"label": "State/Province"
},
{
"fieldname": "country",
@ -138,12 +138,6 @@
"label": "Reference",
"options": "fa fa-pushpin"
},
{
"default": "0",
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"label": "Is Your Company Address"
},
{
"fieldname": "links",
"fieldtype": "Table",
@ -153,7 +147,8 @@
],
"icon": "fa fa-map-marker",
"idx": 5,
"modified": "2019-09-08 11:41:04.145589",
"links": [],
"modified": "2020-10-21 16:14:37.284830",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Address",

View file

@ -39,14 +39,13 @@ class Address(Document):
def validate(self):
self.link_address()
self.validate_reference()
self.validate_preferred_address()
set_link_title(self)
deduplicate_dynamic_links(self)
def link_address(self):
"""Link address based on owner"""
if not self.links and not self.is_your_company_address:
if not self.links:
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
if contact_name:
contact = frappe.get_cached_doc('Contact', contact_name)
@ -56,12 +55,6 @@ class Address(Document):
return False
def validate_reference(self):
if self.is_your_company_address:
if not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table below."),
title =_("Company not Linked"))
def validate_preferred_address(self):
preferred_fields = ['is_primary_address', 'is_shipping_address']
@ -204,25 +197,6 @@ def get_address_templates(address):
else:
return result
@frappe.whitelist()
def get_shipping_address(company, address = None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
["Address", "is_your_company_address", "=", 1]
]
fields = ["*"]
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
if address:
address_as_dict = address[0]
name, address_template = get_address_templates(address_as_dict)
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
def get_company_address(company):
ret = frappe._dict()
ret.company_address = get_default_address('Company', company)

View file

@ -40,7 +40,11 @@ def add_authentication_log(subject, user, operation="Login", status="Success"):
"operation": operation,
}).insert(ignore_permissions=True, ignore_links=True)
def clear_authentication_logs():
"""clear 100 day old authentication logs"""
def clear_activity_logs(days=None):
"""clear 90 day old authentication logs or configured in log settings"""
if not days:
days = 90
frappe.db.sql("""delete from `tabActivity Log` where \
creation< (NOW() - INTERVAL '100' DAY)""")
creation< (NOW() - INTERVAL '{0}' DAY)""".format(days))

View file

@ -260,10 +260,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)
@ -342,7 +340,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:
@ -357,7 +355,7 @@ def get_contacts(email_strings):
email = get_email_without_link(email)
contact_name = get_contact_name(email)
if not contact_name and email:
if not contact_name and email and auto_create_contact:
email_parts = email.split("@")
first_name = frappe.unscrub(email_parts[0])

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,9 +17,6 @@ def set_old_logs_as_seen():
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""")
# clear old logs
frappe.db.sql("""DELETE FROM `tabError Log` WHERE `creation` < (NOW() - INTERVAL '30' DAY)""")
@frappe.whitelist()
def clear_error_logs():
'''Flush all Error Logs'''

View file

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

View file

@ -0,0 +1,34 @@
{
"actions": [],
"autoname": "field:user",
"creation": "2020-10-08 13:09:36.034430",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-08 17:22:04.690348",
"modified_by": "Administrator",
"module": "Core",
"name": "Log Setting User",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,83 @@
{
"actions": [],
"creation": "2020-10-08 12:12:21.694424",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"error_log_notification_section",
"users_to_notify",
"log_cleanup_section",
"clear_error_log_after",
"clear_activity_log_after",
"column_break_4",
"clear_email_queue_after"
],
"fields": [
{
"fieldname": "log_cleanup_section",
"fieldtype": "Section Break",
"label": "Log Cleanup"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "error_log_notification_section",
"fieldtype": "Section Break",
"label": "Error Log Notification"
},
{
"fieldname": "users_to_notify",
"fieldtype": "Table MultiSelect",
"label": "Users To Notify",
"options": "Log Setting User"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_error_log_after",
"fieldtype": "Int",
"label": "Clear Error log After"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_activity_log_after",
"fieldtype": "Int",
"label": "Clear Activity Log After"
},
{
"default": "90",
"description": "In Days",
"fieldname": "clear_email_queue_after",
"fieldtype": "Int",
"label": "Clear Email Queue After"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-13 12:18:48.649038",
"modified_by": "Administrator",
"module": "Core",
"name": "Log Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,51 @@
# -*- 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 import _
from frappe.model.document import Document
class LogSettings(Document):
def clear_logs(self):
self.clear_error_logs()
self.clear_activity_logs()
self.clear_email_queue()
def clear_error_logs(self):
frappe.db.sql(""" DELETE FROM `tabError Log`
WHERE `creation` < (NOW() - INTERVAL '{0}' DAY)
""".format(self.clear_error_log_after))
def clear_activity_logs(self):
from frappe.core.doctype.activity_log.activity_log import clear_activity_logs
clear_activity_logs(days=self.clear_activity_log_after)
def clear_email_queue(self):
from frappe.email.queue import clear_outbox
clear_outbox(days=self.clear_email_queue_after)
def run_log_clean_up():
doc = frappe.get_doc("Log Settings")
doc.clear_logs()
@frappe.whitelist()
def has_unseen_error_log(user):
def _get_response(show_alert=True):
return {
'show_alert': True,
'message': _("You have unseen {0}").format('<a href="/desk#List/Error%20Log/List"> Error Logs </a>')
}
if frappe.db.sql_list("select name from `tabError Log` where seen = 0 limit 1"):
log_settings = frappe.get_cached_doc('Log Settings')
if log_settings.users_to_notify:
if user in [u.user for u in log_settings.users_to_notify]:
return _get_response()
else:
return _get_response(show_alert=False)
else:
return _get_response()

View file

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

View file

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

View file

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

View file

@ -49,6 +49,10 @@ 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))
and not frappe.flags.in_patch):
frappe.throw(_("You are not allowed to delete Standard Report"))
delete_custom_role('report', self.name)
def get_columns(self):

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(all_job.frequency, 'All')
daily_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.clear_outbox'))
dict(method='frappe.email.queue.set_expiry_for_email_queue'))
self.assertEqual(daily_job.frequency, 'Daily')
# check if cron jobs are synced
@ -38,7 +38,7 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(updated_scheduled_job.frequency, "Hourly")
def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.set_expiry_for_email_queue'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06')))

View file

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

View file

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

View file

@ -32,6 +32,7 @@ class CustomField(Document):
self.fieldname = self.fieldname.lower()
def before_insert(self):
self.set_fieldname()
meta = frappe.get_meta(self.dt, cached=False)
fieldnames = [df.fieldname for df in meta.get("fields")]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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
}

View file

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

View file

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

View file

@ -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(`
<p class="text-muted text-medium">
Package Publish Tool let's you copy documents from your site to any other remote site.
Follow the steps below to publish.
</p>
<ol class="text-muted small">
<li>Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.</li>
<li>Add the Sites URL where you want to copy these documents, and enter the Username and Password.</li>
<li>Click on Save. Now, you can click on Publish and the documents will be copied.</li>
</ol>
`);
}
});
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 =
$(`<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`);
table.find('tbody').append(filter_row);
});
} else {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
${__("Click to Set Filters")}</td></tr>`);
table.find('tbody').append(filter_row);
}
}
let row = frappe.get_doc(cdt, cdn);
let wrapper = $(`[data-fieldname="filters_json"]`).empty();
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
<thead>
<tr>
<th style="width: 33%">${__('Filter')}</th>
<th style="width: 33%">${__('Condition')}</th>
<th>${__('Value')}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>`).appendTo(wrapper);
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).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();
});
});
},
});

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,9 @@
"use_ssl": 0,
"auto_email_id": "hello@example.com",
"google_analytics_id": "google_analytics_id",
"google_analytics_anonymize_ip": 1,
"google_login": {
"client_id": "google_client_id",
"client_secret": "google_client_secret"

View file

@ -30,7 +30,7 @@ frappe.ui.form.on('Data Migration Connector', {
frm.set_value('connector_type', 'Custom');
frm.set_value('python_module', r.message);
frm.save();
frappe.show_alert(__(`New module created ${r.message}`));
frappe.show_alert(__("New module created {0}", [r.message]));
d.hide();
}
});

View file

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

View file

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

View file

@ -20,8 +20,11 @@ def setup_database(force, source_sql=None, verbose=False):
source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
subprocess.check_output([
'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U',
frappe.conf.db_name, '-f', source_sql
'psql', frappe.conf.db_name,
'-h', frappe.conf.db_host or 'localhost',
'-p', str(frappe.conf.db_port or '5432'),
'-U', frappe.conf.db_name,
'-f', source_sql
], env=subprocess_env)
frappe.connect()

View file

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

View file

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

View file

@ -21,8 +21,10 @@ frappe.ui.form.on('Dashboard Chart', {
refresh: function(frm) {
frm.chart_filters = null;
frm.is_disabled = !frappe.boot.developer_mode && frm.doc.is_standard;
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
if (frm.is_disabled) {
!frm.doc.custom_options && frm.set_df_property('chart_options_section', 'hidden', 1);
frm.disable_form();
}
@ -169,7 +171,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
} else {
let y_field_df = frappe.meta.get_docfield('Dashboard Chart Field', 'y_field', frm.doc.name);
y_field_df.options = frm.field_options.numeric_fields;
@ -333,6 +335,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
table.on('click', () => {
frm.is_disabled && frappe.throw(__('Cannot edit filters for standard charts'));
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),

View file

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

View file

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

View file

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

View file

@ -207,7 +207,7 @@ frappe.ui.form.on('Number Card', {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
}
} else {
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));

View file

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

View file

@ -42,7 +42,7 @@ class UserProfile {
}
make_user_profile() {
frappe.set_route('user-profile', this.user_id);
frappe.set_route('user-profile', this.user_id, { redirect: true });
this.user = frappe.user_info(this.user_id);
this.page.set_title(this.user.fullname);
this.setup_user_search();
@ -360,11 +360,12 @@ class UserProfile {
this.get_user_rank().then(() => {
this.get_user_points().then(() => {
let html = $(__(`<p class="user-energy-points text-muted">${__('Energy Points: ')}<span class="rank">{0}</span></p>
<p class="user-energy-points text-muted">${__('Review Points: ')}<span class="rank">{1}</span></p>
<p class="user-energy-points text-muted">${__('Rank: ')}<span class="rank">{2}</span></p>
<p class="user-energy-points text-muted">${__('Monthly Rank: ')}<span class="rank">{3}</span></p>
`, [this.energy_points, this.review_points, this.rank, this.month_rank]));
let html = $(`
<p class="user-energy-points text-muted">${__('Energy Points:')} <span class="rank">${this.energy_points}</span></p>
<p class="user-energy-points text-muted">${__('Review Points:')} <span class="rank">${this.review_points}</span></p>
<p class="user-energy-points text-muted">${__('Rank:')} <span class="rank">${this.rank}</span></p>
<p class="user-energy-points text-muted">${__('Monthly Rank:')} <span class="rank">${this.month_rank}</span></p>
`);
$profile_details.append(html);
});

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,8 @@ class EmailDomain(Document):
def validate(self):
"""Validate email id and check POP3/IMAP and SMTP connections is enabled."""
logger = frappe.logger()
if self.email_id:
validate_email_address(self.email_id, True)
@ -26,19 +28,25 @@ class EmailDomain(Document):
if not frappe.local.flags.in_install and not frappe.local.flags.in_patch:
try:
if self.use_imap:
logger.info('Checking incoming IMAP email server {host}:{port} ssl={ssl}...'.format(
host=self.email_server, port=get_port(self), ssl=self.use_ssl))
if self.use_ssl:
test = imaplib.IMAP4_SSL(self.email_server, port=get_port(self))
else:
test = imaplib.IMAP4(self.email_server, port=get_port(self))
else:
logger.info('Checking incoming POP3 email server {host}:{port} ssl={ssl}...'.format(
host=self.email_server, port=get_port(self), ssl=self.use_ssl))
if self.use_ssl:
test = poplib.POP3_SSL(self.email_server, port=get_port(self))
else:
test = poplib.POP3(self.email_server, port=get_port(self))
except Exception:
frappe.throw(_("Incoming email account not correct"))
except Exception as e:
logger.warn('Incoming email account "{host}" not correct'.format(host=self.email_server), exc_info=e)
frappe.throw(title=_("Incoming email account not correct"),
msg='Error connecting IMAP/POP3 "{host}": {e}'.format(host=self.email_server, e=e))
finally:
try:
@ -54,22 +62,28 @@ class EmailDomain(Document):
if not self.get('smtp_port'):
self.smtp_port = 465
logger.info('Checking outgoing SMTPS email server {host}:{port}...'.format(
host=self.smtp_server, port=self.smtp_port))
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
cint(self.smtp_port) or None)
else:
if self.use_tls and not self.smtp_port:
self.smtp_port = 587
logger.info('Checking outgoing SMTP email server {host}:{port} STARTTLS={tls}...'.format(
host=self.smtp_server, port=self.get('smtp_port'), tls=self.use_tls))
sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None)
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
except Exception as e:
logger.warn('Outgoing email account "{host}" not correct'.format(host=self.smtp_server), exc_info=e)
frappe.throw(title=_("Outgoing email account not correct"),
msg='Error connecting SMTP "{host}": {e}'.format(host=self.smtp_server, e=e))
def on_update(self):
"""update all email accounts using this domain"""
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
try:
email_account = frappe.get_doc("Email Account", email_account.name)
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder", "incoming_port"]:
email_account.set(attr, self.get(attr, default=0))
email_account.save()

View file

@ -5,8 +5,35 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.test_runner import make_test_objects
# test_records = frappe.get_test_records('Domain')
test_records = frappe.get_test_records('Email Domain')
class TestDomain(unittest.TestCase):
pass
def setUp(self):
make_test_objects('Email Domain', reset=True)
def tearDown(self):
frappe.delete_doc("Email Account", "Test")
frappe.delete_doc("Email Domain", "test.com")
def test_on_update(self):
mail_domain = frappe.get_doc("Email Domain", "test.com")
mail_account = frappe.get_doc("Email Account", "Test")
# Initially, incoming_port is different in domain and account
self.assertNotEqual(mail_account.incoming_port, mail_domain.incoming_port)
# Trigger update of accounts using this domain
mail_domain.on_update()
mail_account = frappe.get_doc("Email Account", "Test")
# After update, incoming_port in account should match the domain
self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port)
# Also make sure that the other attributes match
self.assertEqual(mail_account.use_imap, mail_domain.use_imap)
self.assertEqual(mail_account.use_ssl, mail_domain.use_ssl)
self.assertEqual(mail_account.use_tls, mail_domain.use_tls)
self.assertEqual(mail_account.attachment_limit, mail_domain.attachment_limit)
self.assertEqual(mail_account.smtp_server, mail_domain.smtp_server)
self.assertEqual(mail_account.smtp_port, mail_domain.smtp_port)

View file

@ -0,0 +1,30 @@
[
{
"doctype": "Email Domain",
"domain_name": "test.com",
"email_id": "_test@test.com",
"email_server": "imap.test.com",
"use_imap": "imap.test.com",
"use_ssl": 1,
"use_tls": 1,
"incoming_port": "993",
"attachment_limit": "1",
"smtp_server": "smtp.test.com",
"smtp_port": "587"
},
{
"doctype": "Email Account",
"name": "_Test Email Account 1",
"enable_incoming": 1,
"email_id": "_test@test.com",
"domain": "test.com",
"email_server": "imap.test.com",
"use_imap": 1,
"use_ssl": 0,
"use_tls": 1,
"incoming_port": "143",
"attachment_limit": "1",
"smtp_server": "smtp.test.com",
"smtp_port": "587"
}
]

View file

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

View file

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

View file

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

View file

@ -584,14 +584,15 @@ def prepare_message(email, recipient, recipients_list):
return safe_encode(message.as_string())
def clear_outbox():
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
Called daily via scheduler.
def clear_outbox(days=None):
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
Note: Used separate query to avoid deadlock
"""
if not days:
days=31
email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue`
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '31' DAY)""")
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
if email_queues:
frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
@ -602,6 +603,11 @@ def clear_outbox():
','.join(['%s']*len(email_queues)
)), tuple(email_queues))
def set_expiry_for_email_queue():
''' Mark emails as expire that has not sent for 7 days.
Called daily via scheduler.
'''
frappe.db.sql("""
UPDATE `tabEmail Queue`
SET `status`='Expired'

View file

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

View file

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

View file

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

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