Merge branch 'develop' into no-ammend

This commit is contained in:
Suraj Shetty 2020-05-31 20:09:22 +05:30 committed by GitHub
commit bd1a330a31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
194 changed files with 4406 additions and 1486 deletions

View file

@ -25,6 +25,7 @@ cache:
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
- ~/.cache
matrix:
include:
- name: "Python 3.7 MariaDB"
@ -46,7 +47,26 @@ matrix:
script: bench --site test_site run-ui-tests frappe --headless
before_install:
# install wkhtmltopdf
# do we really want to run travis?
- |
ONLY_DOCS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.(md|png|jpg|jpeg)$|^.github|LICENSE' ; echo $?)
ONLY_JS_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.js$' ; echo $?)
ONLY_PY_CHANGES=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '\.py$' ; echo $?)
if [[ $ONLY_DOCS_CHANGES == "1" ]]; then
echo "Only docs were updated, stopping build process.";
exit;
fi
if [[ $ONLY_JS_CHANGES == "1" && $TYPE == "server" ]]; then
echo "Only JavaScript code was updated; Stopping Python build process.";
exit;
fi
if [[ $ONLY_PY_CHANGES == "1" && $TYPE == "ui" ]]; then
echo "Only Python code was updated, stopping Cypress build process.";
exit;
fi
# install wkhtmltopdf
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf

View file

@ -2,6 +2,6 @@
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin",
"defaultCommandTimeout": 10000,
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
}

View file

@ -0,0 +1,45 @@
context('Control Duration', () => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
});
function get_dialog_with_duration(show_days=1, show_seconds=1) {
return cy.dialog({
title: 'Duration',
fields: [{
'fieldname': 'duration',
'fieldtype': 'Duration',
'show_seconds': show_days,
'show_days': show_seconds
}]
});
}
it('should set duration', () => {
get_dialog_with_duration().as('dialog');
cy.get('.frappe-control[data-fieldname=duration] input')
.first()
.click();
cy.get('.duration-input[data-duration=days]')
.type(45, {force: true})
.blur({force: true});
cy.get('.duration-input[data-duration=minutes]')
.type(30)
.blur({force: true});
cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m');
cy.get('.frappe-control[data-fieldname=duration] input').first().blur();
cy.get('.duration-picker').should('not.be.visible');
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('duration');
expect(value).to.equal(3889800);
});
});
it('should hide days or seconds according to duration options', () => {
get_dialog_with_duration(0, 0).as('dialog');
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
});
});

View file

@ -1,7 +1,11 @@
context('Control Link', () => {
beforeEach(() => {
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
});
beforeEach(() => {
cy.visit('/desk#workspace/Website');
cy.create_records({
doctype: 'ToDo',
description: 'this is a test todo for link'
@ -30,7 +34,7 @@ context('Control Link', () => {
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
cy.get('@input').type('todo for link');
cy.get('@input').type('todo for link', { delay: 200 });
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });

View file

@ -1,7 +1,6 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();

View file

@ -44,7 +44,7 @@ class AssignmentRule(Document):
user = self.get_user()
assign_to.add(dict(
assign_to = user,
assign_to = [user],
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc),

View file

@ -299,17 +299,20 @@ def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=N
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
next_date = add_days(schedule_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
next_date = get_next_date(start_date, month_count, day_count)
elif days:
next_date = add_days(next_date, days)
return next_date
def get_next_date(dt, mcount, day=None):
dt = getdate(dt)
dt += relativedelta(months=mcount, day=day)

View file

@ -99,13 +99,18 @@ class TestAutoRepeat(unittest.TestCase):
def test_next_schedule_date(self):
current_date = getdate(today())
todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert()
dict(doctype='ToDo', description='test next schedule date for monthly', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
# next_schedule_date is set as on or after current date
# it should not be a previous month's date
self.assertTrue((doc.next_schedule_date >= current_date))
todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date for daily', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
self.assertEqual(getdate(doc.next_schedule_date), current_date)
def make_auto_repeat(**args):
args = frappe._dict(args)

View file

@ -19,6 +19,7 @@ from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
def get_bootinfo():
@ -106,6 +107,8 @@ def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_desk_sidebar_items
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
bootinfo.dashboards = frappe.get_all("Dashboard")
def get_allowed_pages(cache=False):
return get_user_pages_or_reports('Page', cache=cache)

View file

@ -43,14 +43,16 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
db_port=db_port, new_site=True)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None, db_port=None):
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
db_port=None, new_site=False):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
@ -79,7 +81,10 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
make_site_dirs()
installing = touch_file(get_site_path('locks', 'installing.lock'))
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
if new_site:
# run cleanup only if new-site is called
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
@ -97,7 +102,10 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
print("*** Scheduler is", scheduler_status, "***")
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
installing = get_site_path('locks', 'installing.lock')
try:
installing = get_site_path('locks', 'installing.lock')
except AttributeError:
installing = os.path.join(site, 'locks', 'installing.lock')
if installing and os.path.exists(installing):
if mariadb_root_password:
@ -256,6 +264,15 @@ def migrate(context, rebuild_website=False, skip_failing=False):
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
def migrate_to(context, frappe_provider):
"Migrates site to the specified provider"
from frappe.integrations.frappe_providers import migrate_to
for site in context.sites:
migrate_to(site, frappe_provider)
@click.command('run-patch')
@click.argument('module')
@pass_context
@ -317,23 +334,25 @@ def use(site, sites_path='.'):
if os.path.exists(os.path.join(sites_path, site)):
with open(os.path.join(sites_path, "currentsite.txt"), "w") as sitefile:
sitefile.write(site)
print("Current Site set to {}".format(site))
else:
print("{} does not exist".format(site))
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
@click.option('--verbose', default=False, is_flag=True)
@pass_context
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, quiet=False):
backup_path_private_files=None, quiet=False, verbose=False):
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = context.verbose
verbose = verbose or context.verbose
exit_code = 0
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
except Exception as e:
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
@ -342,10 +361,12 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
if verbose:
from frappe.utils import now
print("database backup taken -", odb.backup_path_db, "- on", now())
summary_title = "Backup Summary at {0}".format(now())
print(summary_title + "\n" + "-" * len(summary_title))
print("Database backup:", odb.backup_path_db)
if with_files:
print("files backup taken -", odb.backup_path_files, "- on", now())
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
print("Public files: ", odb.backup_path_files)
print("Private files: ", odb.backup_path_private_files)
frappe.destroy()
sys.exit(exit_code)
@ -559,6 +580,7 @@ commands = [
install_app,
list_apps,
migrate,
migrate_to,
new_site,
reinstall,
reload_doc,

View file

@ -3,7 +3,7 @@ from frappe import _
def get_data():
return [
{
{
"label": _("Form Customization"),
"icon": "fa fa-glass",
"items": [
@ -57,9 +57,9 @@ def get_data():
},
{
"type": "doctype",
"label": _("Custom Tags"),
"name": "Tag Category",
"description": _("Add your own Tag Categories")
"label": _("Package"),
"name": "Package",
"description": _("Import and Export Packages.")
}
]
}

View file

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, has_gravatar
from frappe.utils import cstr, has_gravatar, cint
from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
@ -133,7 +133,7 @@ def get_default_contact(doctype, name):
dl.parenttype = "Contact"''', (doctype, name))
if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(cint(y[1]), cint(x[1]))))[0][0]
else:
return None

View file

@ -2,20 +2,21 @@
# MIT License. See license.txt
from __future__ import unicode_literals, absolute_import
from collections import Counter
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
from frappe.core.doctype.communication.email import (validate_email,
notify, _notify, update_parent_mins_to_first_response)
from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_seconds
from frappe.core.doctype.communication.email import validate_email, notify, _notify
from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from six.moves.urllib.parse import unquote
from collections import Counter
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.automation.doctype.assignment_rule.assignment_rule import apply as apply_assignment_rule
exclude_from_linked_with = True
@ -119,7 +120,7 @@ class Communication(Document):
update_comment_in_doc(self)
if self.comment_type != 'Updated':
update_parent_mins_to_first_response(self)
update_parent_document_on_communication(self)
self.bot_reply()
def on_trash(self):
@ -258,7 +259,12 @@ class Communication(Document):
# Timeline Links
def set_timeline_links(self):
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
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])
for contact_name in contacts:
self.add_link('Contact', contact_name)
@ -423,3 +429,39 @@ def get_email_without_link(email):
email_host = email.split("@")[1]
return "{0}@{1}".format(email_id, email_host)
def update_parent_document_on_communication(doc):
"""Update mins_to_first_communication of parent document based on who is replying."""
parent = get_parent_doc(doc)
if not parent:
return
# update parent mins_to_first_communication only if we create the Email communication
# ignore in case of only Comment is added
if doc.communication_type == "Comment":
return
status_field = parent.meta.get_field("status")
if status_field:
options = (status_field.options or '').splitlines()
# if status has a "Replied" option, then update the status for received communication
if ('Replied' in options) and doc.sent_or_received=="Received":
parent.db_set("status", "Open")
apply_assignment_rule(parent)
else:
# update the modified date for document
parent.update_modified()
update_mins_to_first_communication(parent, doc)
parent.run_method('notify_communication', doc)
parent.notify_update()
def update_mins_to_first_communication(parent, communication):
if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'):
if is_system_user(communication.sender):
first_responded_on = communication.creation
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
parent.db_set('first_responded_on', first_responded_on)
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)

View file

@ -9,7 +9,7 @@ import json
from email.utils import formataddr
from frappe.core.utils import get_parent_doc
from frappe.utils import (get_url, get_formatted_email, cint,
validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
validate_email_address, split_emails, parse_addr, get_datetime)
from frappe.email.email_body import get_message_id
import frappe.email.smtp
import time
@ -172,33 +172,6 @@ def _notify(doc, print_html=None, print_format=None, attachments=None,
print_letterhead=frappe.flags.print_letterhead
)
def update_parent_mins_to_first_response(doc):
"""Update mins_to_first_communication of parent document based on who is replying."""
parent = get_parent_doc(doc)
if not parent:
return
# update parent mins_to_first_communication only if we create the Email communication
# ignore in case of only Comment is added
if doc.communication_type == "Comment":
return
status_field = parent.meta.get_field("status")
if status_field:
options = (status_field.options or '').splitlines()
# if status has a "Replied" option, then update the status for received communication
if ('Replied' in options) and doc.sent_or_received=="Received":
parent.db_set("status", "Open")
else:
# update the modified date for document
parent.update_modified()
update_mins_to_first_communication(parent, doc)
parent.run_method('notify_communication', doc)
parent.notify_update()
def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_account=False):
doc.all_email_addresses = []
doc.sent_email_addresses = []
@ -499,15 +472,6 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
raise
def update_mins_to_first_communication(parent, communication):
if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'):
if frappe.db.get_all('User', filters={'email': communication.sender,
'user_type': 'System User', 'enabled': 1}, limit=1):
first_responded_on = communication.creation
if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent":
parent.db_set('first_responded_on', first_responded_on)
parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2)
@frappe.whitelist(allow_guest=True)
def mark_email_as_seen(name=None):
try:

View file

@ -202,6 +202,8 @@ class TestCommunication(unittest.TestCase):
self.assertIn(("Note", note.name), doc_links)
def create_email_account():
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
frappe.flags.mute_emails = False
frappe.flags.sent_mail = None

View file

@ -74,7 +74,6 @@ class Importer:
self.read_content(content, extension)
self.validate_template_content()
self.remove_empty_rows_and_columns()
def read_file(self, file_path):
extn = file_path.split(".")[1]
@ -99,6 +98,8 @@ class Importer:
elif extension == "xls":
data = read_xls_file_from_attached_file(content)
data = self.remove_empty_rows_and_columns(data)
if len(data) <= 1:
frappe.throw(
_("Import template should contain a Header and atleast one row."), title=error_title
@ -114,42 +115,41 @@ class Importer:
_("Number of columns does not match with data"), title=_("Invalid Template")
)
def remove_empty_rows_and_columns(self):
def remove_empty_rows_and_columns(self, raw_data):
self.row_index_map = []
removed_rows = []
removed_columns = []
# remove empty rows
data = []
for i, row in enumerate(self.data):
data_without_empty_rows = []
for i, row in enumerate(raw_data):
if all(v in INVALID_VALUES for v in row):
# empty row
removed_rows.append(i)
else:
data.append(row)
data_without_empty_rows.append(row)
self.row_index_map.append(i)
# remove empty columns
# a column with a header and no data is a valid column
# a column with no header and no data will be removed
header_row = []
for i, column in enumerate(self.header_row):
column_values = [row[i] for row in data]
values = [column] + column_values
if all(v in INVALID_VALUES for v in values):
first_row = data_without_empty_rows[0]
for i, column in enumerate(first_row):
column_values = [row[i] for row in data_without_empty_rows]
if all(v in INVALID_VALUES for v in column_values):
# empty column
removed_columns.append(i)
else:
header_row.append(column)
data_without_empty_columns = []
# remove empty columns from data
for i, row in enumerate(data):
new_row = [v for j, v in enumerate(row) if j not in removed_columns]
data_without_empty_columns.append(new_row)
if removed_columns:
data_without_empty_rows_and_columns = []
# remove empty columns from data
for i, row in enumerate(data_without_empty_rows):
new_row = [v for j, v in enumerate(row) if j not in removed_columns]
data_without_empty_rows_and_columns.append(new_row)
else:
data_without_empty_rows_and_columns = data_without_empty_rows
self.data = data_without_empty_columns
self.header_row = header_row
return data_without_empty_rows_and_columns
def get_data_for_import_preview(self):
out = frappe._dict()
@ -325,7 +325,7 @@ class Importer:
def detect_date_formats(self, columns):
for col in columns:
if col.df and col.df.fieldtype in ['Date', 'Time', 'Datetime']:
if col.df and col.df.fieldtype in ["Date", "Time", "Datetime"]:
col.date_format = self.guess_date_format_for_column(col, columns)
return columns
@ -351,7 +351,16 @@ class Importer:
value = cstr(value)
# convert boolean values to 0 or 1
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false", "yes", "no", "y", "n"]:
if df.fieldtype == "Check" and value.lower().strip() in [
"t",
"f",
"true",
"false",
"yes",
"no",
"y",
"n",
]:
value = value.lower().strip()
value = 1 if value in ["t", "true", "y", "yes"] else 0
@ -398,8 +407,9 @@ class Importer:
date_values = [
row[column_index] for row in self.data[:PARSE_ROW_COUNT] if row[column_index]
]
date_formats = [guess_date_format(d) if isinstance(d, str) else None
for d in date_values]
date_formats = [
guess_date_format(d) if isinstance(d, str) else None for d in date_values
]
if not date_formats:
return
max_occurred_date_format = max(set(date_formats), key=date_formats.count)
@ -827,9 +837,9 @@ class Importer:
id_value = doc[id_fieldname]
existing_doc = frappe.get_doc(self.doctype, id_value)
existing_doc.flags.updater_reference = {
'doctype': self.data_import.doctype,
'docname': self.data_import.name,
'label': _('via Data Import')
"doctype": self.data_import.doctype,
"docname": self.data_import.name,
"label": _("via Data Import"),
}
existing_doc.update(doc)
existing_doc.save()

View file

@ -177,8 +177,8 @@ frappe.ui.form.on('Data Import Beta', {
start_import(frm) {
frm
.call({
doc: frm.doc,
method: 'start_import',
method: 'form_start_import',
args: { data_import: frm.doc.name },
btn: frm.page.btn_primary
})
.then(r => {
@ -252,8 +252,8 @@ frappe.ui.form.on('Data Import Beta', {
frm
.call({
doc: frm.doc,
method: 'get_preview_from_template',
args: { data_import: frm.doc.name },
error_handlers: {
TimestampMismatchError() {
// ignore this error

View file

@ -61,6 +61,16 @@ class DataImportBeta(Document):
return Importer(self.reference_doctype, data_import=self)
@frappe.whitelist()
def get_preview_from_template(data_import):
return frappe.get_doc("Data Import Beta", data_import).get_preview_from_template()
@frappe.whitelist()
def form_start_import(data_import):
return frappe.get_doc("Data Import Beta", data_import).start_import()
def start_import(data_import):
"""This method runs in background job"""
data_import = frappe.get_doc("Data Import Beta", data_import)
@ -69,12 +79,11 @@ def start_import(data_import):
i.import_data()
except:
frappe.db.rollback()
data_import.db_set('status', 'Error')
data_import.db_set("status", "Error")
frappe.log_error(title=data_import.name)
frappe.db.commit()
frappe.publish_realtime(
"data_import_refresh", {"data_import": data_import.name}
)
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
@frappe.whitelist()
def download_template(

View file

@ -13,6 +13,8 @@
"fieldname",
"precision",
"length",
"show_days",
"show_seconds",
"reqd",
"search_index",
"in_list_view",
@ -43,6 +45,7 @@
"report_hide",
"remember_last_selected_value",
"ignore_xss_filter",
"hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_38",
@ -86,7 +89,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1,
"search_index": 1
},
@ -448,12 +451,33 @@
{
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_days",
"fieldtype": "Check",
"label": "Show Days"
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_seconds",
"fieldtype": "Check",
"label": "Show Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-19 21:54:13.783908",
"modified": "2020-05-15 09:06:25.224411",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2020-05-11 17:44:54.674657",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"app_name",
"app_version",
"git_branch"
],
"fields": [
{
"fieldname": "git_branch",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Git Branch",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "app_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Application Name",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "app_version",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Application Version",
"read_only": 1,
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-05-12 10:09:49.148087",
"modified_by": "Administrator",
"module": "Core",
"name": "Installed Application",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class InstalledApplication(Document):
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('Installed Applications', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,42 @@
{
"actions": [],
"creation": "2020-05-11 17:45:41.587750",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"installed_applications"
],
"fields": [
{
"fieldname": "installed_applications",
"fieldtype": "Table",
"label": "Installed Applications",
"options": "Installed Application",
"read_only": 1
}
],
"issingle": 1,
"links": [],
"modified": "2020-05-12 10:09:14.310622",
"modified_by": "Administrator",
"module": "Core",
"name": "Installed Applications",
"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,18 @@
# -*- 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 InstalledApplications(Document):
def update_versions(self):
self.delete_key("installed_applications")
for app in frappe.utils.get_installed_apps_info():
self.append("installed_applications", {
"app_name": app.get("app_name"),
"app_version": app.get("version"),
"git_branch": app.get("branch")
})
self.save()

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestInstalledApplications(unittest.TestCase):
pass

View file

@ -0,0 +1,5 @@
.restricted-button {
cursor: default;
position: relative;
right: -5px;
}

View file

@ -26,6 +26,13 @@ class Dashboard {
</div>`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
this.page = wrapper.page;
this.page.set_title_sub(
$(`<button class="restricted-button">
<span class="octicon octicon-lock"></span>
<span>${__('Restricted')}</span>
</button>`)
);
}
show() {

View file

@ -16,6 +16,8 @@
"column_break_6",
"fieldtype",
"precision",
"show_seconds",
"show_days",
"options",
"fetch_from",
"fetch_if_empty",
@ -48,6 +50,7 @@
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
@ -55,361 +58,386 @@
],
"fields": [
{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_seconds",
"fieldtype": "Check",
"label": "Show Seconds",
"show_days": 1,
"show_seconds": 1
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_days",
"fieldtype": "Check",
"label": "Show Days",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
"modified": "2020-04-10 11:57:10.392218",
"modified": "2020-05-15 23:43:00.123572",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",

View file

@ -46,6 +46,9 @@ class CustomField(Document):
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))
if self.fieldname in fieldnames:
frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt))
if self.get('translatable', 0) and not supports_translation(self.fieldtype):
self.translatable = 0

View file

@ -76,7 +76,8 @@ docfield_properties = {
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check'
'allow_in_quick_entry': 'Check',
'hide_border': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),

View file

@ -11,6 +11,8 @@
"label",
"fieldtype",
"fieldname",
"show_seconds",
"show_days",
"reqd",
"unique",
"in_list_view",
@ -39,6 +41,7 @@
"allow_on_submit",
"report_hide",
"remember_last_selected_value",
"hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_33",
@ -57,343 +60,368 @@
],
"fields": [
{
"fieldname": "label_and_type",
"fieldtype": "Section Break",
"label": "Label and Type"
"fieldname": "label_and_type",
"fieldtype": "Section Break",
"label": "Label and Type"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"oldfieldname": "label",
"oldfieldtype": "Data",
"search_index": 1
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"oldfieldname": "label",
"oldfieldtype": "Data",
"search_index": 1
},
{
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1,
"search_index": 1
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name",
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory",
"oldfieldname": "reqd",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
"description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
"fieldname": "permissions",
"fieldtype": "Section Break",
"label": "Permissions"
},
{
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"oldfieldname": "depends_on",
"oldfieldtype": "Data",
"options": "JS"
"description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age&gt;18",
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"oldfieldname": "depends_on",
"oldfieldtype": "Data",
"options": "JS"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Perm Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Perm Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden",
"oldfieldname": "hidden",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden",
"oldfieldname": "hidden",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype == \"Table\"",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
"default": "0",
"depends_on": "eval: doc.fieldtype == \"Table\"",
"fieldname": "allow_bulk_edit",
"fieldtype": "Check",
"label": "Allow Bulk Edit"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On",
"options": "JS"
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On",
"options": "JS"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
"default": "0",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
"fieldname": "remember_last_selected_value",
"fieldtype": "Check",
"label": "Remember Last Selected Value"
"default": "0",
"depends_on": "eval:(doc.fieldtype == 'Link')",
"fieldname": "remember_last_selected_value",
"fieldtype": "Check",
"label": "Remember Last Selected Value"
},
{
"fieldname": "display",
"fieldtype": "Section Break",
"label": "Display"
"fieldname": "display",
"fieldtype": "Section Break",
"label": "Display"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default",
"oldfieldname": "default",
"oldfieldtype": "Text"
"fieldname": "default",
"fieldtype": "Text",
"label": "Default",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"default": "0",
"fieldname": "in_filter",
"fieldtype": "Check",
"label": "In Filter",
"oldfieldname": "in_filter",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
"default": "0",
"fieldname": "in_filter",
"fieldtype": "Check",
"label": "In Filter",
"oldfieldname": "in_filter",
"oldfieldtype": "Check",
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"description": "Print Width of the field, if the field is a column in a table",
"fieldname": "print_width",
"fieldtype": "Data",
"label": "Print Width",
"print_width": "50px",
"width": "50px"
"description": "Print Width of the field, if the field is a column in a table",
"fieldname": "print_width",
"fieldtype": "Data",
"label": "Print Width",
"print_width": "50px",
"width": "50px"
},
{
"depends_on": "eval:cur_frm.doc.istable",
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
"depends_on": "eval:cur_frm.doc.istable",
"description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "width",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data",
"print_width": "50px",
"width": "50px"
"fieldname": "width",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data",
"print_width": "50px",
"width": "50px"
},
{
"default": "0",
"fieldname": "is_custom_field",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Custom Field",
"read_only": 1
"default": "0",
"fieldname": "is_custom_field",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Custom Field",
"read_only": 1
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"fieldname": "property_depends_on_section",
"fieldtype": "Section Break",
"label": "Property Depends On"
"fieldname": "property_depends_on_section",
"fieldtype": "Section Break",
"label": "Property Depends On"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"options": "JS"
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"options": "JS"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_seconds",
"fieldtype": "Check",
"label": "Show Seconds",
"show_days": 1,
"show_seconds": 1
},
{
"default": "1",
"depends_on": "eval:doc.fieldtype === \"Duration\";",
"fieldname": "show_days",
"fieldtype": "Check",
"label": "Show Days",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-10 11:58:44.573537",
"modified": "2020-05-15 23:45:46.810869",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -0,0 +1,65 @@
{
"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

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PackageDocumentType(Document):
pass

View file

@ -0,0 +1,47 @@
{
"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

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class PackagePublishTarget(Document):
pass

View file

@ -0,0 +1,159 @@
// 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

@ -0,0 +1,84 @@
{
"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

@ -0,0 +1,177 @@
# -*- 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."""
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

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestPackagePublishTool(unittest.TestCase):
pass

View file

@ -55,7 +55,8 @@ class MariaDBDatabase(Database):
'Signature': ('longtext', ''),
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', '')
'Geolocation': ('longtext', ''),
'Duration': ('decimal', '18,6')
}
def get_connection(self):

View file

@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` (
`precision` varchar(255) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
`hide_border` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),

View file

@ -60,7 +60,8 @@ class PostgresDatabase(Database):
'Signature': ('text', ''),
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', '')
'Geolocation': ('text', ''),
'Duration': ('decimal', '18,6')
}
def get_connection(self):

View file

@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" (
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
"hide_border" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;

View file

@ -159,7 +159,10 @@ class Workspace:
}
def get_cards(self):
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module)
cards = self.doc.cards
if not self.doc.hide_custom:
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
if len(self.extended_cards):
cards = cards + self.extended_cards
default_country = frappe.db.get_default("country")
@ -274,6 +277,8 @@ class Workspace:
for doc in self.onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
if step.action == "Create Entry":
step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
steps.append(step)
return steps

View file

@ -9,6 +9,7 @@
"dashboard_name",
"is_default",
"charts",
"chart_options",
"cards"
],
"fields": [
@ -33,6 +34,13 @@
"options": "Dashboard Chart Link",
"reqd": 1
},
{
"description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
"fieldname": "chart_options",
"fieldtype": "Code",
"label": "Chart Options",
"options": "JSON"
},
{
"fieldname": "cards",
"fieldtype": "Table",
@ -41,7 +49,7 @@
}
],
"links": [],
"modified": "2020-04-19 17:44:36.237163",
"modified": "2020-04-29 13:26:37.362482",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",

View file

@ -5,6 +5,8 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
from frappe import _
import json
class Dashboard(Document):
def on_update(self):
@ -13,13 +15,29 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
def validate(self):
self.validate_custom_options()
def validate_custom_options(self):
if self.chart_options:
try:
json.loads(self.chart_options)
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
dashboard = frappe.get_doc('Dashboard', dashboard_name)
for chart in dashboard.charts:
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
permitted_charts.append(chart)
chart_dict = frappe._dict()
chart_dict.update(chart.as_dict())
if dashboard.get('chart_options'):
chart_dict.custom_options = dashboard.get('chart_options')
permitted_charts.append(chart_dict)
return permitted_charts
@frappe.whitelist()

View file

@ -92,12 +92,6 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
} else {
if (frm.doc.chart_type == 'Group By') {
frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie', 'Donut']);
} else {
frm.set_df_property('type', 'options', ['Line', 'Bar', 'Heatmap']);
}
frm.set_value('document_type', '');
}
},
@ -257,6 +251,7 @@ frappe.ui.form.on('Dashboard Chart', {
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom';
let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default;
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
@ -274,6 +269,18 @@ frappe.ui.form.on('Dashboard Chart', {
let filters = JSON.parse(frm.doc.filters_json || '[]');
var filters_set = false;
// Set dynamic filters for reports
if (frm.doc.chart_type == 'Report') {
let set_filters = false;
frm.chart_filters.forEach(f => {
if (is_dynamic_filter(f)) {
filters[f.fieldname] = f.default;
set_filters = true;
}
});
set_filters && frm.set_value('filters_json', JSON.stringify(filters));
}
let fields;
if (is_document_type) {
fields = [
@ -298,6 +305,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
} else if (frm.chart_filters.length) {
fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
if (filters[f.fieldname]) {
let condition = '=';
@ -324,7 +332,7 @@ frappe.ui.form.on('Dashboard Chart', {
let dialog = new frappe.ui.Dialog({
title: __('Set Filters'),
fields: fields,
fields: fields.filter(f => !is_dynamic_filter(f)),
primary_action: function() {
let values = this.get_values();
if (values) {
@ -357,8 +365,15 @@ frappe.ui.form.on('Dashboard Chart', {
}
dialog.show();
//Set query report object so that it can be used while fetching filter values in the report
frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
if (frm.doc.chart_type == 'Report') {
//Set query report object so that it can be used while fetching filter values in the report
frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
frappe.query_reports[frm.doc.report_name]
&& frappe.query_reports[frm.doc.report_name].onload
&& frappe.query_reports[frm.doc.report_name].onload(frappe.query_report);
}
dialog.set_values(filters);
});
},

View file

@ -128,8 +128,7 @@
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Line\nBar\nHeatmap",
"reqd": 1
"options": "Line\nBar\nPercentage\nPie\nDonut\nHeatmap"
},
{
"fieldname": "column_break_2",
@ -239,7 +238,7 @@
}
],
"links": [],
"modified": "2020-05-01 19:45:01.669384",
"modified": "2020-05-16 15:03:02.455395",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -110,11 +110,11 @@ def create_dashboard_chart(args):
doc.insert(ignore_permissions=True)
return doc
@frappe.whitelist()
def create_report_chart(args):
create_dashboard_chart(args)
doc = create_dashboard_chart(args)
args = frappe.parse_json(args)
args.chart_name = doc.chart_name
if args.dashboard:
add_chart_to_dashboard(json.dumps(args))
@ -137,7 +137,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
to_date = datetime.datetime.now()
doctype = chart.document_type
unit_function = get_unit_function(doctype, chart.based_on, timegrain)
datefield = chart.based_on
aggregate_function = get_aggregate_function(chart.chart_type)
value_field = chart.value_based_on or '1'
@ -147,26 +146,21 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
filters.append([doctype, datefield, '>=', from_date, False])
filters.append([doctype, datefield, '<=', to_date, False])
data = frappe.db.get_all(
data = frappe.db.get_list(
doctype,
fields = [
'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield),
'{} as _unit'.format(unit_function),
'{} as _unit'.format(datefield),
'{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
],
filters = filters,
group_by = '_year, _unit',
order_by = '_year asc, _unit asc',
group_by = '_unit',
order_by = '_unit asc',
as_list = True,
ignore_ifnull = True
)
result = get_result(data, timegrain, from_date, to_date)
# result given as year, unit -> convert it to end of period of that unit
result = convert_to_dates(data, timegrain)
# add missing data points for periods where there was no result
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
chart_config = {
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
"datasets": [{
@ -220,7 +214,7 @@ def get_group_by_chart_config(chart, filters):
group_by_field = chart.group_by_based_on
doctype = chart.document_type
data = frappe.db.get_all(
data = frappe.db.get_list(
doctype,
fields = [
'{} as name'.format(group_by_field),
@ -261,75 +255,22 @@ def get_aggregate_function(chart_type):
}[chart_type]
def convert_to_dates(data, timegrain):
""" Converts individual dates within data to the end of period """
result = []
for d in data:
if d[2] != 0:
if timegrain == 'Daily':
result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]])
elif timegrain == 'Weekly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
elif timegrain == 'Monthly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
elif timegrain == 'Quarterly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
elif timegrain == 'Yearly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
result[-1][0] = getdate(result[-1][0])
return result
def get_unit_function(doctype, datefield, timegrain):
unit_function = ''
if timegrain=='Daily':
if frappe.db.db_type == 'mariadb':
unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format(
doctype=doctype, datefield=datefield)
else:
unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format(
doctype=doctype, datefield=datefield)
else:
unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format(
unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield)
return unit_function
def add_missing_values(data, timegrain, timespan, from_date, to_date):
# add missing intervals
def get_result(data, timegrain, from_date, to_date):
start_date = getdate(from_date)
end_date = getdate(to_date)
result = []
if timespan != 'All Time':
first_expected_date = get_period_ending(from_date, timegrain)
# fill out data before the first data point
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
while first_data_point_date > first_expected_date:
result.append([first_expected_date, 0.0])
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
while start_date <= end_date:
next_date = get_next_expected_date(start_date, timegrain)
result.append([next_date, 0.0])
start_date = next_date
# fill data points and missing points
for i, d in enumerate(data):
result.append(d)
next_expected_date = get_next_expected_date(d[0], timegrain)
if i < len(data)-1:
next_date = data[i+1][0]
else:
# already reached at end of data, see if we need any more dates
next_date = getdate(nowdate())
# if next data point is earler than the expected date
# need to fill out missing data points
while next_date > next_expected_date:
# fill missing value
result.append([next_expected_date, 0.0])
next_expected_date = get_next_expected_date(next_expected_date, timegrain)
# add date for the last period (if missing)
if result and get_period_ending(to_date, timegrain) > result[-1][0]:
result.append([get_period_ending(to_date, timegrain), 0.0])
data_index = 0
if data:
for i, d in enumerate(result):
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
d[1] += data[data_index][1]
data_index += 1
return result
@ -358,17 +299,12 @@ def get_period_ending(date, timegrain):
return getdate(date)
def get_week_ending(date):
# fun fact: week ends on the day before 1st Jan of the year.
# for 2019 it is Monday
# week starts on monday
from datetime import timedelta
start = date - timedelta(days = date.weekday())
end = start + timedelta(days=6)
week_of_the_year = int(date.strftime('%U'))
if week_of_the_year == 52:
date = add_to_date(date, years=1)
# first day of next week
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1)
# last day of this week
return add_to_date(date, days=-1)
return end
def get_month_ending(date):
month_of_the_year = int(date.strftime('%m'))
@ -435,11 +371,11 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
frappe.throw("You cannot create a dashboard chart from single DocTypes")
frappe.throw(_("You cannot create a dashboard chart from single DocTypes"))
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
frappe.throw("Invalid json added in the custom options: %s" % error)
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))

View file

@ -17,10 +17,9 @@ class TestDashboardChart(unittest.TestCase):
self.assertEqual(get_period_ending('2019-04-10', 'Daily'),
getdate('2019-04-10'))
# fun fact: week ends on the day before 1st Jan of the year.
# for 2019 it is Monday
# week starts on monday
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
getdate('2019-04-15'))
getdate('2019-04-14'))
self.assertEqual(get_period_ending('2019-04-10', 'Monthly'),
getdate('2019-04-30'))
@ -133,6 +132,34 @@ class TestDashboardChart(unittest.TestCase):
frappe.db.rollback()
def test_weekly_dashboard_chart(self):
insert_test_records()
if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart')
frappe.get_doc(dict(
doctype = 'Dashboard Chart',
chart_name = 'Test Weekly Dashboard Chart',
chart_type = 'Sum',
document_type = 'Communication',
based_on = 'communication_date',
value_based_on = 'rating',
timespan = 'Select Date Range',
time_interval = 'Weekly',
from_date = datetime(2018, 12, 30),
to_date = datetime(2019, 1, 15),
filters_json = '[]',
timeseries = 1
)).insert()
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0])
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
frappe.db.rollback()
def test_group_by_chart_type(self):
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
@ -155,17 +182,16 @@ class TestDashboardChart(unittest.TestCase):
frappe.db.rollback()
def test_dashboard_with_single_doctype(self):
if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart')
def insert_test_records():
create_new_communication(datetime(2019, 1, 10), 100)
create_new_communication(datetime(2019, 1, 6), 200)
create_new_communication(datetime(2019, 1, 8), 300)
chart_doc = frappe.get_doc(dict(
doctype = 'Dashboard Chart',
chart_name = 'Test Single DocType In Dashboard Chart',
chart_type = 'Count',
document_type = 'System Settings',
group_by_based_on = 'Created On',
filters_json = '{}',
))
self.assertRaises(frappe.ValidationError, chart_doc.insert)
def create_new_communication(date, rating):
communication = {
'doctype': 'Communication',
'subject': 'Test Communication',
'rating': rating,
'communication_date': date
}
frappe.get_doc(communication).insert()

View file

@ -21,6 +21,7 @@
"disable_user_customization",
"pin_to_top",
"pin_to_bottom",
"hide_custom",
"section_break_2",
"charts_label",
"charts",
@ -189,10 +190,17 @@
"fieldtype": "Link",
"label": "Onboarding",
"options": "Module Onboarding"
},
{
"default": "0",
"description": "Checking this will hide custom doctypes and reports cards in Links section",
"fieldname": "hide_custom",
"fieldtype": "Check",
"label": "Hide Custom DocTypes and Reports"
}
],
"links": [],
"modified": "2020-05-13 19:01:42.041524",
"modified": "2020-05-18 19:17:27.206646",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Page",

View file

@ -20,6 +20,17 @@ class DeskPage(Document):
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Desk Page', self.name]], record_module=self.module)
@staticmethod
def get_module_page_map():
filters = {
'extends_another_page': 0,
'for_user': '',
}
pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1)
return { page[1]: page[0] for page in pages }
def disable_saving_as_standard():
return frappe.flags.in_install or \
frappe.flags.in_patch or \

View file

@ -71,7 +71,7 @@ class TestEvent(unittest.TestCase):
ev = frappe.get_doc(self.test_records[0]).insert()
add({
"assign_to": "test@example.com",
"assign_to": ["test@example.com"],
"doctype": "Event",
"name": ev.name,
"description": "Test Assignment"
@ -83,7 +83,7 @@ class TestEvent(unittest.TestCase):
# add another one
add({
"assign_to": self.test_user,
"assign_to": [self.test_user],
"doctype": "Event",
"name": ev.name,
"description": "Test Assignment"

View file

@ -90,7 +90,7 @@
}
],
"links": [],
"modified": "2020-05-01 19:37:21.492405",
"modified": "2020-05-18 19:42:39.738869",
"modified_by": "Administrator",
"module": "Desk",
"name": "Module Onboarding",
@ -118,6 +118,7 @@
"share": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1

View file

@ -13,7 +13,7 @@ class TestNotificationLog(unittest.TestCase):
user = get_user()
assign_task({
"assign_to": user,
"assign_to": [user],
"doctype": 'ToDo',
"name": todo.name,
"description": todo.description

View file

@ -70,7 +70,7 @@ def get_result(doc, to_date=None):
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters)
number = res[0]['result'] if res else 0
return cint(number)

View file

@ -184,7 +184,7 @@
}
],
"links": [],
"modified": "2020-05-14 15:10:05.627706",
"modified": "2020-05-18 19:42:30.435604",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
@ -213,6 +213,7 @@
}
],
"quick_entry": 1,
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1

View file

@ -11,6 +11,7 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create
get_title, get_title_html
import frappe.utils
import frappe.share
import json
class DuplicateToDoError(frappe.ValidationError): pass
@ -19,17 +20,17 @@ def get(args=None):
if not args:
args = frappe.local.form_dict
return frappe.get_all('ToDo', fields = ['owner', 'description'], filters = dict(
return frappe.get_all('ToDo', fields=['owner', 'name'], filters=dict(
reference_type = args.get('doctype'),
reference_name = args.get('name'),
status = ('!=', 'Cancelled')
), limit = 5)
), limit=5)
@frappe.whitelist()
def add(args=None):
"""add in someone's to do list
args = {
"assign_to": ,
"assign_to": [],
"doctype": ,
"name": ,
"description": ,
@ -40,56 +41,68 @@ def add(args=None):
if not args:
args = frappe.local.form_dict
if frappe.db.sql("""SELECT `owner`
FROM `tabToDo`
WHERE `reference_type`=%(doctype)s
AND `reference_name`=%(name)s
AND `status`='Open'
AND `owner`=%(assign_to)s""", args):
frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
else:
from frappe.utils import nowdate
users_with_duplicate_todo = []
shared_with_users = []
if not args.get('description'):
args['description'] = _('Assignment for {0} {1}').format(args['doctype'], args['name'])
d = frappe.get_doc({
"doctype":"ToDo",
"owner": args['assign_to'],
for assign_to in frappe.parse_json(args.get("assign_to")):
filters = {
"reference_type": args['doctype'],
"reference_name": args['name'],
"description": args.get('description'),
"priority": args.get("priority", "Medium"),
"status": "Open",
"date": args.get('date', nowdate()),
"assigned_by": args.get('assigned_by', frappe.session.user),
'assignment_rule': args.get('assignment_rule')
}).insert(ignore_permissions=True)
"owner": assign_to
}
# set assigned_to if field exists
if frappe.get_meta(args['doctype']).get_field("assigned_to"):
frappe.db.set_value(args['doctype'], args['name'], "assigned_to", args['assign_to'])
if frappe.get_all("ToDo", filters=filters):
users_with_duplicate_todo.append(assign_to)
else:
from frappe.utils import nowdate
doc = frappe.get_doc(args['doctype'], args['name'])
if not args.get('description'):
args['description'] = _('Assignment for {0} {1}').format(args['doctype'], args['name'])
# if assignee does not have permissions, share
if not frappe.has_permission(doc=doc, user=args['assign_to']):
frappe.share.add(doc.doctype, doc.name, args['assign_to'])
frappe.msgprint(_('Shared with user {0} with read access').format(args['assign_to']), alert=True)
d = frappe.get_doc({
"doctype": "ToDo",
"owner": assign_to,
"reference_type": args['doctype'],
"reference_name": args['name'],
"description": args.get('description'),
"priority": args.get("priority", "Medium"),
"status": "Open",
"date": args.get('date', nowdate()),
"assigned_by": args.get('assigned_by', frappe.session.user),
'assignment_rule': args.get('assignment_rule')
}).insert(ignore_permissions=True)
# make this document followed by assigned user
follow_document(args['doctype'], args['name'], args['assign_to'])
# set assigned_to if field exists
if frappe.get_meta(args['doctype']).get_field("assigned_to"):
frappe.db.set_value(args['doctype'], args['name'], "assigned_to", assign_to)
# notify
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
description=args.get("description"))
doc = frappe.get_doc(args['doctype'], args['name'])
# if assignee does not have permissions, share
if not frappe.has_permission(doc=doc, user=assign_to):
frappe.share.add(doc.doctype, doc.name, assign_to)
shared_with_users.append(assign_to)
# make this document followed by assigned user
follow_document(args['doctype'], args['name'], assign_to)
# notify
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',
description=args.get("description"))
if shared_with_users:
user_list = format_message_for_assign_to(shared_with_users)
frappe.msgprint(_("Shared with the following Users with Read access:{0}").format(user_list, alert=True))
if users_with_duplicate_todo:
user_list = format_message_for_assign_to(users_with_duplicate_todo)
frappe.msgprint(_("Already in the following Users ToDo list:{0}").format(user_list, alert=True))
return get(args)
@frappe.whitelist()
def add_multiple(args=None):
import json
if not args:
args = frappe.local.form_dict
@ -183,3 +196,5 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
enqueue_create_notification(owner, notification_doc)
def format_message_for_assign_to(users):
return "<br><br>" + "<br>".join(users)

View file

@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None):
def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
"""
Get all nested submitted linked doctype linkinfo
@ -31,12 +31,26 @@ def get_submitted_linked_docs(doctype, name, docs=None):
if not docs:
docs = []
if not linked:
linked = {}
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
for link_doctype, link_names in linked_docs.items():
if link_doctype not in linked:
linked[link_doctype] = []
for link in link_names:
if link['name'] == name:
continue
if linked and name in linked[link_doctype]:
continue
linked[link_doctype].append(link['name'])
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
@ -47,7 +61,7 @@ def get_submitted_linked_docs(doctype, name, docs=None):
if link.name in [doc.get("name") for doc in docs]:
continue
links = get_submitted_linked_docs(link_doctype, link.name, docs)
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
docs.append({
"doctype": link_doctype,
"name": link.name,

View file

@ -212,7 +212,10 @@ def get_notification_config():
def get_filters_for(doctype):
'''get open filters for doctype'''
config = get_notification_config()
return config.get("for_doctype").get(doctype, {})
doctype_config = config.get("for_doctype").get(doctype, {})
filters = doctype_config if not isinstance(doctype_config, string_types) else None
return filters
@frappe.whitelist()
@frappe.read_only()

View file

@ -14,6 +14,7 @@ def install():
update_global_search_doctypes()
setup_email_linking()
sync_dashboards()
add_unsubscribe()
@frappe.whitelist()
def update_genders():
@ -37,3 +38,15 @@ def setup_email_linking():
"email_id": "email_linking@example.com",
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
def add_unsubscribe():
email_unsubscribe = [
{"email": "admin@example.com", "global_unsubscribe": 1},
{"email": "guest@example.com", "global_unsubscribe": 1}
]
for unsubscribe in email_unsubscribe:
if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
doc = frappe.new_doc("Email Unsubscribe")
doc.update(unsubscribe)
doc.insert(ignore_permissions=True)

View file

@ -110,7 +110,11 @@ class UserProfile {
render_line_chart() {
this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]];
this.line_chart_filters = [
['Energy Point Log', 'user', '=', this.user_id, false],
['Energy Point Log', 'type', '!=', 'Review', false]
];
this.line_chart_config = {
timespan: 'Last Month',
time_interval: 'Daily',
@ -186,7 +190,10 @@ class UserProfile {
options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'],
action: (selected_item) => {
if (selected_item === 'All') {
if (this.line_chart_filters.length > 1) this.line_chart_filters.pop();
this.line_chart_filters = [
['Energy Point Log', 'user', '=', this.user_id, false],
['Energy Point Log', 'type', '!=', 'Review', false]
];
} else {
this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false];
}

View file

@ -62,8 +62,16 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
ljust_list(res, 6)
if report.custom_columns:
# Original query columns, needed to reorder data as per custom columns
query_columns = columns
# Reordered columns
columns = json.loads(report.custom_columns)
if report.report_type == 'Query Report':
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
if custom_columns:
result = add_data_to_custom_columns(custom_columns, result)
@ -208,6 +216,23 @@ def add_data_to_custom_columns(columns, result):
return data
def reorder_data_for_custom_columns(custom_columns, columns, result):
reordered_result = []
columns = [col.split(":")[0] for col in columns]
for res in result:
r = []
for col in custom_columns:
try:
idx = columns.index(col.get("label"))
r.append(res[idx])
except ValueError:
pass
reordered_result.append(r)
return reordered_result
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
doc = None

View file

@ -29,6 +29,7 @@
"default_incoming",
"email_sync_option",
"initial_sync_count",
"create_contact",
"section_break_12",
"enable_automatic_linking",
"section_break_13",
@ -114,9 +115,9 @@
"depends_on": "eval:!doc.service",
"fieldname": "domain",
"fieldtype": "Link",
"label": "Domain",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Domain",
"options": "Email Domain"
},
{
@ -408,11 +409,17 @@
"fieldname": "use_ssl_for_outgoing",
"fieldtype": "Check",
"label": "Use SSL for Outgoing"
},
{
"default": "1",
"fieldname": "create_contact",
"fieldtype": "Check",
"label": "Create Contacts from Incoming Emails"
}
],
"icon": "fa fa-inbox",
"links": [],
"modified": "2020-04-06 19:20:50.491146",
"modified": "2020-05-11 15:18:43.931499",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@ -427,11 +434,11 @@
"write": 1
},
{
"read": 1,
"role": "Inbox User"
"read": 1,
"role": "Inbox User"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View file

@ -90,7 +90,7 @@ def backup_to_dropbox(upload_db_backup=True):
dropbox_settings['access_token'] = access_token['oauth2_token']
set_dropbox_access_token(access_token['oauth2_token'])
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'])
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None)
if upload_db_backup:
if frappe.flags.create_new_backup:

View file

@ -147,11 +147,14 @@ def sync_contacts_from_google_contacts(g_contact):
results = []
contacts_updated = 0
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
contacts = frappe._dict()
while True:
try:
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token,
personFields="names,emailAddresses,organizations,phoneNumbers").execute()
contacts = google_contacts.people().connections().list(resourceName='people/me', pageToken=contacts.get("nextPageToken"),
syncToken=sync_token, pageSize=2000, requestSyncToken=True, personFields="names,emailAddresses,organizations,phoneNumbers").execute()
except HttpError as err:
frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))

View file

@ -0,0 +1,14 @@
# imports - standard imports
import sys
# imports - module imports
from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrator
def migrate_to(local_site, frappe_provider):
if frappe_provider in ("frappe.cloud", "frappecloud.com"):
frappe_provider = "frappecloud.com"
return frappecloud_migrator(local_site, frappe_provider)
else:
print("{} is not supported yet".format(frappe_provider))
sys.exit(1)

View file

@ -0,0 +1,268 @@
# imports - standard imports
import getpass
import json
import re
import sys
# imports - third party imports
import click
from html2text import html2text
import requests
# imports - module imports
import frappe
import frappe.utils.backups
from frappe.utils import get_installed_apps_info
from frappe.utils.commands import render_table, add_line_after
def get_new_site_options():
site_options_sc = session.post(options_url)
if site_options_sc.ok:
site_options = site_options_sc.json()["message"]
return site_options
else:
print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
def is_valid_subdomain(subdomain):
if len(subdomain) < 5:
print("Subdomain too short. Use 5 or more characters")
return False
matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
if matched:
return True
print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
def is_subdomain_available(subdomain):
res = session.post(site_exists_url, {"subdomain": subdomain})
if res.ok:
available = not res.json()["message"]
if not available:
print("Subdomain already exists! Try another one")
return available
def render_plan_table(plans_list):
plans_table = []
# title row
visible_headers = ["name", "cpu_time_per_day"]
plans_table.append(["Plan", "CPU Time"])
# all rows
for plan in plans_list:
plan, cpu_time = [plan[header] for header in visible_headers]
plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
render_table(plans_table)
@add_line_after
def choose_plan(plans_list):
print("{} plans available".format(len(plans_list)))
available_plans = [plan["name"] for plan in plans_list]
render_plan_table(plans_list)
while True:
input_plan = click.prompt("Select Plan").strip()
if input_plan in available_plans:
print("{} Plan selected ✅".format(input_plan))
return input_plan
else:
print("Invalid Selection ❌")
@add_line_after
def check_app_compat(available_group):
is_compat = True
incompatible_apps, filtered_apps, branch_msgs = [], [], []
existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
print("Checking availability of existing app group")
for (app, branch) in existing_group:
info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
if info:
app_title, available_branch = info[0]
if branch != available_branch:
print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
branch_msgs.append([app, branch, available_branch])
filtered_apps.append(app_title)
is_compat = False
else:
print("✅ App {}:{}".format(app, branch))
filtered_apps.append(app_title)
else:
incompatible_apps.append(app)
print("❌ App {}:{}".format(app, branch))
is_compat = False
start_msg = "\nSelecting this group will "
incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
changes = (incompatible_apps + branch_change) or "be perfect for you :)"
warning_message = start_msg + changes
print(warning_message)
return is_compat, filtered_apps
def render_group_table(app_groups):
# title row
app_groups_table = [["#", "App Group", "Apps"]]
# all rows
for idx, app_group in enumerate(app_groups):
apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
row = [idx + 1, app_group["name"], apps_list]
app_groups_table.append(row)
render_table(app_groups_table)
@add_line_after
def filter_apps(app_groups):
render_group_table(app_groups)
while True:
app_group_index = click.prompt("Select App Group Number", type=int) - 1
try:
if app_group_index == -1:
raise IndexError
selected_group = app_groups[app_group_index]
except IndexError:
print("Invalid Selection ❌")
continue
is_compat, filtered_apps = check_app_compat(selected_group)
if is_compat or click.confirm("Continue anyway?"):
print("App Group {} selected! ✅".format(selected_group["name"]))
break
return selected_group["name"], filtered_apps
@add_line_after
def create_session():
# take user input from STDIN
username = click.prompt("Username").strip()
password = getpass.unix_getpass()
auth_credentials = {"usr": username, "pwd": password}
session = requests.Session()
login_sc = session.post(login_url, auth_credentials)
if login_sc.ok:
print("Authorization Successful! ✅")
session.headers.update({"X-Press-Team": username})
return session
else:
print("Authorization Failed with Error Code {}".format(login_sc.status_code))
@add_line_after
def get_subdomain(domain):
while True:
subdomain = click.prompt("Enter subdomain").strip()
if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
print("Site Domain: {}.{}".format(subdomain, domain))
return subdomain
@add_line_after
def upload_backup(local_site):
# take backup
files_session = {}
print("Taking backup for site {}".format(local_site))
odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
# upload files
for x, (file_type, file_path) in enumerate([
("database", odb.backup_path_db),
("public", odb.backup_path_files),
("private", odb.backup_path_private_files)
]):
file_upload_response = session.post(files_url, data={}, files={
"file": open(file_path, "rb"),
"is_private": 1,
"folder": "Home",
"method": "press.api.site.upload_backup",
"type": file_type
})
print("Uploading files ({}/3)".format(x+1), end="\r")
if file_upload_response.ok:
files_session[file_type] = file_upload_response.json()["message"]
else:
print("Upload failed for: {}".format(file_path))
files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
print("Uploaded backup files! ✅")
return files_uploaded
def frappecloud_migrator(local_site, remote_site):
global login_url, upload_url, files_url, options_url, site_exists_url, session
login_url = "https://{}/api/method/login".format(remote_site)
upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
files_url = "https://{}/api/method/upload_file".format(remote_site)
options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
print("Frappe Cloud credentials @ {}".format(remote_site))
# get credentials + auth user + start session
session = create_session()
if session:
# connect to site db
frappe.init(site=local_site)
frappe.connect()
# get new site options
site_options = get_new_site_options()
# set preferences from site options
subdomain = get_subdomain(site_options["domain"])
plan = choose_plan(site_options["plans"])
app_groups = site_options["groups"]
selected_group, filtered_apps = filter_apps(app_groups)
files_uploaded = upload_backup(local_site)
# push to frappe_cloud
payload = json.dumps({
"site": {
"apps": filtered_apps,
"files": files_uploaded,
"group": selected_group,
"name": subdomain,
"plan": plan
}
})
session.headers.update({"Content-Type": "application/json; charset=utf-8"})
site_creation_request = session.post(upload_url, payload)
frappe.destroy()
if site_creation_request.ok:
site_url = site_creation_request.json()["message"]
print("Your site {} is being migrated ✨".format(local_site))
print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
print("Your site URL: {}".format(site_url))
else:
print("Request failed with error code {}".format(site_creation_request.status_code))
reason = html2text(site_creation_request.text)
print(reason)
sys.exit(1)
else:
sys.exit(1)

View file

@ -83,6 +83,9 @@ Otherwise, check the server logs and ensure that all the required services are r
# add static pages to global search
global_search.update_global_search_for_all_web_pages()
# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
#run after_migrate hooks
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):

View file

@ -34,7 +34,8 @@ data_fieldtypes = (
'Signature',
'Color',
'Barcode',
'Geolocation'
'Geolocation',
'Duration'
)
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Table MultiSelect', 'Button', 'Image',

View file

@ -693,7 +693,7 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
sanitized_value = value
if df and df.get("fieldtype") in ("Data", "Code", "Small Text") and df.get("options")=="Email":
if df and df.get("fieldtype") in ("Data", "Code", "Small Text", "Text") and df.get("options")=="Email":
sanitized_value = sanitize_email(value)
elif df and (df.get("ignore_xss_filter")

View file

@ -437,7 +437,7 @@ class Meta(Document):
if not self.custom:
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
data = frappe._dict(frappe.get_attr(hook)(data=data))
return data

View file

@ -278,5 +278,11 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
frappe.patches.v13_0.migrate_translation_column_data
frappe.patches.v13_0.set_read_times
frappe.patches.v13_0.remove_web_view
frappe.patches.v13_0.set_unique_for_page_view
frappe.patches.v13_0.remove_tailwind_from_page_builder
frappe.patches.v13_0.rename_onboarding
frappe.patches.v13_0.email_unsubscribe
execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1)
execute:frappe.delete_doc("DocType", "Onboarding Slide")
execute:frappe.delete_doc("DocType", "Onboarding Slide Field")
execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link")

View file

@ -0,0 +1,13 @@
import frappe
def execute():
email_unsubscribe = [
{"email": "admin@example.com", "global_unsubscribe": 1},
{"email": "guest@example.com", "global_unsubscribe": 1}
]
for unsubscribe in email_unsubscribe:
if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
doc = frappe.new_doc("Email Unsubscribe")
doc.update(unsubscribe)
doc.insert(ignore_permissions=True)

View file

@ -0,0 +1,6 @@
import frappe
def execute():
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
site_url = frappe.utils.get_site_url(frappe.local.site)
frappe.db.sql("""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url))

View file

@ -250,6 +250,8 @@
"public/less/form_grid.less"
],
"js/form.min.js": [
"public/js/frappe/form/templates/address_list.html",
"public/js/frappe/form/templates/contact_list.html",
"public/js/frappe/form/templates/print_layout.html",
"public/js/frappe/form/templates/users_in_sidebar.html",
"public/js/frappe/form/templates/set_sharing.html",

View file

@ -1,82 +1,64 @@
/* csslint ignore:start */
/* palette colors*/
body {
line-height: 1.5;
color: #36414c;
}
p {
margin: 1em 0 !important;
}
hr {
border-top: 1px solid #d1d8dd;
}
.body-table {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.body-table td {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.email-header,
.email-body,
.email-footer {
width: 100% !important;
min-width: 100% !important;
}
.email-body {
font-size: 14px;
}
.email-footer {
border-top: 1px solid #d1d8dd;
font-size: 12px;
}
.email-header {
border: 1px solid #d1d8dd;
border-radius: 4px 4px 0 0;
}
.email-header .brand-image {
width: 24px;
height: 24px;
display: block;
}
.email-header-title {
font-weight: bold;
}
.body-table.has-header .email-body {
border: 1px solid #d1d8dd;
border-radius: 0 0 4px 4px;
border-top: none;
}
.body-table.has-header .email-footer {
border-top: none;
}
.email-footer-container {
margin-top: 30px;
}
.email-footer-container > div:not(:last-child) {
margin-bottom: 5px;
}
.email-unsubscribe a {
color: #8d99a6;
text-decoration: underline;
}
.btn {
text-decoration: none;
padding: 7px 10px;
@ -84,24 +66,20 @@ hr {
border: 1px solid;
border-radius: 3px;
}
.btn.btn-default {
color: #fff;
background-color: #f0f4f7;
border-color: transparent;
}
.btn.btn-primary {
color: #fff;
background-color: #5e64ff;
border-color: #444bff;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table td,
.table th {
padding: 8px;
@ -110,68 +88,53 @@ hr {
border-top: 1px solid #d1d8dd;
text-align: left;
}
.table th {
font-weight: bold;
}
.table > thead > tr > th {
vertical-align: middle;
border-bottom: 2px solid #d1d8dd;
}
.table > thead:first-child > tr:first-child > th {
border-top: none;
}
.table.table-bordered {
border: 1px solid #d1d8dd;
}
.table.table-bordered td,
.table.table-bordered th {
border: 1px solid #d1d8dd;
}
.more-info {
font-size: 80% !important;
color: #8d99a6 !important;
border-top: 1px solid #ebeff2;
padding-top: 10px;
}
.text-right {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
.text-muted {
color: #8d99a6 !important;
}
.text-extra-muted {
color: #d1d8dd !important;
}
.text-regular {
font-size: 14px;
}
.text-medium {
font-size: 12px;
}
.text-small {
font-size: 10px;
}
.text-bold {
font-weight: bold;
}
.indicator {
width: 8px;
height: 8px;
@ -180,43 +143,33 @@ hr {
display: inline-block;
margin-right: 5px;
}
.indicator.indicator-blue {
background-color: #5e64ff;
}
.indicator.indicator-green {
background-color: #98d85b;
}
.indicator.indicator-orange {
background-color: #ffa00a;
}
.indicator.indicator-red {
background-color: #ff5858;
}
.indicator.indicator-yellow {
background-color: #feef72;
}
.screenshot {
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #d1d8dd;
margin: 8px 0;
max-width: 100%;
}
.list-unstyled {
list-style-type: none;
padding: 0;
}
/* auto email report */
.report-title {
margin-bottom: 20px;
}
/* csslint ignore:end */

View file

@ -81,7 +81,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
</span>`;
return {
id: frappe.utils.get_random(6),
name: col.header_title || df.label,
name: col.header_title || (df ? df.label : 'Untitled Column'),
content: column_title,
skip_import: true,
editable: false,

View file

@ -38,6 +38,7 @@ import './table_multiselect';
import './multiselect_pills';
import './multiselect_list';
import './rating';
import './duration';
frappe.ui.form.make_control = function (opts) {
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");

View file

@ -0,0 +1,152 @@
frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
make_input: function() {
this._super();
this.make_picker();
},
make_picker: function() {
this.inputs = [];
this.set_duration_options();
this.$picker = $(
`<div class="duration-picker">
<div class="picker-row row"></div>
</div>`
);
this.$wrapper.append(this.$picker);
this.build_numeric_input("days", !this.duration_options.show_days);
this.build_numeric_input("hours", false);
this.build_numeric_input("minutes", false);
this.build_numeric_input("seconds", !this.duration_options.show_seconds);
this.set_duration_picker_value(this.value);
this.$picker.hide();
this.bind_events();
this.refresh();
},
build_numeric_input: function(label, hidden, max) {
let $duration_input = $(`
<input class="input-sm duration-input" data-duration="${label}" type="number" min="0" value="0">
`);
let $input = $(`<div class="row duration-row"></div>`).prepend($duration_input);
if (max) {
$duration_input.attr("max", max);
}
this.inputs[label] = $duration_input;
let $control = $(`
<div class="col duration-col">
<div class="row duration-row duration-label">${__(label)}</div>
</div>`
);
if (hidden) {
$control.addClass("hidden");
}
$control.prepend($input);
$control.appendTo(this.$picker.find(".picker-row"));
},
set_duration_options() {
this.duration_options = frappe.utils.get_duration_options(this.df);
},
set_duration_picker_value: function(value) {
let total_duration = frappe.utils.seconds_to_duration(value, this.duration_options);
if (this.$picker) {
Object.keys(total_duration).forEach(duration => {
this.inputs[duration].prop("value", total_duration[duration]);
});
}
},
bind_events: function() {
// flag to handle the display property of the picker
let clicked = false;
this.$wrapper.find(".duration-input").mousedown(() => {
// input in individual duration boxes
clicked = true;
});
this.$picker.on("change", ".duration-input", () => {
// duration changed in individual boxes
clicked = false;
let duration = this.get_duration();
let value = frappe.utils.duration_to_seconds(
duration.days,
duration.hours,
duration.minutes,
duration.seconds
);
this.set_value(value);
this.set_focus();
});
this.$input.on("focus", () => {
this.$picker.show();
let is_picker_set = this.is_duration_picker_set(this.inputs);
if (!is_picker_set) {
this.set_duration_picker_value(this.value);
}
});
this.$input.on("blur", () => {
// input in duration boxes, don't close the picker
if (clicked) {
clicked = false;
} else {
// blur event was not due to duration inputs
this.$picker.hide();
}
});
},
get_value() {
return cint(this.value);
},
refresh_input: function() {
this._super();
this.set_duration_options();
this.set_duration_picker_value(this.value);
},
format_for_input: function(value) {
return frappe.utils.get_formatted_duration(value, this.duration_options);
},
get_duration() {
// returns an object of days, hours, minutes and seconds from the inputs array
let total_duration = {
minutes: 0,
hours: 0,
days: 0,
seconds: 0
};
if (this.inputs) {
total_duration.minutes = parseInt(this.inputs.minutes.val());
total_duration.hours = parseInt(this.inputs.hours.val());
if (this.duration_options.show_days) {
total_duration.days = parseInt(this.inputs.days.val());
}
if (this.duration_options.show_seconds) {
total_duration.seconds = parseInt(this.inputs.seconds.val());
}
}
return total_duration;
},
is_duration_picker_set(inputs) {
let is_set = false;
Object.values(inputs).forEach(duration => {
if (duration.prop("value") != 0) {
is_set = true;
}
});
return is_set;
}
});

View file

@ -129,7 +129,8 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte
get_data() {
let data;
if(this.df.get_data) {
data = this.df.get_data();
let txt = this.$input.val();
data = this.df.get_data(txt);
if (data && data.then) {
data.then((r) => {
this.set_data(r);

View file

@ -589,7 +589,6 @@ frappe.ui.form.Timeline = class Timeline {
out.push(me.get_version_comment(version, message));
}
} else {
p = p.map(frappe.utils.escape_html);
const df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
if (df && !df.hidden) {
const field_display_status = frappe.perm.get_field_display_status(df, null,
@ -597,8 +596,8 @@ frappe.ui.form.Timeline = class Timeline {
if (field_display_status === 'Read' || field_display_status === 'Write') {
parts.push(__('{0} from {1} to {2}', [
__(df.label),
(frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(),
(frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold()
me.format_content_for_timeline(p[1]),
me.format_content_for_timeline(p[2])
]));
}
}
@ -608,9 +607,9 @@ frappe.ui.form.Timeline = class Timeline {
if (parts.length) {
let message;
if (updater_reference_link) {
message = __("changed value of {0} {1}", [parts.join(', ').bold(), updater_reference_link]);
message = __("changed value of {0} {1}", [parts.join(', '), updater_reference_link]);
} else {
message = __("changed value of {0}", [parts.join(', ').bold()]);
message = __("changed value of {0}", [parts.join(', ')]);
}
out.push(me.get_version_comment(version, message));
}
@ -618,23 +617,23 @@ frappe.ui.form.Timeline = class Timeline {
// value changed in table field
if (data.row_changed && data.row_changed.length) {
var parts = [], count = 0;
let parts = [];
data.row_changed.every(function(row) {
row[3].every(function(p) {
var df = me.frm.fields_dict[row[0]] &&
frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype,
p[0], me.frm.docname);
if(df && !df.hidden) {
if (df && !df.hidden) {
var field_display_status = frappe.perm.get_field_display_status(df,
null, me.frm.perm);
if(field_display_status === 'Read' || field_display_status === 'Write') {
if (field_display_status === 'Read' || field_display_status === 'Write') {
parts.push(__('{0} from {1} to {2} in row #{3}', [
frappe.meta.get_label(me.frm.fields_dict[row[0]].grid.doctype,
p[0]),
(frappe.ellipsis(p[1], 40) || '""').bold(),
(frappe.ellipsis(p[2], 40) || '""').bold(),
me.format_content_for_timeline(p[1]),
me.format_content_for_timeline(p[2]),
row[1]
]));
}
@ -657,20 +656,22 @@ frappe.ui.form.Timeline = class Timeline {
// rows added / removed
// __('added'), __('removed') # for translation, don't remove
['added', 'removed'].forEach(function(key) {
if(data[key] && data[key].length) {
parts = (data[key] || []).map(function(p) {
if (data[key] && data[key].length) {
let parts = (data[key] || []).map(function(p) {
var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname);
if(df && !df.hidden) {
if (df && !df.hidden) {
var field_display_status = frappe.perm.get_field_display_status(df, null,
me.frm.perm);
if(field_display_status === 'Read' || field_display_status === 'Write') {
if (field_display_status === 'Read' || field_display_status === 'Write') {
return frappe.meta.get_label(me.frm.doctype, p[0])
}
}
});
parts = parts.filter(function(p) { return p; });
if(parts.length) {
parts = parts.filter(function(p) {
return p;
});
if (parts.length) {
out.push(me.get_version_comment(version, __("{0} rows for {1}",
[__(key), parts.join(', ')])));
}
@ -717,6 +718,17 @@ frappe.ui.form.Timeline = class Timeline {
}
format_content_for_timeline(content) {
// text to HTML
// limits content to 40 characters
// escapes HTML
// and makes it bold
content = frappe.utils.html2text(content);
content = frappe.ellipsis(content, 40) || '""';
content = frappe.utils.escape_html(content);
return content.bold();
}
delete_comment(name) {
var me = this;

View file

@ -650,7 +650,14 @@ frappe.ui.form.Form = class FrappeForm {
frappe.utils.play_sound("submit");
callback && callback();
me.script_manager.trigger("on_submit")
.then(() => resolve(me));
.then(() => resolve(me))
.then(() => {
if (frappe.route_hooks.after_submit) {
let route_callback = frappe.route_hooks.after_submit;
delete frappe.route_hooks.after_submit;
route_callback(me);
}
});
}
}, btn, () => me.handle_save_fail(btn, on_error), resolve);
});
@ -1589,7 +1596,7 @@ frappe.ui.form.Form = class FrappeForm {
let steps = frappe.tour[this.doctype].map(step => {
let field = this.get_docfield(step.fieldname);
return {
element: `.frappe-control[title='${step.fieldname}']`,
element: `.frappe-control[data-fieldname='${step.fieldname}']`,
popover: {
title: step.title || field.label,
description: step.description

View file

@ -142,10 +142,7 @@ frappe.form.formatters = {
},
DateRange: function(value) {
if($.isArray(value)) {
return __("{0} to {1}", [
frappe.datetime.str_to_user(value[0]),
frappe.datetime.str_to_user(value[1])
]);
return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
} else {
return value || "";
}
@ -188,6 +185,14 @@ frappe.form.formatters = {
return value || "";
},
Duration: function(value, docfield) {
if (value) {
let duration_options = frappe.utils.get_duration_options(docfield);
value = frappe.utils.get_formatted_duration(value, duration_options);
}
return value || "";
},
LikedBy: function(value) {
var html = "";
$.each(JSON.parse(value || "[]"), function(i, v) {

View file

@ -599,12 +599,15 @@ frappe.ui.form.Section = Class.extend({
if(this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
if (this.df.hide_border) {
this.wrapper.toggleClass("hide-border", true);
}
}
// for bc
this.body = $('<div class="section-body">').appendTo(this.wrapper);
},
make_head: function() {
var me = this;
if(!this.df.collapsible) {
@ -663,9 +666,11 @@ frappe.ui.form.Section = Class.extend({
}
});
},
is_collapsed() {
return this.body.hasClass('hide');
},
has_missing_mandatory: function() {
var missing_mandatory = false;
for (var j=0, l=this.fields_list.length; j < l; j++) {

View file

@ -1,110 +1,62 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.MultiSelectDialog = Class.extend({
init: function(opts) {
/* Options: doctype, target, setters, get_query, action */
$.extend(this, opts);
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
constructor(opts) {
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
Object.assign(this, opts);
var me = this;
if(this.doctype!="[Select]") {
frappe.model.with_doctype(this.doctype, function(r) {
if (this.doctype != "[Select]") {
frappe.model.with_doctype(this.doctype, function () {
me.make();
});
} else {
this.make();
}
},
make: function() {
let me = this;
}
make() {
let me = this;
this.page_length = 20;
this.start = 0;
let fields = this.get_primary_filters();
let fields = [
{
fieldtype: "Data",
label: __("Search Term"),
fieldname: "search_term"
},
{
fieldtype: "Column Break"
}
];
let count = 0;
if(!this.date_field) {
this.date_field = "transaction_date";
}
// setters can be defined as a dict or a list of fields
// setters define the additional filters that get applied
// for selection
// CASE 1: DocType name and fieldname is the same, example "customer" and "customer"
// setters define the filters applied in the modal
// if the fieldnames and doctypes are consistently named,
// pass a dict with the setter key and value, for example
// {customer: [customer_name]}
// CASE 2: if the fieldname of the target is different,
// then pass a list of fields with appropriate fieldname
if($.isArray(this.setters)) {
for (let df of this.setters) {
fields.push(df, {fieldtype: "Column Break"});
}
} else {
Object.keys(this.setters).forEach(function(setter) {
fields.push({
fieldtype: me.target.fields_dict[setter].df.fieldtype,
label: me.target.fields_dict[setter].df.label,
fieldname: setter,
options: me.target.fields_dict[setter].df.options,
default: me.setters[setter]
});
if (count++ < Object.keys(me.setters).length) {
fields.push({fieldtype: "Column Break"});
}
});
}
// Make results area
fields = fields.concat([
{
"fieldname":"date_range",
"label": __("Date Range"),
"fieldtype": "DateRange",
},
{ fieldtype: "Section Break" },
{ fieldtype: "HTML", fieldname: "results_area" },
{ fieldtype: "Button", fieldname: "more_btn", label: __("More"),
click: function(){
me.start += 20;
frappe.flags.auto_scroll = true;
me.get_results();
{
fieldtype: "Button", fieldname: "more_btn", label: __("More"),
click: () => {
this.start += 20;
this.get_results();
}
}
]);
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
: this.doctype.slice(0, -1) + 'ies';
// Custom Data Fields
if (this.data_fields) {
fields.push({ fieldtype: "Section Break" });
fields = fields.concat(this.data_fields);
}
let doctype_plural = this.doctype.plural();
this.dialog = new frappe.ui.Dialog({
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]),
fields: fields,
primary_action_label: __("Get Items"),
primary_action_label: this.primary_action_label || __("Get Items"),
secondary_action_label: __("Make {0}", [me.doctype]),
primary_action: function() {
me.action(me.get_checked_values(), me.args);
primary_action: function () {
let filters_data = me.get_custom_filters();
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data);
},
secondary_action: function(e) {
secondary_action: function (e) {
// If user wants to close the modal
if (e) {
frappe.route_options = {};
if($.isArray(me.setters)) {
if (Array.isArray(me.setters)) {
for (let df of me.setters) {
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
}
} else {
Object.keys(me.setters).forEach(function(setter) {
Object.keys(me.setters).forEach(function (setter) {
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
});
}
@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
}
});
if (this.add_filters_group) {
this.make_filter_area();
}
this.$parent = $(this.dialog.body);
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
this.bind_events();
this.get_results();
this.dialog.show();
},
}
bind_events: function() {
get_primary_filters() {
let fields = [];
let columns = new Array(3);
// Hack for three column layout
// To add column break
columns[0] = [
{
fieldtype: "Data",
label: __("Search"),
fieldname: "search_term"
}
];
columns[1] = [];
columns[2] = [];
Object.keys(this.setters).forEach((setter, index) => {
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
// Index + 1 to start filling from index 1
// Since Search is a standrd field already pushed
columns[(index + 1) % 3].push({
fieldtype: df_prop.fieldtype,
label: df_prop.label,
fieldname: setter,
options: df_prop.options,
default: this.setters[setter]
});
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
if (Object.seal) {
Object.seal(columns);
// now a is a fixed-size array with mutable entries
}
fields = [
...columns[0],
{ fieldtype: "Column Break" },
...columns[1],
{ fieldtype: "Column Break" },
...columns[2],
{ fieldtype: "Section Break", fieldname: "primary_filters_sb" }
];
if (this.add_filters_group) {
fields.push(
{
fieldtype: 'HTML',
fieldname: 'filter_area',
}
);
}
return fields;
}
make_filter_area() {
this.filter_group = new frappe.ui.FilterGroup({
parent: this.dialog.get_field('filter_area').$wrapper,
doctype: this.doctype,
on_change: () => {
this.get_results();
}
});
}
get_custom_filters() {
if (this.add_filters_group && this.filter_group) {
return this.filter_group.get_filters().reduce((acc, filter) => {
return Object.assign(acc, {
[filter[1]]: [filter[2], filter[3]]
});
}, {});
} else {
return [];
}
}
bind_events() {
let me = this;
this.$results.on('click', '.list-item-container', function (e) {
@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
$(this).find(':checkbox').trigger('click');
}
});
this.$results.on('click', '.list-item--head :checkbox', (e) => {
this.$results.find('.list-item-container .list-row-check')
.prop("checked", ($(e.target).is(':checked')));
});
this.$parent.find('.input-with-feedback').on('change', (e) => {
this.$parent.find('.input-with-feedback').on('change', () => {
frappe.flags.auto_scroll = false;
this.get_results();
});
this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => {
frappe.flags.auto_scroll = false;
this.get_results();
});
this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => {
this.$parent.find('[data-fieldtype="Data"]').on('input', () => {
var $this = $(this);
clearTimeout($this.data('timeout'));
$this.data('timeout', setTimeout(function() {
$this.data('timeout', setTimeout(function () {
frappe.flags.auto_scroll = false;
me.empty_list();
me.get_results();
}, 300));
});
},
}
get_checked_values: function() {
get_checked_values() {
// Return name of checked value.
return this.$results.find('.list-item-container').map(function() {
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) {
return this.$results.find('.list-item-container').map(function () {
if ($(this).find('.list-row-check:checkbox:checked').length > 0) {
return $(this).attr('data-item-name');
}
}).get();
},
}
get_checked_items: function() {
get_checked_items() {
// Return checked items with all the column values.
let checked_values = this.get_checked_values();
return this.results.filter(res => checked_values.includes(res.name));
},
}
make_list_row: function(result={}) {
make_list_row(result = {}) {
var me = this;
// Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0;
@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
let contents = ``;
let columns = ["name"];
if($.isArray(this.setters)) {
for (let df of this.setters) {
columns.push(df.fieldname);
}
} else {
columns = columns.concat(Object.keys(this.setters));
}
columns.push("Date");
columns = columns.concat(Object.keys(this.setters));
columns.forEach(function(column) {
columns.forEach(function (column) {
contents += `<div class="list-item__content ellipsis">
${
head ? `<span class="ellipsis">${__(frappe.model.unscrub(column))}</span>`
: (column !== "name" ? `<span class="ellipsis">${__(result[column])}</span>`
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id ellipsis">
${__(result[column])}</a>`)
}
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
: (column !== "name" ? `<span class="ellipsis result-row" title="${__(result[column] || '')}">${__(result[column] || '')}</span>`
: `<a href="${"#Form/" + me.doctype + "/" + result[column] || ''}" class="list-id ellipsis" title="${__(result[column] || '')}">
${__(result[column] || '')}</a>`)}
</div>`;
})
});
let $row = $(`<div class="list-item">
<div class="list-item__content" style="flex: 0 0 10px;">
@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
head ? $row.addClass('list-item--head')
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row);
return $row;
},
render_result_list: function(results, more = 0, empty=true) {
$(".modal-dialog .list-item--head").css("z-index", 0);
return $row;
}
render_result_list(results, more = 0, empty = true) {
var me = this;
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
@ -240,44 +265,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
});
if (frappe.flags.auto_scroll) {
this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500);
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
}
},
}
empty_list: function() {
empty_list() {
// Store all checked items
let checked = this.get_checked_items().map(item => {
return {
...item,
checked: true
}
};
});
// Remove **all** items
this.$results.find('.list-item-container').remove();
// Rerender checked items
this.render_result_list(checked, 0, false);
},
}
get_results: function() {
get_results() {
let me = this;
let filters = this.get_query ? this.get_query().filters : {} || {};
let filter_fields = [];
let filters = this.get_query ? this.get_query().filters : {};
let filter_fields = [me.date_field];
if($.isArray(this.setters)) {
for (let df of this.setters) {
filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
me.args[df.fieldname] = filters[df.fieldname];
filter_fields.push(df.fieldname);
}
} else {
Object.keys(this.setters).forEach(function(setter) {
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
Object.keys(this.setters).forEach(function (setter) {
var value = me.dialog.fields_dict[setter].get_value();
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
filters[setter] = ["like", "%" + value + "%"];
} else {
filters[setter] = value || undefined;
me.args[setter] = filters[setter];
filter_fields.push(setter);
});
}
}
});
let date_val = this.dialog.fields_dict["date_range"].get_value();
if(date_val) {
filters[this.date_field] = ['between', date_val];
}
let filter_group = this.get_custom_filters();
Object.assign(filters, filter_group);
let args = {
doctype: me.doctype,
@ -288,13 +313,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
page_length: this.page_length + 1,
query: this.get_query ? this.get_query().query : '',
as_dict: 1
}
};
frappe.call({
type: "GET",
method:'frappe.desk.search.search_widget',
method: 'frappe.desk.search.search_widget',
no_spinner: true,
args: args,
callback: function(r) {
callback: function (r) {
let more = 0;
me.results = [];
if (r.values.length) {
@ -302,30 +327,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
r.values.pop();
more = 1;
}
r.values.forEach(function(result) {
if(me.date_field in result) {
result["Date"] = result[me.date_field]
}
r.values.forEach(function (result) {
result.checked = 0;
result.parsed_date = Date.parse(result["Date"]);
me.results.push(result);
});
me.results.map( (result) => {
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"});
})
me.results.sort((a, b) => {
return a.parsed_date - b.parsed_date;
});
// Preselect oldest entry
if (me.start < 1 && r.values.length === 1) {
me.results[0].checked = 1;
}
}
me.render_result_list(me.results, more);
}
});
},
});
}
};

View file

@ -16,12 +16,22 @@ frappe.ui.form.get_event_handler_list = function(doctype, fieldname) {
frappe.ui.form.on = frappe.ui.form.on_change = function(doctype, fieldname, handler) {
var add_handler = function(fieldname, handler) {
var handler_list = frappe.ui.form.get_event_handler_list(doctype, fieldname);
handler_list.push(handler);
let _handler = (...args) => {
try {
handler(...args);
} catch (error) {
console.error(handler);
throw error;
}
}
handler_list.push(_handler);
// add last handler to events so it can be called as
// frm.events.handler(frm)
if(cur_frm && cur_frm.doctype===doctype) {
cur_frm.events[fieldname] = handler;
cur_frm.events[fieldname] = _handler;
}
}

View file

@ -87,23 +87,17 @@ frappe.ui.form.AssignTo = Class.extend({
if(!me.assign_to) {
me.assign_to = new frappe.ui.form.AssignToDialog({
obj: me,
method: 'frappe.desk.form.assign_to.add',
method: "frappe.desk.form.assign_to.add",
doctype: me.frm.doctype,
docname: me.frm.docname,
callback: function(r) {
frm: me.frm,
callback: function (r) {
me.render(r.message);
}
});
}
me.assign_to.dialog.clear();
if(me.frm.meta.title_field) {
me.assign_to.dialog.set_value("description", me.frm.doc[me.frm.meta.title_field])
}
me.assign_to.dialog.show();
me.assign_to = null;
},
remove: function(owner) {
var me = this;
@ -130,81 +124,126 @@ frappe.ui.form.AssignTo = Class.extend({
frappe.ui.form.AssignToDialog = Class.extend({
init: function(opts){
var me = this
var dialog = new frappe.ui.Dialog({
title: __('Add to To Do'),
fields: [
{ fieldtype: 'Link', fieldname: 'assign_to', options: 'User', label: __("Assign To"), reqd: true, filters: { 'user_type': 'System User' }},
{ fieldtype: 'Check', fieldname: 'myself', label: __("Assign to me"), "default": 0 },
{ fieldtype: 'Small Text', fieldname: 'description', label: __("Comment") },
{ fieldtype: 'Section Break' },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Date', fieldname: 'date', label: __("Complete By") },
{ fieldtype: 'Column Break' },
{ fieldtype: 'Select', fieldname: 'priority', label: __("Priority"),
options: [
{ value: 'Low', label: __('Low') },
{ value: 'Medium', label: __('Medium') },
{ value: 'High', label: __('High') }
],
// Pick up priority from the source document, if it exists and is available in ToDo
'default': ["Low", "Medium", "High"].includes(opts.obj.frm && opts.obj.frm.doc.priority
? opts.obj.frm.doc.priority : 'Medium')
},
],
primary_action: function() { frappe.ui.add_assignment(opts, this) },
primary_action_label: __("Add")
})
$.extend(me, dialog);
$.extend(this, opts);
me.dialog = dialog;
me.dialog.fields_dict.assign_to.get_query = "frappe.core.doctype.user.user.user_query";
var myself = me.dialog.get_input("myself").on("click", function() {
me.toggle_myself(this);
});
me.toggle_myself(myself);
},
toggle_myself: function(myself) {
var me = this;
if($(myself).prop("checked")) {
me.dialog.set_value("assign_to", frappe.session.user);
me.dialog.get_field("notify").$wrapper.toggle(false);
me.dialog.get_field("assign_to").$wrapper.toggle(false);
} else {
me.dialog.set_value("assign_to", "");
me.dialog.get_field("assign_to").$wrapper.toggle(true);
}
this.make();
this.set_description_from_doc();
},
make: function() {
let me = this;
});
me.dialog = new frappe.ui.Dialog({
title: __('Add to ToDo'),
fields: me.get_fields(),
primary_action_label: __("Add"),
primary_action: function() {
let args = me.dialog.get_values();
frappe.ui.add_assignment = function(opts, dialog) {
var assign_to = dialog.fields_dict.assign_to.get_value();
var args = dialog.get_values();
if(args && assign_to) {
dialog.set_message('Assigning...');
return frappe.call({
method: opts.method,
args: $.extend(args, {
doctype: opts.doctype,
name: opts.docname,
assign_to: assign_to,
bulk_assign: opts.bulk_assign || false,
re_assign: opts.re_assign || false
}),
btn: dialog.get_primary_btn(),
callback: function(r) {
if(!r.exc) {
if(opts.callback){
opts.callback(r);
}
dialog && dialog.hide();
} else {
dialog.clear_message();
if (args && args.assign_to) {
me.dialog.set_message("Assigning...");
frappe.call({
method: me.method,
args: $.extend(args, {
doctype: me.doctype,
name: me.docname,
assign_to: args.assign_to,
bulk_assign: me.bulk_assign || false,
re_assign: me.re_assign || false
}),
btn: me.dialog.get_primary_btn(),
callback: function(r) {
if (!r.exc) {
if (me.callback) {
me.callback(r);
}
me.dialog && me.dialog.hide();
} else {
me.dialog.clear_message();
}
},
});
}
},
});
},
assign_to_me: function() {
let me = this;
let assign_to = [];
if (me.dialog.get_value("assign_to_me")) {
assign_to.push(frappe.session.user);
}
me.dialog.set_value("assign_to", assign_to);
},
set_description_from_doc: function() {
let me = this;
if (me.frm && me.frm.meta.title_field) {
me.dialog.set_value("description", me.frm.doc[me.frm.meta.title_field]);
}
},
get_fields: function() {
let me = this;
return [
{
fieldtype: 'MultiSelectPills',
fieldname: 'assign_to',
label: __("Assign To"),
reqd: true,
get_data: function(txt) {
return frappe.db.get_link_options("User", txt, {user_type: "System User", enabled: 1});
}
},
{
label: __("Assign to me"),
fieldtype: 'Check',
fieldname: 'assign_to_me',
default: 0,
onchange: () => me.assign_to_me()
},
{
label: __("Comment"),
fieldtype: 'Small Text',
fieldname: 'description'
},
{
fieldtype: 'Section Break'
},
{
fieldtype: 'Column Break'
},
{
label: __("Complete By"),
fieldtype: 'Date',
fieldname: 'date'
},
{
fieldtype: 'Column Break'
},
{
label: __("Priority"),
fieldtype: 'Select',
fieldname: 'priority',
options: [
{
value: 'Low',
label: __('Low')
},
{
value: 'Medium',
label: __('Medium')
},
{
value: 'High',
label: __('High')
}
],
// Pick up priority from the source document, if it exists and is available in ToDo
default: ["Low", "Medium", "High"].includes(me.frm && me.frm.doc.priority ? me.frm.doc.priority : 'Medium')
}
];
}
}
});

View file

@ -0,0 +1,22 @@
<div class="clearfix"></div>
{% for(var i=0, l=addr_list.length; i<l; i++) { %}
<div class="address-box">
<p class="h6">
{%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
<span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
{% if(addr_list[i].is_primary_address) { %}
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
{% if(addr_list[i].is_shipping_address) { %}
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
<a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
style="margin-top:-3px; margin-right: -5px;">
{%= __("Edit") %}</a>
</p>
<p>{%= addr_list[i].display %}</p>
</div>
{% } %}
{% if(!addr_list.length) { %}
<p class="text-muted small">{%= __("No address added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>

View file

@ -0,0 +1,54 @@
<div class="clearfix"></div>
{% for(var i=0, l=contact_list.length; i<l; i++) { %}
<div class="address-box">
<p class="h6">
{%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
{% if(contact_list[i].is_primary_contact) { %}
<span class="text-muted">({%= __("Primary") %})</span>
{% } %}
{% if(contact_list[i].designation){ %}
<span class="text-muted">&ndash; {%= contact_list[i].designation %}</span>
{% } %}
<a href="#Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
class="btn btn-xs btn-default pull-right"
style="margin-top:-3px; margin-right: -5px;">
{%= __("Edit") %}</a>
</p>
{% if (contact_list[i].phones || contact_list[i].email_ids) { %}
<p>
{% if(contact_list[i].phone) { %}
{%= __("Phone") %}: {%= contact_list[i].phone %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% endif %}
{% if(contact_list[i].mobile_no) { %}
{%= __("Mobile No") %}: {%= contact_list[i].mobile_no %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% endif %}
{% if(contact_list[i].phone_nos) { %}
{% for(var j=0, k=contact_list[i].phone_nos.length; j<k; j++) { %}
{%= __("Phone") %}: {%= contact_list[i].phone_nos[j].phone %}<br>
{% } %}
{% endif %}
</p>
<p>
{% if(contact_list[i].email_id) { %}
{%= __("Email") %}: {%= contact_list[i].email_id %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
{% endif %}
{% if(contact_list[i].email_ids) { %}
{% for(var j=0, k=contact_list[i].email_ids.length; j<k; j++) { %}
{%= __("Email") %}: {%= contact_list[i].email_ids[j].email_id %}<br>
{% } %}
{% endif %}
</p>
{% endif %}
<p>
{% if (contact_list[i].address) { %}
{%= __("Address") %}: {%= contact_list[i].address %}<br>
{% endif %}
</p>
</div>
{% } %}
{% if(!contact_list.length) { %}
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
{% } %}
<p><button class="btn btn-xs btn-default btn-contact">
{{ __("New Contact") }}</button>
</p>

View file

@ -103,7 +103,11 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
frappe.utils.setup_search(dropdown, '.group-by-item', '.group-by-value', 'data-name');
} else {
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
dropdown.html(
`<div class="list-loading text-center group-by-empty text-muted">
${__("No filters found")}
</div>`
);
}
});
});

View file

@ -139,7 +139,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
show_restricted_list_indicator_if_applicable() {
const match_rules_list = frappe.perm.get_match_rules(this.doctype);
if (match_rules_list.length) {
this.restricted_list = $(`<button class="restricted-list form-group">${__('Restricted')}</button>`)
this.restricted_list = $(`<button class="restricted-button">${__('Restricted')}</button>`)
.prepend('<span class="octicon octicon-lock"></span>')
.click(() => this.show_restrictions(match_rules_list))
.appendTo(this.page.page_form);

View file

@ -161,8 +161,7 @@ $.extend(frappe.meta, {
if(!out) {
// eslint-disable-next-line
console.log(__('Warning: Unable to find {0} in any table related to {1}', [
key, __(doctype)]));
console.log(__('Warning: Unable to find {0} in any table related to {1}', [key, __(doctype)]));
}
}
return out;
@ -266,5 +265,5 @@ $.extend(frappe.meta, {
precision = cint(frappe.defaults.get_default("float_precision")) || 3;
}
return precision;
},
}
});

View file

@ -114,8 +114,8 @@ export default {
{label: "Time", slug: "time", sortable: true},
],
query: {
sort: "time",
order: "asc",
sort: "duration",
order: "desc",
filters: {},
pagination: {
limit: 20,

View file

@ -79,7 +79,7 @@
<span class="octicon octicon-triangle-down"></span></a>
</div>
</div>
<div class="form-in-grid" v-if="showing == call.index">
<div class="recorder-form-in-grid" v-if="showing == call.index">
<div class="grid-form-heading" @click="showing = null">
<div class="toolbar grid-header-toolbar">
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ call.index }}</span></span>
@ -216,8 +216,8 @@ export default {
{label: "Exact Copies", slug: "exact_copies", sortable: true},
],
query: {
sort: "index",
order: "asc",
sort: "duration",
order: "desc",
pagination: {
limit: 20,
page: 1,

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