Merge branch 'develop' into no-ammend
This commit is contained in:
commit
bd1a330a31
194 changed files with 4406 additions and 1486 deletions
22
.travis.yml
22
.travis.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
45
cypress/integration/control_duration.js
Normal file
45
cypress/integration/control_duration.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
0
frappe/core/doctype/installed_applications/__init__.py
Normal file
0
frappe/core/doctype/installed_applications/__init__.py
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.restricted-button {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
right: -5px;
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 <script> or just characters like < or >, 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 <script> or just characters like < or >, 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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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>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>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",
|
||||
|
|
|
|||
0
frappe/custom/doctype/package_document_type/__init__.py
Normal file
0
frappe/custom/doctype/package_document_type/__init__.py
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
0
frappe/custom/doctype/package_publish_target/__init__.py
Normal file
0
frappe/custom/doctype/package_publish_target/__init__.py
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
0
frappe/custom/doctype/package_publish_tool/__init__.py
Normal file
0
frappe/custom/doctype/package_publish_tool/__init__.py
Normal 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();
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
14
frappe/integrations/frappe_providers/__init__.py
Normal file
14
frappe/integrations/frappe_providers/__init__.py
Normal 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)
|
||||
268
frappe/integrations/frappe_providers/frappecloud.py
Normal file
268
frappe/integrations/frappe_providers/frappecloud.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
13
frappe/patches/v13_0/email_unsubscribe.py
Normal file
13
frappe/patches/v13_0/email_unsubscribe.py
Normal 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)
|
||||
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal file
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal 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))
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
|
|||
152
frappe/public/js/frappe/form/controls/duration.js
Normal file
152
frappe/public/js/frappe/form/controls/duration.js
Normal 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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
22
frappe/public/js/frappe/form/templates/address_list.html
Normal file
22
frappe/public/js/frappe/form/templates/address_list.html
Normal 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>
|
||||
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal file
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal 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">– {%= 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>
|
||||
|
|
@ -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>`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue