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

This commit is contained in:
Suraj Shetty 2020-10-01 13:10:37 +05:30
commit ba29d8d8b4
297 changed files with 5998 additions and 4254 deletions

View file

@ -7,22 +7,28 @@ start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
# skip first argument
files = sys.argv[1:]
for _file in files:
if not _file.endswith(('.py', '.js')):
continue
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
for _file in files_to_scan:
with open(_file, 'r') as f:
print(f'Checking: {_file}')
for num, line in enumerate(f, 1):
all_matches = start_pattern.finditer(line)
if all_matches:
for match in all_matches:
verify = pattern.search(line)
if not verify:
errors_encounter += 1
print(f'A syntax error has been discovered at line number: {num}')
print(f'Syntax error occurred with: {line}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
start_matches = start_pattern.search(line)
if start_matches:
match = pattern.search(line)
if not match and line.endswith(',\n'):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1:])
line = line[start_matches.start() + 1:]
match = pattern.match(line)
if not match:
errors_encounter += 1
print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}')
if errors_encounter > 0:
print('You can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
assert 1+1 == 3
print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
sys.exit(1)
else:
print('Good To Go!')
print('\nGood To Go!')

View file

@ -0,0 +1,43 @@
name: Build and Publish Assets for Development
on:
push:
branches: [ develop ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
path: 'frappe'
- uses: actions/setup-node@v1
with:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
run: |
npm install -g yarn
pip3 install -U frappe-bench
bench init frappe-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --frappe-path $GITHUB_WORKSPACE/frappe
cd frappe-bench && bench build
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
- name: Publish assets to S3
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read
env:
AWS_S3_BUCKET: 'assets.frappeframework.com'
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ASSETS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_ASSETS_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: 'http://s3.fr-par.scw.cloud'
AWS_REGION: 'fr-par'
SOURCE_DIR: '$GITHUB_WORKSPACE/build'

View file

@ -0,0 +1,47 @@
name: Build and Publish Assets built for Releases
on:
release:
types: [ created ]
env:
GITHUB_TOKEN: ${{ github.token }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
path: 'frappe'
- uses: actions/setup-node@v1
with:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
run: |
npm install -g yarn
pip3 install -U frappe-bench
bench init frappe-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --frappe-path $GITHUB_WORKSPACE/frappe
cd frappe-bench && bench build
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
- name: Get release
id: get_release
uses: bruceadams/get-release@v1.2.0
- name: Upload built Assets to Release
uses: actions/upload-release-asset@v1.0.2
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: build/assets.tar.gz
asset_name: assets.tar.gz
asset_content_type: application/octet-stream

34
.snyk
View file

@ -65,3 +65,37 @@ patch:
patched: '2020-04-30T23:02:32.330Z'
- quill-image-resize > lodash:
patched: '2020-08-24T23:06:37.710Z'
- node-sass > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > sass-graph > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > gaze > globule > lodash:
patched: '2020-09-15T23:06:41.931Z'
- snyk > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-php-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'

View file

@ -59,15 +59,18 @@ context('Recorder', () => {
cy.get('.title-text').should('contain', 'DocType');
cy.get('.list-count').should('contain', '20 of ');
cy.visit('/desk#recorder');
// temporarily commenting out theses tests as they seem to be
// randomly failing maybe due a backround event
cy.get('.list-row-container span').contains('frappe.desk.reportview.get').click();
// cy.visit('/desk#recorder');
cy.location('hash').should('contain', '#recorder/request/');
cy.get('form').should('contain', 'frappe.desk.reportview.get');
// cy.get('.list-row-container span').contains('/api/method/frappe').click();
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
cy.location('hash').should('eq', '#recorder');
// cy.location('hash').should('contain', '#recorder/request/');
// cy.get('form').should('contain', '/api/method/frappe');
// cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
// cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
// cy.location('hash').should('eq', '#recorder');
});
});

View file

@ -10,7 +10,7 @@ from six import iteritems, binary_type, text_type, string_types, PY2
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
from past.builtins import cmp
import click
from faker import Faker
# public
@ -182,6 +182,7 @@ def init(site, sites_path=None, new_site=False):
local.meta_cache = {}
local.form_dict = _dict()
local.session = _dict()
local.dev_server = os.environ.get('DEV_SERVER', False)
setup_module_map()
@ -225,12 +226,20 @@ def get_site_config(sites_path=None, site_path=None):
if sites_path:
common_site_config = os.path.join(sites_path, "common_site_config.json")
if os.path.exists(common_site_config):
config.update(get_file_json(common_site_config))
try:
config.update(get_file_json(common_site_config))
except Exception as error:
click.secho("common_site_config.json is invalid", fg="red")
print(error)
if site_path:
site_config = os.path.join(site_path, "site_config.json")
if os.path.exists(site_config):
config.update(get_file_json(site_config))
try:
config.update(get_file_json(site_config))
except Exception as error:
click.secho("{0}/site_config.json is invalid".format(local.site), fg="red")
print(error)
elif local.site and not local.flags.new_site:
raise IncorrectSitePath("{0} does not exist".format(local.site))
@ -513,12 +522,15 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
whitelisted = []
guest_methods = []
xss_safe_methods = []
def whitelist(allow_guest=False, xss_safe=False):
allowed_http_methods_for_whitelisted_func = {}
def whitelist(allow_guest=False, xss_safe=False, methods=None):
"""
Decorator for whitelisting a function and making it accessible via HTTP.
Standard request will be `/api/method/[path.to.method]`
:param allow_guest: Allow non logged-in user to access this method.
:param methods: Allowed http method to access the method.
Use as:
@ -526,10 +538,16 @@ def whitelist(allow_guest=False, xss_safe=False):
def myfunc(param1, param2):
pass
"""
if not methods:
methods = ['GET', 'POST', 'PUT', 'DELETE']
def innerfn(fn):
global whitelisted, guest_methods, xss_safe_methods
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
whitelisted.append(fn)
allowed_http_methods_for_whitelisted_func[fn] = methods
if allow_guest:
guest_methods.append(fn)
@ -1109,8 +1127,8 @@ def get_newargs(fn, kwargs):
if (a in fnargs) or varkw:
newargs[a] = kwargs.get(a)
if "flags" in newargs:
del newargs["flags"]
newargs.pop("ignore_permissions", None)
newargs.pop("flags", None)
return newargs

View file

@ -193,7 +193,8 @@ def handle_exception(e):
else:
traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
if frappe.local.flags.disable_traceback:
# disable traceback in production if flag is set
if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
traceback = ""
frappe.respond_as_web_page("Server Error",

View file

@ -146,7 +146,7 @@ class AutoRepeat(Document):
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)
@ -403,6 +403,7 @@ def update_reference(docname, reference):
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
frappe.has_permission("Auto Repeat", "write", throw=True)
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})

View file

@ -11,24 +11,141 @@ import warnings
import tempfile
from distutils.spawn import find_executable
from six import iteritems, text_type
import frappe
from frappe.utils.minify import JavascriptMinify
import click
from requests import get
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
timestamps = {}
app_paths = None
sites_path = os.path.abspath(os.getcwd())
def download_file(url, prefix):
filename = urlparse(url).path.split("/")[-1]
local_filename = os.path.join(prefix, filename)
with get(url, stream=True, allow_redirects=True) as r:
r.raise_for_status()
with open(local_filename, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return local_filename
def build_missing_files():
# check which files dont exist yet from the build.json and tell build.js to build only those!
missing_assets = []
current_asset_files = []
for type in ["css", "js"]:
current_asset_files.extend(
[
"{0}/{1}".format(type, name)
for name in os.listdir(os.path.join(sites_path, "assets", type))
]
)
with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f:
all_asset_files = json.load(f).keys()
for asset in all_asset_files:
if asset.replace("concat:", "") not in current_asset_files:
missing_assets.append(asset)
if missing_assets:
from subprocess import check_call
from shlex import split
click.secho("\nBuilding missing assets...\n", fg="yellow")
command = split(
"node rollup/build.js --files {0} --no-concat".format(",".join(missing_assets))
)
check_call(command, cwd=os.path.join("..", "apps", "frappe"))
def get_assets_link(frappe_head):
from subprocess import getoutput
from requests import head
tag = getoutput(
"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
" refs/tags/,,' -e 's/\^{}//'"
% frappe_head
)
if tag:
# if tag exists, download assets from github release
url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag)
else:
url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head)
if not head(url):
raise ValueError("URL {0} doesn't exist".format(url))
return url
def download_frappe_assets(verbose=True):
"""Downloads and sets up Frappe assets if they exist based on the current
commit HEAD.
Returns True if correctly setup else returns False.
"""
from simple_chalk import green
from subprocess import getoutput
from tempfile import mkdtemp
assets_setup = False
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
if frappe_head:
try:
url = get_assets_link(frappe_head)
click.secho("Retreiving assets...", fg="yellow")
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
assets_archive = download_file(url, prefix)
print("\n{0} Downloaded Frappe assets from {1}".format(green(''), url))
if assets_archive:
import tarfile
click.secho("\nExtracting assets...\n", fg="yellow")
with tarfile.open(assets_archive) as tar:
for file in tar:
if not file.isdir():
dest = "." + file.name.replace("./frappe-bench/sites", "")
show = dest.replace("./assets/", "")
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))
build_missing_files()
return True
else:
raise
except Exception:
# TODO: log traceback in bench.log
click.secho("An Error occurred while downloading assets...", fg="red")
assets_setup = False
finally:
try:
shutil.rmtree(os.path.dirname(assets_archive))
except Exception:
pass
return assets_setup
def symlink(target, link_name, overwrite=False):
'''
"""
Create a symbolic link named link_name pointing to target.
If link_name exists then FileExistsError is raised, unless overwrite=True.
When trying to overwrite a directory, IsADirectoryError is raised.
Source: https://stackoverflow.com/a/55742015/10309266
'''
"""
if not overwrite:
return os.symlink(target, link_name)
@ -76,27 +193,28 @@ def setup():
def get_node_pacman():
pacmans = ['yarn', 'npm']
for exec_ in pacmans:
exec_ = find_executable(exec_)
if exec_:
return exec_
raise ValueError('No Node.js Package Manager found.')
exec_ = find_executable("yarn")
if exec_:
return exec_
raise ValueError("Yarn not found")
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False):
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False, skip_frappe=False):
"""concat / minify js files"""
setup()
make_asset_dirs(make_copy=make_copy, restore=restore)
pacman = get_node_pacman()
mode = 'build' if no_compress else 'production'
command = '{pacman} run {mode}'.format(pacman=pacman, mode=mode)
mode = "build" if no_compress else "production"
command = "{pacman} run {mode}".format(pacman=pacman, mode=mode)
if app:
command += ' --app {app}'.format(app=app)
command += " --app {app}".format(app=app)
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
if skip_frappe:
command += " --skip_frappe"
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe.commands.popen(command, cwd=frappe_app_path)
@ -107,22 +225,22 @@ def watch(no_compress):
pacman = get_node_pacman()
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe_app_path = frappe.get_app_path('frappe', '..')
frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd=frappe_app_path)
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path)
def check_yarn():
if not find_executable('yarn'):
print('Please install yarn using below command and try again.\nnpm install -g yarn')
if not find_executable("yarn"):
print("Please install yarn using below command and try again.\nnpm install -g yarn")
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
assets_path = os.path.join(frappe.local.sites_path, "assets")
for dir_path in [os.path.join(assets_path, 'js'), os.path.join(assets_path, 'css')]:
for dir_path in [os.path.join(assets_path, "js"), os.path.join(assets_path, "css")]:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
@ -131,24 +249,27 @@ def make_asset_dirs(make_copy=False, restore=False):
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
symlinks = []
app_public_path = os.path.join(app_base_path, 'public')
app_public_path = os.path.join(app_base_path, "public")
# app/public > assets/app
symlinks.append([app_public_path, os.path.join(assets_path, app_name)])
# app/node_modules > assets/app/node_modules
if os.path.exists(os.path.abspath(app_public_path)):
symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join(
assets_path, app_name, 'node_modules')])
symlinks.append(
[
os.path.join(app_base_path, "..", "node_modules"),
os.path.join(assets_path, app_name, "node_modules"),
]
)
app_doc_path = None
if os.path.isdir(os.path.join(app_base_path, 'docs')):
app_doc_path = os.path.join(app_base_path, 'docs')
if os.path.isdir(os.path.join(app_base_path, "docs")):
app_doc_path = os.path.join(app_base_path, "docs")
elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')):
app_doc_path = os.path.join(app_base_path, 'www', 'docs')
elif os.path.isdir(os.path.join(app_base_path, "www", "docs")):
app_doc_path = os.path.join(app_base_path, "www", "docs")
if app_doc_path:
symlinks.append([app_doc_path, os.path.join(
assets_path, app_name + '_docs')])
symlinks.append([app_doc_path, os.path.join(assets_path, app_name + "_docs")])
for source, target in symlinks:
source = os.path.abspath(source)
@ -162,7 +283,7 @@ def make_asset_dirs(make_copy=False, restore=False):
shutil.copytree(source, target)
elif make_copy:
if os.path.exists(target):
warnings.warn('Target {target} already exists.'.format(target=target))
warnings.warn("Target {target} already exists.".format(target=target))
else:
shutil.copytree(source, target)
else:
@ -174,7 +295,7 @@ def make_asset_dirs(make_copy=False, restore=False):
try:
symlink(source, target, overwrite=True)
except OSError:
print('Cannot link {} to {}'.format(source, target))
print("Cannot link {} to {}".format(source, target))
else:
# warnings.warn('Source {source} does not exist.'.format(source = source))
pass
@ -193,7 +314,7 @@ def get_build_maps():
build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, 'public', 'build.json')
path = os.path.join(app_path, "public", "build.json")
if os.path.exists(path):
with open(path) as f:
try:
@ -202,8 +323,7 @@ def get_build_maps():
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(
source[0], *source[1].split("/"))
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
source_paths.append(s)
@ -211,36 +331,42 @@ def get_build_maps():
build_maps[target] = source_paths
except ValueError as e:
print(path)
print('JSON syntax error {0}'.format(str(e)))
print("JSON syntax error {0}".format(str(e)))
return build_maps
def pack(target, sources, no_compress, verbose):
from six import StringIO
outtype, outtxt = target.split(".")[-1], ''
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()
for f in sources:
suffix = None
if ':' in f:
f, suffix = f.split(':')
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
try:
with open(f, 'r') as sourcefile:
data = text_type(sourcefile.read(), 'utf-8', errors='ignore')
with open(f, "r") as sourcefile:
data = text_type(sourcefile.read(), "utf-8", errors="ignore")
extn = f.rsplit(".", 1)[1]
if outtype == "js" and extn == "js" and (not no_compress) and suffix != "concat" and (".min." not in f):
tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO()
if (
outtype == "js"
and extn == "js"
and (not no_compress)
and suffix != "concat"
and (".min." not in f)
):
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += text_type(minified or '', 'utf-8').strip('\n') + ';'
outtxt += text_type(minified or "", "utf-8").strip("\n") + ";"
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
@ -248,27 +374,27 @@ def pack(target, sources, no_compress, verbose):
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
outtxt += ('\n/*\n *\t%s\n */' % f)
outtxt += '\n' + data + '\n'
outtxt += "\n/*\n *\t%s\n */" % f
outtxt += "\n" + data + "\n"
except Exception:
print("--Error in:" + f + "--")
print(frappe.get_traceback())
with open(target, 'w') as f:
with open(target, "w") as f:
f.write(outtxt.encode("utf-8"))
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target)/1024))))
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))
def html_to_js_template(path, content):
'''returns HTML template content as Javascript code, adding it to `frappe.templates`'''
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
def scrub_html_template(content):
'''Returns HTML content with removed whitespace and comments'''
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
content = re.sub("\s+", " ", content)
@ -281,12 +407,12 @@ def scrub_html_template(content):
def files_dirty():
for target, sources in iteritems(get_build_maps()):
for f in sources:
if ':' in f:
f, suffix = f.split(':')
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + ' dirty')
print(f + " dirty")
return True
else:
return False

View file

@ -4,7 +4,6 @@
from __future__ import unicode_literals
import frappe, json
import frappe.defaults
from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)

View file

@ -62,11 +62,11 @@
"label": "URLs"
}
],
"modified": "2019-11-07 13:21:19.395927",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Chat",
"name": "Chat Message",
"owner": "arjun@gmail.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -107,7 +107,7 @@ def get_single_value(doctype, field):
value = frappe.db.get_single_value(doctype, field)
return value
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def set_value(doctype, name, fieldname, value=None):
'''Set a value using get_doc, group of values
@ -142,7 +142,7 @@ def set_value(doctype, name, fieldname, value=None):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert(doc=None):
'''Insert a document
@ -160,7 +160,7 @@ def insert(doc=None):
doc = frappe.get_doc(doc).insert()
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert_many(docs=None):
'''Insert multiple documents
@ -186,7 +186,7 @@ def insert_many(docs=None):
return out
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def save(doc):
'''Update (save) an existing document
@ -199,7 +199,7 @@ def save(doc):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def rename_doc(doctype, old_name, new_name, merge=False):
'''Rename document
@ -209,7 +209,7 @@ def rename_doc(doctype, old_name, new_name, merge=False):
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge)
return new_name
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def submit(doc):
'''Submit a document
@ -222,7 +222,7 @@ def submit(doc):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def cancel(doctype, name):
'''Cancel a document
@ -233,7 +233,7 @@ def cancel(doctype, name):
return wrapper.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['DELETE', 'POST'])
def delete(doctype, name):
'''Delete a remote document
@ -241,13 +241,13 @@ def delete(doctype, name):
:param name: name of the document to be deleted'''
frappe.delete_doc(doctype, name, ignore_missing=False)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def set_default(key, value, parent=None):
"""set a user default value"""
frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def make_width_property_setter(doc):
'''Set width Property Setter
@ -257,7 +257,7 @@ def make_width_property_setter(doc):
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
frappe.get_doc(doc).insert(ignore_permissions = True)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def bulk_update(docs):
'''Bulk update documents
@ -333,7 +333,7 @@ def get_time_zone():
'''Returns default time zone'''
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None):
'''Attach a file to Document (POST)

View file

@ -12,7 +12,6 @@ import click
# imports - module imports
import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.exceptions import SiteNotSpecifiedError
@ -272,11 +271,10 @@ def disable_user(context, email):
@click.command('migrate')
@click.option('--rebuild-website', help="Rebuild webpages after migration")
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, rebuild_website=False, skip_failing=False, skip_search_index=False):
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
from frappe.migrate import migrate
@ -287,7 +285,6 @@ def migrate(context, rebuild_website=False, skip_failing=False, skip_search_inde
try:
migrate(
context.verbose,
rebuild_website=rebuild_website,
skip_failing=skip_failing,
skip_search_index=skip_search_index
)

View file

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function
import click
import json, os, sys, subprocess
import json
import os
import subprocess
import sys
from distutils.spawn import find_executable
import click
import frappe
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import update_progress_bar, get_bench_path
from frappe.utils.response import json_handler
from coverage import Coverage
import cProfile, pstats
from six import StringIO
from frappe.utils import get_bench_path, update_progress_bar
@click.command('build')
@ -19,14 +19,22 @@ from six import StringIO
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking')
@click.option('--restore', is_flag=True, default=False, help='Copy the files instead of symlinking with force')
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
def build(app=None, make_copy=False, restore = False, verbose=False):
@click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
def build(app=None, make_copy=False, restore=False, verbose=False, force=False):
"Minify + concatenate JS and CSS files, build translations"
import frappe.build
import frappe
frappe.init('')
# don't minify in developer_mode for faster builds
no_compress = frappe.local.conf.developer_mode or False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore = restore, verbose=verbose)
# dont try downloading assets if force used, app specified or running via CI
if not (force or app or os.environ.get('CI')):
# skip building frappe if assets exist remotely
skip_frappe = frappe.build.download_frappe_assets(verbose=verbose)
else:
skip_frappe = False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore=restore, verbose=verbose, skip_frappe=skip_frappe)
@click.command('watch')
@ -133,6 +141,7 @@ def reset_perms(context):
def execute(context, method, args=None, kwargs=None, profile=False):
"Execute a function"
for site in context.sites:
ret = ""
try:
frappe.init(site=site)
frappe.connect()
@ -151,12 +160,19 @@ def execute(context, method, args=None, kwargs=None, profile=False):
kwargs = {}
if profile:
import cProfile
pr = cProfile.Profile()
pr.enable()
ret = frappe.get_attr(method)(*args, **kwargs)
try:
ret = frappe.get_attr(method)(*args, **kwargs)
except Exception:
ret = frappe.safe_eval(method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals())
if profile:
import pstats
from six import StringIO
pr.disable()
s = StringIO()
pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(.5)
@ -167,6 +183,7 @@ def execute(context, method, args=None, kwargs=None, profile=False):
finally:
frappe.destroy()
if ret:
from frappe.utils.response import json_handler
print(json.dumps(ret, default=json_handler))
if not context.sites:
@ -288,8 +305,6 @@ def import_doc(context, path, force=False):
@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
"Import CSV using data import"
@ -420,7 +435,7 @@ def jupyter(context):
os.mkdir(jupyter_notebooks_path)
bin_path = os.path.abspath('../env/bin')
print('''
Stating Jupyter notebook
Starting Jupyter notebook
Run the following in your first cell to connect notebook to frappe
```
import frappe
@ -492,6 +507,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
frappe.flags.skip_test_records = skip_test_records
if coverage:
from coverage import Coverage
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
cov = Coverage(source=[source_path], omit=[

View file

@ -2,7 +2,7 @@
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"allow_rename": 1,
"autoname": "field:salutation",
"beta": 0,
"creation": "2017-04-10 12:17:58.071915",
@ -53,7 +53,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-04-10 12:55:18.855578",
"modified": "2020-09-14 12:55:18.855578",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Salutation",
@ -129,4 +129,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View file

@ -1,731 +1,184 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
{
"actions": [],
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-10-05 11:10:38.780133",
"custom": 0,
"description": "Keep track of all update feeds",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"subject",
"section_break_8",
"content",
"column_break_5",
"additional_info",
"communication_date",
"column_break_7",
"operation",
"status",
"reference_section",
"reference_doctype",
"reference_name",
"reference_owner",
"column_break_14",
"timeline_doctype",
"timeline_name",
"link_doctype",
"link_name",
"user",
"full_name"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "subject",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "400"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "additional_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "More Information",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "More Information"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Now",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Date"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "operation",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Operation",
"length": 0,
"no_copy": 0,
"options": "\nLogin\nLogout",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "\nLogin\nLogout"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nSuccess\nFailed\nLinked\nClosed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "\nSuccess\nFailed\nLinked\nClosed"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "reference_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Reference"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "reference_doctype"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "reference_name.owner",
"fieldname": "reference_owner",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Owner",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timeline_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Timeline DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "timeline_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Timeline Name",
"length": 0,
"no_copy": 0,
"options": "timeline_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "timeline_doctype"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Link DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Link Name",
"length": 0,
"no_copy": 0,
"options": "link_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Full Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Full Name"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-comment",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-28 11:43:57.504565",
"modified_by": "Administrator",
"module": "Core",
"name": "Activity Log",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
"share": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
"share": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "subject",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",

View file

@ -25,9 +25,6 @@ class ActivityLog(Document):
if self.reference_doctype and self.reference_name:
self.status = "Linked"
def on_trash(self): # pylint: disable=no-self-use
frappe.throw(_("Sorry! You cannot delete auto-generated comments"))
def on_doctype_update():
"""Add indexes in `tabActivity Log`"""
frappe.db.add_index("Activity Log", ["reference_doctype", "reference_name"])

View file

@ -454,18 +454,18 @@ def update_parent_document_on_communication(doc):
# update the modified date for document
parent.update_modified()
update_mins_to_first_communication(parent, doc)
update_first_response_time(parent, doc)
set_avg_response_time(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"):
def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
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)
parent.db_set("first_response_time", round(time_diff_in_seconds(first_responded_on, parent.creation), 2))
def set_avg_response_time(parent, communication):
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":

View file

@ -8,7 +8,7 @@ from frappe import _
import frappe.permissions
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
from frappe.core.doctype.data_import_legacy.importer import get_data_keys
from six import string_types
from frappe.core.doctype.access_log.access_log import make_access_log
@ -330,6 +330,8 @@ class DataExporter:
value = formatdate(value)
elif fieldtype == "Datetime":
value = format_datetime(value)
elif fieldtype == "Duration":
value = format_duration(value, df.hide_days)
row[_column_start_end.start + i + 1] = value

View file

@ -8,6 +8,7 @@ from frappe.model import (
no_value_fields,
table_fields as table_fieldtypes,
)
from frappe.utils import flt, format_duration
from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response
@ -146,8 +147,13 @@ class Exporter:
if df.parent == doctype:
if df.is_child_table_field and df.child_table_df.fieldname != parentfield:
continue
row[i] = doc.get(df.fieldname, "")
value = doc.get(df.fieldname, None)
if df.fieldtype == "Duration":
value = flt(value or 0)
value = format_duration(value, df.hide_days)
row[i] = value
return rows
def get_data_as_docs(self):

View file

@ -1,5 +1,5 @@
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
Test ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
, , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , ,
Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71
Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7
, , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, ,
Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71
Can't render this file because it contains an unexpected character in line 2 and column 54.

View file

@ -9,7 +9,7 @@ import timeit
import json
from datetime import datetime, date
from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
from frappe.utils.xlsxutils import (
read_xlsx_file_from_attached_file,
@ -664,6 +664,20 @@ class Row:
}
)
return
elif df.fieldtype == "Duration":
import re
is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration:
self.warnings.append(
{
"row": self.row_number,
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value)
)
}
)
return value
@ -692,6 +706,8 @@ class Row:
value = flt(value)
elif df.fieldtype in ["Date", "Datetime"]:
value = self.get_date(value, col)
elif df.fieldtype == "Duration":
value = duration_to_seconds(value)
return value

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate
from frappe.utils import getdate, format_duration
doctype_name = 'DocType for Import'
@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.description, 'test description')
self.assertEqual(doc1.number, 1)
self.assertEqual(format_duration(doc1.duration), '3h')
self.assertEqual(doc1.table_field_1[0].child_title, 'child title')
self.assertEqual(doc1.table_field_1[0].child_description, 'child description')
@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22'))
self.assertEqual(doc2.description, 'test description 2')
self.assertEqual(format_duration(doc2.duration), '4d 3h')
self.assertEqual(doc3.another_number, 5)
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m')
def test_data_import_preview(self):
import_file = get_import_file('sample_import_file')
@ -48,7 +52,7 @@ class TestImporter(unittest.TestCase):
preview = data_import.get_preview_from_template()
self.assertEqual(len(preview.data), 4)
self.assertEqual(len(preview.columns), 15)
self.assertEqual(len(preview.columns), 16)
def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory')
@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False):
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},

View file

@ -15,7 +15,7 @@ from frappe import _
from frappe.utils.csvutils import getlink
from frappe.utils.dateutils import parse_date
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds
from six import string_types
@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
d[fieldname] = get_datetime(_date + " " + _time)
else:
d[fieldname] = None
elif fieldtype == "Duration":
d[fieldname] = duration_to_seconds(cstr(d[fieldname]))
elif fieldtype in ("Image", "Attach Image", "Attach"):
# added file to attachments list
attachments.append(d[fieldname])

View file

@ -163,6 +163,7 @@
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
@ -475,9 +476,10 @@
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-02-06 09:06:25.224413",
"modified": "2020-08-28 11:28:21.252853",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -490,7 +490,7 @@
"collapsible_depends_on": "links",
"fieldname": "links_section",
"fieldtype": "Section Break",
"label": "Links Section"
"label": "Linked Documents"
},
{
"fieldname": "links",
@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2020-08-06 12:59:32.369095",
"modified": "2020-09-24 13:13:58.227153",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -99,6 +99,10 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -234,6 +238,8 @@ class DocType(Document):
if not autoname and self.get("fields", {"fieldname":"naming_series"}):
self.autoname = "naming_series:"
elif self.autoname == "naming_series:" and not self.get("fields", {"fieldname":"naming_series"}):
frappe.throw(_("Invalid fieldname '{0}' in autoname").format(self.autoname))
# validate field name if autoname field:fieldname is used
# Create unique index on autoname field automatically.
@ -634,13 +640,15 @@ class DocType(Document):
if not name:
name = self.name
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
# and should only contain letters, numbers and underscore
if six.PY2:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name)
else:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name, flags = re.ASCII)
if not is_a_valid_name:
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
@ -762,7 +770,7 @@ def validate_fields(meta):
if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname):
has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*)
from `tab{doctype}` where ifnull({fieldname}, '') != ''
from `tab{doctype}` where ifnull(`{fieldname}`, '') != ''
group by `{fieldname}` having count(*) > 1 limit 1""".format(
doctype=d.parent, fieldname=d.fieldname))

View file

@ -8,7 +8,8 @@
"label",
"action_type",
"action",
"group"
"group",
"hidden"
],
"fields": [
{
@ -31,20 +32,28 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Action Type",
"options": "Server Action",
"options": "Server Action\nRoute",
"reqd": 1
},
{
"columns": 4,
"fieldname": "action",
"fieldtype": "Data",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Action",
"label": "Action / Route",
"reqd": 1
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-09-24 09:11:39.860100",
"links": [],
"modified": "2020-08-21 14:44:03.845315",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Action",

View file

@ -0,0 +1,23 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Document Naming Rule', {
refresh: function(frm) {
frm.trigger('document_type');
},
document_type: (frm) => {
// update the select field options with fieldnames
if (frm.doc.document_type) {
frappe.model.with_doctype(frm.doc.document_type, () => {
let fieldnames = frappe.get_meta(frm.doc.document_type).fields
.filter((d) => {
return frappe.model.no_value_type.indexOf(d.fieldtype) === -1;
}).map((d) => {
return {label: `${d.label} (${d.fieldname})`, value: d.fieldname};
});
frappe.meta.get_docfield('Document Naming Rule Condition', 'field', frm.doc.name).options = fieldnames;
frm.refresh_field('conditions');
});
}
}
});

View file

@ -0,0 +1,104 @@
{
"actions": [],
"creation": "2020-09-07 12:48:48.334318",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"disabled",
"priority",
"section_break_3",
"conditions",
"naming_section",
"prefix",
"prefix_digits",
"counter"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
"options": "DocType"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "prefix",
"fieldtype": "Data",
"label": "Prefix",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
"read_only": 1
},
{
"default": "5",
"description": "Example: 00001",
"fieldname": "prefix_digits",
"fieldtype": "Int",
"label": "Digits",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "naming_section",
"fieldtype": "Section Break",
"label": "Naming"
},
{
"collapsible": 1,
"collapsible_depends_on": "conditions",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Rule Conditions"
},
{
"fieldname": "conditions",
"fieldtype": "Table",
"label": "Conditions",
"options": "Document Naming Rule Condition"
},
{
"description": "Rules with higher priority will be applied first.",
"fieldname": "priority",
"fieldtype": "Int",
"label": "Priority"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-21 10:23:34.401539",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "document_type",
"track_changes": 1
}

View file

@ -0,0 +1,21 @@
# -*- 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
from frappe.utils.data import evaluate_filters
class DocumentNamingRule(Document):
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
if self.conditions:
if not evaluate_filters(doc, [(d.field, d.condition, d.value) for d in self.conditions]):
return
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestDocumentNamingRule(unittest.TestCase):
def test_naming_rule_by_series(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-todo-',
prefix_digits = 5
)).insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
self.assertEqual(todo.name, 'test-todo-00001')
naming_rule.delete()
todo.delete()
def test_naming_rule_by_condition(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-high-',
prefix_digits = 5,
priority = 10,
conditions = [dict(
field = 'priority',
condition = '=',
value = 'High'
)]
)).insert()
# another rule
naming_rule_1 = frappe.copy_doc(naming_rule)
naming_rule_1.prefix = 'test-medium-'
naming_rule_1.conditions[0].value = 'Medium'
naming_rule_1.insert()
# default rule with low priority - should not get applied for rules
# with higher priority
naming_rule_2 = frappe.copy_doc(naming_rule)
naming_rule_2.prefix = 'test-low-'
naming_rule_2.priority = 0
naming_rule_2.conditions = []
naming_rule_2.insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'High',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_1 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Medium',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_2 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Low',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
try:
self.assertEqual(todo.name, 'test-high-00001')
self.assertEqual(todo_1.name, 'test-medium-00001')
self.assertEqual(todo_2.name, 'test-low-00001')
finally:
naming_rule.delete()
naming_rule_1.delete()
naming_rule_2.delete()
todo.delete()
todo_1.delete()
todo_2.delete()

View file

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

View file

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2020-09-08 10:17:54.366279",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"condition",
"value"
],
"fields": [
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field",
"reqd": 1
},
{
"fieldname": "condition",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Condition",
"options": "=\n!=\n>\n<\n>=\n<=",
"reqd": 1
},
{
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Value",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-08 10:19:56.192949",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule Condition",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

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

View file

@ -17,11 +17,11 @@
"unique": 1
}
],
"modified": "2019-06-30 13:24:13.732202",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -278,25 +278,26 @@ class File(Document):
base_url = os.path.dirname(self.file_url)
files = []
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(os.path.dirname(zip_path))
for info in zf.infolist():
if not info.filename.startswith('__MACOSX'):
file_url = file_url = base_url + '/' + info.filename
file_name = frappe.db.get_value('File', dict(file_url=file_url))
if file_name:
file_doc = frappe.get_doc('File', file_name)
else:
file_doc = frappe.new_doc("File")
file_doc.file_name = info.filename
file_doc.file_size = info.file_size
file_doc.folder = self.folder
file_doc.is_private = self.is_private
file_doc.file_url = file_url
file_doc.attached_to_doctype = self.attached_to_doctype
file_doc.attached_to_name = self.attached_to_name
file_doc.save()
files.append(file_doc)
with zipfile.ZipFile(zip_path) as z:
for file in z.filelist:
if file.is_dir() or file.filename.startswith('__MACOSX/'):
# skip directories and macos hidden directory
continue
filename = os.path.basename(file.filename)
if filename.startswith('.'):
# skip hidden files
continue
file_doc = frappe.new_doc('File')
file_doc.content = z.read(file.filename)
file_doc.file_name = filename
file_doc.folder = self.folder
file_doc.is_private = self.is_private
file_doc.attached_to_doctype = self.attached_to_doctype
file_doc.attached_to_name = self.attached_to_name
file_doc.save()
files.append(file_doc)
frappe.delete_doc('File', self.name)
return files
@ -359,6 +360,9 @@ class File(Document):
"""write file to disk with a random name (to compare)"""
file_path = get_files_path(is_private=self.is_private)
if os.path.sep in self.file_name:
frappe.throw(_('File name cannot have {0}').format(os.path.sep))
# create directory (if not exists)
frappe.create_folder(file_path)
# write the file
@ -938,7 +942,7 @@ def attach_files_to_document(doc, event):
# we dont want the update to fail if file cannot be attached for some reason
try:
value = doc.get(df.fieldname)
if not value.startswith(("/files", "/private/files")):
if not (value or '').startswith(("/files", "/private/files")):
return
if frappe.db.exists("File", {

View file

@ -54,12 +54,12 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-05-04 11:05:54.750351",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Has Domain",
"name_case": "",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,

View file

@ -31,7 +31,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"reqd": 1
},
{
@ -48,7 +48,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 14:32:17.174796",
"modified": "2020-09-03 10:52:03.895817",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Column",

View file

@ -109,12 +109,14 @@ class ScheduledJobType(Document):
def on_trash(self):
frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue()
def run_scheduled_job(job_type):
'''This is a wrapper function that runs a hooks.scheduler_events method'''
try:
@ -122,44 +124,62 @@ def run_scheduled_job(job_type):
except Exception:
print(frappe.get_traceback())
def sync_jobs():
frappe.reload_doc('core', 'doctype', 'scheduled_job_type')
all_events = []
scheduler_events = frappe.get_hooks("scheduler_events")
insert_events(all_events, scheduler_events)
clear_events(all_events, scheduler_events)
def insert_events(all_events, scheduler_events):
def sync_jobs(hooks=None):
frappe.reload_doc("core", "doctype", "scheduled_job_type")
scheduler_events = hooks or frappe.get_hooks("scheduler_events")
all_events = insert_events(scheduler_events)
clear_events(all_events)
def insert_events(scheduler_events):
cron_jobs, event_jobs = [], []
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
if isinstance(events, dict):
insert_cron_event(events, all_events)
cron_jobs += insert_cron_jobs(events)
else:
# hourly, daily etc
insert_event_list(events, event_type, all_events)
event_jobs += insert_event_jobs(events, event_type)
return cron_jobs + event_jobs
def insert_cron_event(events, all_events):
def insert_cron_jobs(events):
cron_jobs = []
for cron_format in events:
for event in events.get(cron_format):
all_events.append(event)
insert_single_event('Cron', event, cron_format)
cron_jobs.append(event)
insert_single_event("Cron", event, cron_format)
return cron_jobs
def insert_event_list(events, event_type, all_events):
def insert_event_jobs(events, event_type):
event_jobs = []
for event in events:
all_events.append(event)
event_jobs.append(event)
frequency = event_type.replace('_', ' ').title()
insert_single_event(frequency, event)
return event_jobs
def insert_single_event(frequency, event, cron_format = None):
if not frappe.db.exists('Scheduled Job Type', dict(method=event)):
frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = event,
cron_format = cron_format,
frequency = frequency
)).insert()
def clear_events(all_events, scheduler_events):
for event in frappe.get_all('Scheduled Job Type', ('name', 'method')):
def insert_single_event(frequency, event, cron_format=None):
cron_expr = {"cron_format": cron_format} if cron_format else {}
doc = frappe.get_doc({
"doctype": "Scheduled Job Type",
"method": event,
"cron_format": cron_format,
"frequency": frequency
})
if not frappe.db.exists("Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr }):
try:
doc.insert()
except frappe.DuplicateEntryError:
doc.delete()
doc.insert()
def clear_events(all_events):
for event in frappe.get_all("Scheduled Job Type", ("name", "method")):
if event.method not in all_events:
frappe.delete_doc('Scheduled Job Type', event.name)
frappe.delete_doc("Scheduled Job Type", event.name)

View file

@ -11,11 +11,10 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
class TestScheduledJobType(unittest.TestCase):
def setUp(self):
if not frappe.get_all('Scheduled Job Type', limit=1):
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()
def test_sync_jobs(self):
all_job = frappe.get_doc('Scheduled Job Type',
@ -32,6 +31,12 @@ class TestScheduledJobType(unittest.TestCase):
self.assertEqual(cron_job.frequency, 'Cron')
self.assertEqual(cron_job.cron_format, '0/15 * * * *')
# check if jobs are synced after change in hooks
updated_scheduler_events = { "hourly": ["frappe.email.queue.flush"] }
sync_jobs(updated_scheduler_events)
updated_scheduled_job = frappe.get_doc("Scheduled Job Type", {"method": "frappe.email.queue.flush"})
self.assertEqual(updated_scheduled_job.frequency, "Hourly")
def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job.db_set('last_execution', '2019-01-01 00:00:00')

View file

@ -7,12 +7,12 @@
"engine": "InnoDB",
"field_order": [
"script_type",
"disabled",
"column_break_3",
"reference_doctype",
"doctype_event",
"api_method",
"allow_guest",
"column_break_3",
"disabled",
"section_break_8",
"script",
"help_section",
@ -85,8 +85,9 @@
"fieldtype": "HTML"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-07 13:13:02.483963",
"modified": "2020-08-24 16:44:41.060350",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -24,7 +24,8 @@ class ServerScript(Document):
# validate if guest is allowed
if frappe.session.user == 'Guest' and not self.allow_guest:
raise frappe.PermissionError
safe_exec(self.script)
_globals, _locals = safe_exec(self.script)
return _globals.frappe.flags # output can be stored in flags
else:
# wrong report type!
raise frappe.DoesNotExistError

View file

@ -36,6 +36,15 @@ if "validate" in doc.description:
allow_guest = 1,
script = '''
frappe.response['message'] = 'hello'
'''
),
dict(
name='test_return_value',
script_type = 'API',
api_method = 'test_return_value',
allow_guest = 1,
script = '''
frappe.flags = 'hello'
'''
)
]
@ -73,3 +82,6 @@ class TestServerScript(unittest.TestCase):
response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script")
self.assertEqual(response.status_code, 200)
self.assertEqual("hello", response.json()["message"])
def test_api_return(self):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')

View file

@ -40,6 +40,7 @@
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
@ -415,6 +416,13 @@
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
},
{
"default": "3",
"description": "Hourly rate limit for generating password reset links",
"fieldname": "password_reset_limit",
"fieldtype": "Int",
"label": "Password Reset Link Generation Limit"
},
{
"default": "1",
"fieldname": "logout_on_password_reset",

View file

@ -19,6 +19,7 @@ class TestUser(unittest.TestCase):
# disable password strength test
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", "")
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 3)
def test_user_type(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com',
@ -222,6 +223,19 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
def test_rate_limiting_for_reset_password(self):
from frappe.utils.password import delete_password_reset_cache
delete_password_reset_cache()
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
user = frappe.get_doc("User", "testperm@example.com")
link = user.reset_password()
self.assertRegex(link, "\/update-password\?key=[A-Za-z0-9]*")
self.assertRaises(frappe.ValidationError, user.reset_password, False)
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)

View file

@ -357,7 +357,7 @@
"depends_on": "enabled",
"fieldname": "email_settings",
"fieldtype": "Section Break",
"label": "Email Settings"
"label": "Email"
},
{
"default": "1",
@ -383,12 +383,6 @@
"label": "Email Signature",
"no_copy": 1
},
{
"collapsible": 1,
"fieldname": "email_inbox",
"fieldtype": "Section Break",
"label": "Email Inbox"
},
{
"fieldname": "user_emails",
"fieldtype": "Table",
@ -670,7 +664,7 @@
}
],
"max_attachments": 5,
"modified": "2020-08-06 19:48:49.677800",
"modified": "2020-08-26 19:48:49.677800",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@ -704,4 +698,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -13,15 +13,16 @@ from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
import frappe.share
import re
import json
from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue
STANDARD_USERS = ("Guest", "Administrator")
class MaxUsersReachedError(frappe.ValidationError): pass
class MaxUsersReachedError(frappe.ValidationError):
pass
class User(Document):
__new_password = None
@ -225,6 +226,11 @@ class User(Document):
def reset_password(self, send_email=False, password_expired=False):
from frappe.utils import random_string, get_url
rate_limit = frappe.db.get_single_value("System Settings", "password_reset_limit")
if rate_limit:
check_password_reset_limit(self.name, rate_limit)
key = random_string(32)
self.db_set("reset_password_key", key)
@ -236,6 +242,7 @@ class User(Document):
if send_email:
self.password_reset_mail(link)
update_password_reset_limit(self.name)
return link
def get_other_system_managers(self):
@ -1113,4 +1120,17 @@ def generate_keys(user):
@frappe.whitelist()
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
def update_password_reset_limit(user):
generated_link_count = get_generated_link_count(user)
generated_link_count += 1
frappe.cache().hset("password_reset_link_count", user, generated_link_count)
def check_password_reset_limit(user, rate_limit):
generated_link_count = get_generated_link_count(user)
if generated_link_count >= rate_limit:
frappe.throw(_("You have reached the hourly limit for generating password reset links. Please try again later."))
def get_generated_link_count(user):
return cint(frappe.cache().hget("password_reset_link_count", user)) or 0

View file

@ -26,8 +26,7 @@ class TestUserPermission(unittest.TestCase):
user = create_user('test_user_perm1@example.com', 'Website Manager')
for category in ['general', 'public']:
if not frappe.db.exists('Blog Category', category):
frappe.get_doc({'doctype': 'Blog Category',
'category_name': category, 'title': category}).insert()
frappe.get_doc({'doctype': 'Blog Category', 'title': category}).insert()
param = get_params(user, 'Blog Category', 'general', is_default=1)
add_user_permissions(param)

View file

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

View file

@ -1,187 +1,91 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2013-01-10 16:34:01",
"custom": 0,
"description": "Adds a client custom script to a DocType",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 0,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a client custom script to a DocType",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"dt",
"enabled",
"script",
"sample"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "dt",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "DocType",
"length": 0,
"no_copy": 0,
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "dt",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "DocType",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "script",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Script",
"length": 0,
"no_copy": 0,
"oldfieldname": "script",
"oldfieldtype": "Code",
"options": "JS",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "script",
"fieldtype": "Code",
"label": "Script",
"oldfieldname": "script",
"oldfieldtype": "Code",
"options": "JS",
"show_days": 1,
"show_seconds": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sample",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sample",
"length": 0,
"no_copy": 0,
"options": "<h3>Custom Script Help</h3>\n<p>Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date &lt; get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' &amp;&amp; frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total &gt; 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n</code></pre>",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "sample",
"fieldtype": "HTML",
"label": "Sample",
"options": "<h3>Custom Script Help</h3>\n<p>Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started</p>\n<pre><code>\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date &lt; get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' &amp;&amp; frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total &gt; 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n</code></pre>",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled",
"show_days": 1,
"show_seconds": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-glass",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2019-03-21 14:26:57.402994",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Script",
"owner": "Administrator",
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-24 21:56:07.719579",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Script",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"sort_order": "ASC",
"track_changes": 1
}

View file

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

View file

@ -100,6 +100,7 @@ def export_package():
@frappe.whitelist()
def import_package(package=None):
"""Import package from JSON."""
frappe.only_for("System Manager")
if isinstance(package, string_types):
package = json.loads(package)

View file

@ -5,12 +5,12 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import get_source_value
from frappe.utils.safe_exec import get_safe_globals
class DataMigrationMapping(Document):
def get_filters(self):
if self.condition:
return frappe.safe_eval(self.condition, dict(frappe=frappe))
return frappe.safe_eval(self.condition, get_safe_globals())
def get_fields(self):
fields = []
@ -64,9 +64,16 @@ def get_value_from_fieldname(field_map, fieldname_field, doc):
field_name = get_source_value(field_map, fieldname_field)
if field_name.startswith('eval:'):
value = frappe.safe_eval(field_name[5:], dict(frappe=frappe))
value = frappe.safe_eval(field_name[5:], get_safe_globals())
elif field_name[0] in ('"', "'"):
value = field_name[1:-1]
else:
value = get_source_value(doc, field_name)
return value
def get_source_value(source, key):
'''Get value from source (object or dict) based on key'''
if isinstance(source, dict):
return source.get(key)
else:
return getattr(source, key)

View file

@ -186,8 +186,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-28 15:49:54.019073",
"modified_by": "cave@aperture.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Plan",
"name_case": "",

View file

@ -800,12 +800,12 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-30 07:02:26.980372",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Run",
"name_case": "",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"amend": 0,

View file

@ -6,7 +6,8 @@ from __future__ import unicode_literals
import frappe, json, math
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_source_value, cstr
from frappe.utils import cstr
from frappe.data_migration.doctype.data_migration_mapping.data_migration_mapping import get_source_value
class DataMigrationRun(Document):
def run(self):

View file

@ -128,7 +128,7 @@ CREATE TABLE `tabDocType Action` (
`label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)

View file

@ -92,6 +92,8 @@ def bootstrap_database(db_name, verbose, source_sql=None):
sys.exit(1)
import_db_from_sql(source_sql, verbose)
frappe.connect(db_name=db_name)
if not 'tabDefaultValue' in frappe.db.get_tables():
print('''Database not installed, this can due to lack of permission, or that the database name exists.
Check your mysql root password, or use --force to reinstall''')

View file

@ -128,7 +128,7 @@ CREATE TABLE "tabDocType Action" (
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) NOT NULL,
"group" varchar(140) DEFAULT NULL,
"group" text DEFAULT NULL,
"action_type" varchar(140) NOT NULL,
"action" varchar(140) NOT NULL,
PRIMARY KEY ("name")

View file

@ -49,7 +49,7 @@ class PostgresTable(DBTable):
elif col.fieldtype in ("Check"):
using_clause = "USING {}::smallint".format(col.fieldname)
query.append("ALTER COLUMN {0} TYPE {1} {2}".format(
query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause)

View file

@ -40,7 +40,7 @@ class Workspace:
self.doc = self.get_page_for_user()
if self.doc.module not in self.allowed_modules:
if self.doc.module and self.doc.module not in self.allowed_modules:
raise frappe.PermissionError
self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
@ -203,7 +203,7 @@ class Workspace:
cards = cards + get_custom_reports_and_doctypes(self.doc.module)
if len(self.extended_cards):
cards = cards + self.extended_cards
cards = merge_cards_based_on_label(cards + self.extended_cards)
default_country = frappe.db.get_default("country")
def _doctype_contains_a_record(name):
@ -590,3 +590,16 @@ def reset_customization(page):
original_page = frappe.get_doc("Desk Page", page)
page_doc = get_custom_workspace_for_user(page)
page_doc.delete()
def merge_cards_based_on_label(cards):
"""Merge cards with common label."""
cards_dict = {}
for card in cards:
if card.label in cards_dict:
links = loads(cards_dict[card.label].links) + loads(card.links)
cards_dict[card.label].update(dict(links=dumps(links)))
cards_dict[card.label] = cards_dict.pop(card.label)
else:
cards_dict[card.label] = card
return list(cards_dict.values())

View file

@ -53,11 +53,11 @@
}
],
"links": [],
"modified": "2020-06-15 11:24:57.639430",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Calendar View",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

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

View file

@ -0,0 +1,52 @@
{
"actions": [],
"autoname": "format:Log on {timestamp}",
"creation": "2020-08-18 19:56:12.336427",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"script",
"output"
],
"fields": [
{
"fieldname": "script",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Script",
"read_only": 1
},
{
"fieldname": "output",
"fieldtype": "Code",
"label": "Output",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-18 20:07:57.587344",
"modified_by": "Administrator",
"module": "Desk",
"name": "Console Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,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 ConsoleLog(Document):
pass

View file

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

View file

@ -60,11 +60,11 @@ def has_permission(doc, ptype, user):
if doc.chart_type == 'Report':
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
allowed_reports = [key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()]
if doc.report_name in allowed_reports:
return True
else:
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
allowed_doctypes = [frappe.permissions.get_doctypes_with_read()]
if doc.document_type in allowed_doctypes:
return True

View file

@ -38,7 +38,7 @@ class DeskPage(Document):
pages = frappe.get_all("Desk Page", fields=["name", "module"], filters=filters, as_list=1)
return { page[1]: page[0] for page in pages }
return { page[1]: page[0] for page in pages if page[1] }
def disable_saving_as_standard():
return frappe.flags.in_install or \

View file

@ -120,8 +120,8 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2020-05-31 22:31:12.886950",
"modified_by": "umair@erpnext.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",

View file

@ -0,0 +1,21 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('System Console', {
onload: function(frm) {
frappe.ui.keys.add_shortcut({
shortcut: 'shift+enter',
action: () => frm.execute_action('Execute'),
page: frm.page,
description: __('Execute Console script'),
ignore_inputs: true,
});
},
refresh: function(frm) {
frm.disable_save();
frm.page.set_primary_action(__("Execute"), () => {
frm.execute_action('Execute');
});
}
});

View file

@ -0,0 +1,68 @@
{
"actions": [
{
"action": "#List/Console Log/List",
"action_type": "Route",
"label": "Logs"
},
{
"action": "frappe.desk.doctype.system_console.system_console.execute_code",
"action_type": "Server Action",
"hidden": 1,
"label": "Execute"
}
],
"creation": "2020-08-18 17:44:35.647815",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"console",
"commit",
"output"
],
"fields": [
{
"description": "To print output use <code>log(text)</code>",
"fieldname": "console",
"fieldtype": "Code",
"label": "Console",
"options": "Python"
},
{
"fieldname": "output",
"fieldtype": "Code",
"label": "Output",
"read_only": 1
},
{
"default": "0",
"fieldname": "commit",
"fieldtype": "Check",
"label": "Commit"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-21 14:44:35.296877",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import frappe
from frappe.utils.safe_exec import safe_exec
from frappe.model.document import Document
class SystemConsole(Document):
def run(self):
frappe.only_for('System Manager')
try:
frappe.debug_log = []
safe_exec(self.console)
self.output = '\n'.join(frappe.debug_log)
except: # noqa: E722
self.output = frappe.get_traceback()
if self.commit:
frappe.db.commit()
else:
frappe.db.rollback()
frappe.get_doc(dict(
doctype='Console Log',
script=self.console,
output=self.output)).insert()
frappe.db.commit()
@frappe.whitelist()
def execute_code(doc):
console = frappe.get_doc(json.loads(doc))
console.run()
return console.as_dict()

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestSystemConsole(unittest.TestCase):
def test_system_console(self):
system_console = frappe.get_doc('System Console')
system_console.console = 'log("hello")'
system_console.run()
self.assertEqual(system_console.output, 'hello')
system_console.console = 'log(frappe.db.get_value("DocType", "DocType", "module"))'
system_console.run()
self.assertEqual(system_console.output, 'Core')

View file

@ -169,16 +169,14 @@ def get_comments(doctype, doc_name, frequency, user):
return timeline
def is_document_followed(doctype, doc_name, user):
docs = frappe.get_all(
return frappe.db.exists(
"Document Follow",
filters={
{
"ref_doctype": doctype,
"ref_docname": doc_name,
"user": user
},
limit=1
}
)
return len(docs)
@frappe.whitelist()
def get_follow_users(doctype, doc_name):

View file

@ -130,7 +130,7 @@ class FormMeta(Meta):
def add_custom_script(self):
"""embed all require files"""
# custom script
custom = frappe.db.get_value("Custom Script", {"dt": self.name}, "script") or ""
custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or ""
self.set("__custom_js", custom)

View file

@ -23,6 +23,8 @@ def savedocs(doc, action):
# update recent documents
run_onload(doc)
send_updated_docs(doc)
frappe.msgprint(frappe._("Saved"), indicator='green', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())
raise
@ -36,6 +38,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
doc.set(workflow_state_fieldname, workflow_state)
doc.cancel()
send_updated_docs(doc)
frappe.msgprint(frappe._("Cancelled"), indicator='red', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())

View file

@ -1,5 +1,5 @@
import frappe
from frappe.model import no_value_fields
from frappe.model import no_value_fields, table_fields
import json
@frappe.whitelist()
@ -9,11 +9,13 @@ def get_preview_data(doctype, docname):
if not meta.show_preview_popup: return
preview_fields = [field.fieldname for field in meta.fields \
if field.in_preview and field.fieldtype not in no_value_fields]
if field.in_preview and field.fieldtype not in no_value_fields \
and field.fieldtype not in table_fields]
# no preview fields defined, build list from mandatory fields
if not preview_fields:
preview_fields = [field.fieldname for field in meta.fields if field.reqd]
preview_fields = [field.fieldname for field in meta.fields if field.reqd \
and field.fieldtype not in table_fields]
title_field = meta.get_title_field()
image_field = meta.image_field

View file

@ -1,263 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
out = frappe.response
from frappe.utils import cint
import frappe.defaults
from six import text_type
def get_sql_tables(q):
if q.find('WHERE') != -1:
tl = q.split('FROM')[1].split('WHERE')[0].split(',')
elif q.find('GROUP BY') != -1:
tl = q.split('FROM')[1].split('GROUP BY')[0].split(',')
else:
tl = q.split('FROM')[1].split('ORDER BY')[0].split(',')
return [t.strip().strip('`')[3:] for t in tl]
def get_parent_dt(dt):
pdt = ''
if frappe.db.sql('select name from `tabDocType` where istable=1 and name=%s', dt):
import frappe.model.meta
return frappe.model.meta.get_parent_dt(dt)
return pdt
def get_sql_meta(tl):
std_columns = {
'owner':('Owner', '', '', '100'),
'creation':('Created on', 'Date', '', '100'),
'modified':('Last modified on', 'Date', '', '100'),
'modified_by':('Modified By', '', '', '100')
}
meta = {}
for dt in tl:
meta[dt] = std_columns.copy()
# for table doctype, the ID is the parent id
pdt = get_parent_dt(dt)
if pdt:
meta[dt]['parent'] = ('ID', 'Link', pdt, '200')
# get the field properties from DocField
res = frappe.db.sql("select fieldname, label, fieldtype, options, width \
from tabDocField where parent=%s", dt)
for r in res:
if r[0]:
meta[dt][r[0]] = (r[1], r[2], r[3], r[4]);
# name
meta[dt]['name'] = ('ID', 'Link', dt, '200')
return meta
def add_match_conditions(q, tl):
from frappe.desk.reportview import build_match_conditions
sl = []
for dt in tl:
s = build_match_conditions(dt)
if s:
sl.append(s)
# insert the conditions
if sl:
condition_st = q.find('WHERE')!=-1 and ' AND ' or ' WHERE '
condition_end = q.find('ORDER BY')!=-1 and 'ORDER BY' or 'LIMIT'
condition_end = q.find('GROUP BY')!=-1 and 'GROUP BY' or condition_end
if q.find('ORDER BY')!=-1 or q.find('LIMIT')!=-1 or q.find('GROUP BY')!=-1: # if query continues beyond conditions
q = q.split(condition_end)
q = q[0] + condition_st + '(' + ' OR '.join(sl) + ') ' + condition_end + q[1]
else:
q = q + condition_st + '(' + ' OR '.join(sl) + ')'
return q
def guess_type(m):
"""
Returns fieldtype depending on the MySQLdb Description
"""
if frappe.db.is_type_number(m):
return 'Currency'
elif m in frappe.is_type_datetime(m):
return 'Date'
else:
return 'Data'
def build_description_simple():
colnames, coltypes, coloptions, colwidths = [], [], [], []
for m in frappe.db.get_description():
colnames.append(m[0])
coltypes.append(guess_type[m[1]])
coloptions.append('')
colwidths.append('100')
return colnames, coltypes, coloptions, colwidths
def build_description_standard(meta, tl):
desc = frappe.db.get_description()
colnames, coltypes, coloptions, colwidths = [], [], [], []
# merged metadata - used if we are unable to
# get both the table name and field name from
# the description - in case of joins
merged_meta = {}
for d in meta:
merged_meta.update(meta[d])
for f in desc:
fn, dt = f[0], ''
if '.' in fn:
dt, fn = fn.split('.')
if (not dt) and merged_meta.get(fn):
# no "AS" given, find type from merged description
desc = merged_meta[fn]
colnames.append(desc[0] or fn)
coltypes.append(desc[1] or '')
coloptions.append(desc[2] or '')
colwidths.append(desc[3] or '100')
elif fn in meta.get(dt,{}):
# type specified for a multi-table join
# usually from Report Builder
desc = meta[dt][fn]
colnames.append(desc[0] or fn)
coltypes.append(desc[1] or '')
coloptions.append(desc[2] or '')
colwidths.append(desc[3] or '100')
else:
# nothing found
# guess
colnames.append(fn)
coltypes.append(guess_type(f[1]))
coloptions.append('')
colwidths.append('100')
return colnames, coltypes, coloptions, colwidths
@frappe.whitelist()
def runquery(q='', ret=0, from_export=0):
import frappe.utils
formatted = cint(frappe.form_dict.get('formatted'))
# CASE A: Simple Query
# --------------------
if frappe.form_dict.get('simple_query') or frappe.form_dict.get('is_simple'):
if not q: q = frappe.form_dict.get('simple_query') or frappe.form_dict.get('query')
if q.split()[0].lower() != 'select':
raise Exception('Query must be a SELECT')
as_dict = cint(frappe.form_dict.get('as_dict'))
res = frappe.db.sql(q, as_dict = as_dict, as_list = not as_dict, formatted=formatted)
# build colnames etc from metadata
colnames, coltypes, coloptions, colwidths = [], [], [], []
# CASE B: Standard Query
# -----------------------
else:
if not q: q = frappe.form_dict.get('query')
tl = get_sql_tables(q)
meta = get_sql_meta(tl)
q = add_match_conditions(q, tl)
# replace special variables
q = q.replace('__user', frappe.session.user)
q = q.replace('__today', frappe.utils.nowdate())
res = frappe.db.sql(q, as_list=1, formatted=formatted)
colnames, coltypes, coloptions, colwidths = build_description_standard(meta, tl)
# run server script
# -----------------
style, header_html, footer_html, page_template = '', '', '', ''
out['colnames'] = colnames
out['coltypes'] = coltypes
out['coloptions'] = coloptions
out['colwidths'] = colwidths
out['header_html'] = header_html
out['footer_html'] = footer_html
out['page_template'] = page_template
if style:
out['style'] = style
# just the data - return
if ret==1:
return res
out['values'] = res
# return num of entries
qm = frappe.form_dict.get('query_max') or ''
if qm and qm.strip():
if qm.split()[0].lower() != 'select':
raise Exception('Query (Max) must be a SELECT')
if not frappe.form_dict.get('simple_query'):
qm = add_match_conditions(qm, tl)
out['n_values'] = frappe.utils.cint(frappe.db.sql(qm)[0][0])
@frappe.whitelist()
def runquery_csv():
global out
q = frappe.form_dict.get('query')
rep_name = frappe.form_dict.get('report_name')
if not frappe.form_dict.get('simple_query'):
# Report Name
if not rep_name:
rep_name = get_sql_tables(q)[0]
if not rep_name: rep_name = 'DataExport'
rows = [[rep_name], out['colnames']] + out['values']
from six import StringIO
import csv
f = StringIO()
writer = csv.writer(f)
for r in rows:
# encode only unicode type strings and not int, floats etc.
writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r))
f.seek(0)
out['result'] = text_type(f.read(), 'utf-8')
out['type'] = 'csv'
out['doctype'] = rep_name
def add_limit_to_query(query, args):
"""
Add limit condition to query
can be used by methods called in listing to add limit condition
"""
if args.get('limit_page_length'):
query += """
limit %(limit_start)s, %(limit_page_length)s"""
import frappe.utils
args['limit_start'] = frappe.utils.cint(args.get('limit_start'))
args['limit_page_length'] = frappe.utils.cint(args.get('limit_page_length'))
return query, args

View file

@ -8,14 +8,13 @@ import os, json
from frappe import _
from frappe.modules import scrub, get_module_path
from frappe.utils import flt, cint, get_html_format, get_url_to_form
from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils import gzip_decompress
from frappe.core.utils import ljust_list
def get_report_doc(report_name):
@ -67,7 +66,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
@ -168,7 +167,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
result = None
if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report and not custom_columns:
if filters:
if isinstance(filters, string_types):
filters = json.loads(filters)
@ -215,25 +214,19 @@ def add_data_to_custom_columns(columns, result):
return data
def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
def reorder_data_for_custom_columns(custom_columns, columns, result):
if not result:
return []
if report_type == 'Query Report':
# Assume list result for query reports
# Query report columns exclusively use Label
custom_column_labels = [col["label"] for col in custom_columns]
original_column_labels = [col.split(":")[0] for col in columns]
return get_columns_from_list(custom_column_labels, original_column_labels, result)
custom_column_names = [col["fieldname"] for col in custom_columns]
columns = [get_column_as_dict(col) for col in columns]
if isinstance(result[0], list) or isinstance(result[0], tuple):
# If the result is a list of lists
original_column_names = [col["fieldname"] for col in columns]
custom_column_names = [col["label"] for col in custom_columns]
original_column_names = [col["label"] for col in columns]
return get_columns_from_list(custom_column_names, original_column_names, result)
else:
# If the result is a list of dicts
return get_columns_from_dict(custom_column_names, result)
# columns do not need to be reordered if result is a list of dicts
return result
def get_columns_from_list(columns, target_columns, result):
reordered_result = []
@ -251,21 +244,6 @@ def get_columns_from_list(columns, target_columns, result):
return reordered_result
def get_columns_from_dict(columns, result):
reordered_result = []
for res in result:
r = {}
for col_name in columns:
try:
r[col_name] = res[col_name]
except KeyError:
pass
reordered_result.append(r)
return reordered_result
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
doc = None
@ -360,6 +338,7 @@ def export_query():
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
data['result'] = handle_duration_fieldtype_values(data.get('result'), data.get('columns'))
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
@ -367,6 +346,29 @@ def export_query():
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
def handle_duration_fieldtype_values(result, columns):
for i, col in enumerate(columns):
fieldtype = None
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if col[1]:
fieldtype = col[1]
if "/" in fieldtype:
fieldtype, options = fieldtype.split("/")
else:
fieldtype = "Data"
else:
fieldtype = col.get("fieldtype")
if fieldtype == "Duration":
for entry in range(0, len(result)):
val_in_seconds = result[entry][i]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
result[entry][i] = duration_val
return result
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
@ -384,12 +386,14 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
if isinstance(row, dict) and row:
for idx in range(len(data.columns)):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
# check if column is not hidden
if not columns[idx].get("hidden"):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
else:
row_data = row
@ -427,7 +431,7 @@ def add_total_row(result, columns, meta = None):
if i >= len(row): continue
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell):
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell):
total_row[i] = flt(total_row[i]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
@ -462,6 +466,9 @@ def add_total_row(result, columns, meta = None):
@frappe.whitelist()
def get_data_for_custom_field(doctype, field):
if not frappe.has_permission(doctype, "read"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
value_map = frappe._dict(frappe.get_all(doctype,
fields=["name", field],
as_list=1))
@ -635,31 +642,35 @@ def get_columns_dict(columns):
"""
columns_dict = frappe._dict()
for idx, col in enumerate(columns):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
col_dict = get_column_as_dict(col)
columns_dict[idx] = col_dict
columns_dict[col_dict["fieldname"]] = col_dict
return columns_dict
def get_column_as_dict(col):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
return col_dict
def get_user_match_filters(doctypes, user):
match_filters = {}

View file

@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import string_types, StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr
from frappe.utils import cstr, format_duration
@frappe.whitelist()
@ -36,6 +36,7 @@ def get_form_params():
data.pop('data', None)
data.pop('ignore_permissions', None)
data.pop('view', None)
data.pop('user', None)
if "csrf_token" in data:
del data["csrf_token"]
@ -166,6 +167,8 @@ def export_query():
for i, row in enumerate(ret):
data.append([i+1] + list(row))
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
if file_format_type == "CSV":
# convert to csv
@ -235,6 +238,29 @@ def get_labels(fields, doctype):
return labels
def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
key = field.split(" as ")[0]
if key.startswith(('count(', 'sum(', 'avg(')): continue
if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
else:
parenttype = doctype
fieldname = field.strip("`")
df = frappe.get_meta(parenttype).get_field(fieldname)
if df and df.fieldtype == 'Duration':
index = fields.index(field) + 1
for i in range(1, len(data)):
val_in_seconds = data[i][index]
if val_in_seconds:
duration_val = format_duration(val_in_seconds, df.hide_days)
data[i][index] = duration_val
return data
@frappe.whitelist()
def delete_items():
"""delete selected items"""

View file

@ -6,12 +6,11 @@ import frappe
from frappe import _
@frappe.whitelist()
def get_all_nodes(doctype, parent, tree_method, **filters):
def get_all_nodes(doctype, label, parent, tree_method, **filters):
'''Recursively gets all data from tree nodes'''
if 'cmd' in filters:
del filters['cmd']
filters.pop('data', None)
tree_method = frappe.get_attr(tree_method)
@ -20,7 +19,7 @@ def get_all_nodes(doctype, parent, tree_method, **filters):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
data = tree_method(doctype, parent, **filters)
out = [dict(parent=parent, data=data)]
out = [dict(parent=label, data=data)]
if 'is_root' in filters:
del filters['is_root']

View file

@ -3,23 +3,7 @@
frappe.ui.form.on('Auto Email Report', {
refresh: function(frm) {
if(frm.doc.report_type !== 'Report Builder') {
if(frm.script_setup_for !== frm.doc.report && !frm.doc.__islocal) {
frappe.call({
method:"frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
}
frm.trigger('fetch_report_filters');
if(!frm.is_new()) {
frm.add_custom_button(__('Download'), function() {
var w = window.open(
@ -50,6 +34,27 @@ frappe.ui.form.on('Auto Email Report', {
},
report: function(frm) {
frm.set_value('filters', '');
frm.trigger('fetch_report_filters');
},
fetch_report_filters(frm) {
if (frm.doc.report
&& frm.doc.report_type !== 'Report Builder'
&& frm.script_setup_for !== frm.doc.report
) {
frappe.call({
method: "frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
},
show_filters: function(frm) {
var wrapper = $(frm.get_field('filters_display').wrapper);

View file

@ -1,181 +1,78 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-01-09 16:39:23.746535",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2019-01-09 16:39:23.746535",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ref_doctype",
"ref_docname",
"user"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Doctype",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Doctype",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Name",
"length": 0,
"no_copy": 0,
"options": "ref_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Document Name",
"options": "ref_doctype",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"search_index": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-26 15:43:44.330348",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"name_case": "",
"owner": "Administrator",
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-17 09:19:28.496453",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -4,12 +4,13 @@
frappe.ui.form.on("Email Queue", {
refresh: function(frm) {
if (["Not Sent","Partially Sent"].indexOf(frm.doc.status)!=-1) {
frm.add_custom_button("Send Now", function() {
let button = frm.add_custom_button("Send Now", function() {
frappe.call({
method: 'frappe.email.doctype.email_queue.email_queue.send_now',
args: {
name: frm.doc.name
},
btn: button,
callback: function() {
frm.reload_doc();
}
@ -18,12 +19,13 @@ frappe.ui.form.on("Email Queue", {
}
if (["Error","Partially Errored"].indexOf(frm.doc.status)!=-1) {
frm.add_custom_button("Retry Sending", function() {
let button = frm.add_custom_button("Retry Sending", function() {
frm.call({
method: "retry_sending",
args: {
name: frm.doc.name
},
btn: button,
callback: function(r) {
if (!r.exc) {
frm.set_value("status", "Not Sent");

View file

@ -14,9 +14,6 @@ frappe.ui.form.on('Newsletter', {
});
}, "fa fa-play", "btn-success");
}
if (!doc.__islocal && cint(doc.email_sent)) {
frm.set_df_property('schedule_send', "read_only", 1);
}
frm.events.setup_dashboard(frm);

View file

@ -15,7 +15,10 @@
"email_sent",
"newsletter_content",
"subject",
"content_type",
"message",
"message_md",
"message_html",
"send_unsubscribe_link",
"send_attachments",
"published",
@ -37,8 +40,7 @@
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"label": "Sender",
"no_copy": 1
"label": "Sender"
},
{
"default": "0",
@ -50,7 +52,8 @@
},
{
"fieldname": "newsletter_content",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Content"
},
{
"fieldname": "subject",
@ -61,11 +64,12 @@
"reqd": 1
},
{
"depends_on": "eval: doc.content_type === 'Rich Text'",
"fieldname": "message",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Message",
"reqd": 1
"mandatory_depends_on": "eval: doc.content_type === 'Rich Text'"
},
{
"default": "1",
@ -87,16 +91,20 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "test_the_newsletter",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Testing"
},
{
"description": "A Lead with this Email Address should exist",
"fieldname": "test_email_id",
"fieldtype": "Data",
"label": "Test Email Address"
"label": "Test Email Address",
"options": "Email"
},
{
"depends_on": "eval: doc.test_email_id",
"fieldname": "test_send",
"fieldtype": "Button",
"label": "Test",
@ -117,7 +125,8 @@
"depends_on": "eval: doc.schedule_sending",
"fieldname": "schedule_send",
"fieldtype": "Datetime",
"label": "Schedule Send"
"label": "Schedule Send",
"read_only_depends_on": "eval: doc.email_sent"
},
{
"default": "0",
@ -125,11 +134,32 @@
"fieldtype": "Check",
"label": "Send Attachments"
},
{
"fieldname": "content_type",
"fieldtype": "Select",
"label": "Content Type",
"options": "Rich Text\nMarkdown\nHTML"
},
{
"depends_on": "eval:doc.content_type === 'Markdown'",
"fieldname": "message_md",
"fieldtype": "Markdown Editor",
"label": "Message (Markdown)",
"mandatory_depends_on": "eval:doc.content_type === 'Markdown'"
},
{
"depends_on": "eval:doc.content_type === 'HTML'",
"fieldname": "message_html",
"fieldtype": "HTML Editor",
"label": "Message (HTML)",
"mandatory_depends_on": "eval:doc.content_type === 'HTML'"
},
{
"default": "0",
"fieldname": "schedule_sending",
"fieldtype": "Check",
"label": "Schedule Sending"
"label": "Schedule Sending",
"read_only_depends_on": "eval: doc.email_sent"
}
],
"has_web_view": 1,
@ -139,7 +169,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
"modified": "2020-08-17 18:11:59.541686",
"modified": "2020-08-24 19:59:37.262500",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",

View file

@ -8,12 +8,9 @@ import frappe.utils
from frappe import throw, _
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.utils.background_jobs import enqueue
from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers
from frappe.utils import parse_addr, now_datetime
from frappe.utils import validate_email_address
from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address
class Newsletter(WebsiteGenerator):
def onload(self):
@ -29,8 +26,8 @@ class Newsletter(WebsiteGenerator):
def test_send(self, doctype="Lead"):
self.recipients = frappe.utils.split_emails(self.test_email_id)
self.queue_all()
frappe.msgprint(_("Scheduled to send to {0}").format(self.test_email_id))
self.queue_all(test_email=True)
frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
def send_emails(self):
"""send emails to leads and customers"""
@ -40,21 +37,13 @@ class Newsletter(WebsiteGenerator):
self.recipients = self.get_recipients()
if self.recipients:
if getattr(frappe.local, "is_ajax", False):
self.validate_send()
# using default queue with a longer timeout as this isn't a scheduled task
enqueue(send_newsletter, queue='default', timeout=6000, event='send_newsletter',
newsletter=self.name)
else:
self.queue_all()
frappe.msgprint(_("Scheduled to send to {0} recipients").format(len(self.recipients)))
self.queue_all()
frappe.msgprint(_("Email queued to {0} recipients").format(len(self.recipients)))
else:
frappe.msgprint(_("Newsletter should have atleast one recipient"))
def queue_all(self):
def queue_all(self, test_email=False):
if not self.get("recipients"):
# in case it is called via worker
self.recipients = self.get_recipients()
@ -80,7 +69,7 @@ class Newsletter(WebsiteGenerator):
frappe.throw(_("Unable to find attachment {0}").format(file.name))
send(recipients=self.recipients, sender=sender,
subject=self.subject, message=self.message,
subject=self.subject, message=self.get_message(),
reference_doctype=self.doctype, reference_name=self.name,
add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
unsubscribe_method="/unsubscribe",
@ -90,9 +79,18 @@ class Newsletter(WebsiteGenerator):
if not frappe.flags.in_test:
frappe.db.auto_commit_on_many_writes = False
self.db_set("email_sent", 1)
self.db_set("schedule_send", now_datetime())
self.db_set("scheduled_to_send", len(self.recipients))
if not test_email:
self.db_set("email_sent", 1)
self.db_set("schedule_send", now_datetime())
self.db_set("scheduled_to_send", len(self.recipients))
def get_message(self):
return {
'Rich Text': self.message,
'Markdown': markdown(self.message_md),
'HTML': self.message_html
}[self.content_type or 'Rich Text']
def get_recipients(self):
"""Get recipients from Email Group"""

View file

@ -1,8 +1,10 @@
frappe.listview_settings['Newsletter'] = {
add_fields: ["subject", "email_sent"],
add_fields: ["subject", "email_sent", "schedule_sending"],
get_indicator: function(doc) {
if(doc.email_sent) {
if (doc.email_sent) {
return [__("Sent"), "green", "email_sent,=,Yes"];
} else if (doc.schedule_sending) {
return [__("Scheduled"), "orange", "email_sent,=,No|schedule_sending,=,Yes"];
} else {
return [__("Not Sent"), "orange", "email_sent,=,No"];
}

View file

@ -67,6 +67,7 @@ class TestNewsletter(unittest.TestCase):
"doctype": "Newsletter",
"subject": "_Test Newsletter",
"send_from": "Test Sender <test_sender@example.com>",
"content_type": "Rich Text",
"message": "Testing my news.",
"published": published,
"schedule_sending": bool(schedule_send),

View file

@ -19,9 +19,12 @@ frappe.notification = {
}
frappe.model.with_doctype(frm.doc.document_type, function() {
let get_select_options = function(df) {
let get_select_options = function(df, parent_field) {
// Append parent_field name along with fieldname for child table fields
let select_value = parent_field ? df.fieldname + ',' + parent_field : df.fieldname;
return {
value: df.fieldname,
value: select_value,
label: df.fieldname + ' (' + __(df.label) + ')'
};
};
@ -59,9 +62,21 @@ frappe.notification = {
let receiver_fields = [];
if (frm.doc.channel === 'Email') {
receiver_fields = $.map(fields, function(d) {
return d.options == 'Email' ||
(d.options == 'User' && d.fieldtype == 'Link')
? get_select_options(d) : null;
// Add User and Email fields from child into select dropdown
if (d.fieldtype == 'Table') {
let child_fields = frappe.get_doc('DocType', d.options).fields;
return $.map(child_fields, function(df) {
return df.options == 'Email' ||
(df.options == 'User' && df.fieldtype == 'Link')
? get_select_options(df, d.fieldname) : null;
});
// Add User and Email fields from parent into select dropdown
} else {
return d.options == 'Email' ||
(d.options == 'User' && d.fieldtype == 'Link')
? get_select_options(d) : null;
}
});
} else if (in_list(['WhatsApp', 'SMS'], frm.doc.channel)) {
receiver_fields = $.map(fields, function(d) {
@ -87,7 +102,7 @@ frappe.notification = {
<h5>Message Example</h5>
<pre>
Your {{ doc.name }} order of {{ doc.total }} has shipped and should be delivered on {{ doc.date }}. Details : {{doc.customer}}
Your appointment is coming up on {{ doc.date }} at {{ doc.time }}
</pre>`;
} else if (frm.doc.channel === 'Email') {
template = `<h5>Message Example</h5>
@ -151,6 +166,7 @@ frappe.ui.form.on('Notification', {
},
refresh: function(frm) {
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
frm.get_field('is_standard').toggle(frappe.boot.developer_mode);
frm.trigger('event');
},

View file

@ -34,6 +34,7 @@
"set_property_after_alert",
"property_value",
"column_break_5",
"send_to_all_assignees",
"recipients",
"message_sb",
"message",
@ -66,7 +67,7 @@
},
{
"depends_on": "eval:doc.channel=='Slack'",
"description": "To use Slack Channel, add a <a href=\"\\#Form/Slack Webhook URL\">Slack Webhook URL</a>.",
"description": "To use Slack Channel, add a <a href=\"#List/Slack%20Webhook%20URL/List\">Slack Webhook URL</a>.",
"fieldname": "slack_webhook_url",
"fieldtype": "Link",
"label": "Slack Channel",
@ -216,7 +217,7 @@
"fieldname": "recipients",
"fieldtype": "Table",
"label": "Recipients",
"mandatory_depends_on": "eval:doc.channel!=='Slack'",
"mandatory_depends_on": "eval:doc.channel!=='Slack' && !doc.send_to_all_assignees",
"options": "Notification Recipient"
},
{
@ -268,6 +269,7 @@
"fieldname": "twilio_number",
"fieldtype": "Link",
"label": "Twilio Number",
"mandatory_depends_on": "eval: doc.channel==='WhatsApp'",
"options": "Twilio Number Group"
},
{
@ -277,11 +279,19 @@
"fieldname": "send_system_notification",
"fieldtype": "Check",
"label": "Send System Notification"
},
{
"default": "0",
"depends_on": "eval:doc.channel == 'Email'",
"fieldname": "send_to_all_assignees",
"fieldtype": "Check",
"label": "Send To All Assignees"
}
],
"icon": "fa fa-envelope",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-11 19:24:35.479373",
"modified": "2020-09-03 10:33:23.084590",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",

View file

@ -10,6 +10,7 @@ from frappe.model.document import Document
from frappe.core.doctype.role.role import get_info_based_on_role, get_user_info
from frappe.utils import validate_email_address, nowdate, parse_val, is_html, add_to_date
from frappe.utils.jinja import validate_template
from frappe.utils.safe_exec import get_safe_globals
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
@ -42,6 +43,7 @@ class Notification(Document):
self.validate_forbidden_types()
self.validate_condition()
self.validate_standard()
self.validate_twilio_settings()
frappe.cache().hdel('notifications', self.document_type)
def on_update(self):
@ -68,6 +70,11 @@ def get_context(context):
if self.is_standard and not frappe.conf.developer_mode:
frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it'))
def validate_twilio_settings(self):
if self.enabled and self.channel == "WhatsApp" \
and not frappe.db.get_single_value("Twilio Settings", "enabled"):
frappe.throw(_("Please enable Twilio settings to send WhatsApp messages"))
def validate_condition(self):
temp_doc = frappe.new_doc(self.document_type)
if self.condition:
@ -166,8 +173,13 @@ def get_context(context):
subject = frappe.render_template(self.subject, context)
attachments = self.get_attachment(doc)
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
users = recipients + cc + bcc
if not users:
return
notification_doc = {
'type': 'Alert',
@ -189,6 +201,7 @@ def get_context(context):
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
if not (recipients or cc or bcc):
return
sender = None
if self.sender and self.sender_email:
sender = formataddr((self.sender, self.sender_email))
@ -234,13 +247,20 @@ def get_context(context):
if not frappe.safe_eval(recipient.condition, None, context):
continue
if recipient.receiver_by_document_field:
email_ids_value = doc.get(recipient.receiver_by_document_field)
if validate_email_address(email_ids_value):
email_ids = email_ids_value.replace(",", "\n")
recipients = recipients + email_ids.split("\n")
fields = recipient.receiver_by_document_field.split(',')
# fields from child table
if len(fields) > 1:
for d in doc.get(fields[1]):
email_id = d.get(fields[0])
if validate_email_address(email_id):
recipients.append(email_id)
# field from parent doc
else:
email_ids_value = doc.get(fields[0])
if validate_email_address(email_ids_value):
email_ids = email_ids_value.replace(",", "\n")
recipients = recipients + email_ids.split("\n")
# else:
# print "invalid email"
if recipient.cc and "{" in recipient.cc:
recipient.cc = frappe.render_template(recipient.cc, context)
@ -262,8 +282,9 @@ def get_context(context):
for email in emails:
recipients = recipients + email.split("\n")
if not recipients and not cc and not bcc:
return None, None, None
if self.send_to_all_assignees:
recipients = recipients + get_assignees(doc)
return list(set(recipients)), list(set(cc)), list(set(bcc))
def get_receiver_list(self, doc, context):
@ -404,4 +425,13 @@ def evaluate_alert(doc, alert, event):
frappe.utils.get_link_to_form('Error Log', error_log.name)))
def get_context(doc):
return {"doc": doc, "nowdate": nowdate, "frappe": frappe._dict(utils=frappe.utils)}
return {"doc": doc, "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
def get_assignees(doc):
assignees = []
assignees = frappe.get_all('ToDo', filters={'status': 'Open', 'reference_name': doc.name,
'reference_type': doc.doctype}, fields=['owner'])
recipients = [d.owner for d in assignees]
return recipients

View file

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe, frappe.utils, frappe.utils.scheduler
from frappe.desk.form import assign_to
import unittest
test_records = frappe.get_test_records('Notification')
@ -13,7 +14,31 @@ test_dependencies = ["User"]
class TestNotification(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabEmail Queue`""")
frappe.set_user("test1@example.com")
frappe.set_user("test@example.com")
if not frappe.db.exists('Notification', {'name': 'ToDo Status Update'}, 'name'):
notification = frappe.new_doc('Notification')
notification.name = 'ToDo Status Update'
notification.subject = 'ToDo Status Update'
notification.document_type = 'ToDo'
notification.event = 'Value Change'
notification.value_changed = 'status'
notification.send_to_all_assignees = 1
notification.save()
if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'):
notification = frappe.new_doc('Notification')
notification.name = 'Contact Status Update'
notification.subject = 'Contact Status Update'
notification.document_type = 'Contact'
notification.event = 'Value Change'
notification.value_changed = 'status'
notification.message = 'Test Contact Update'
notification.append('recipients', {
'receiver_by_document_field': 'email_id,email_ids'
})
notification.save()
def tearDown(self):
frappe.set_user("Administrator")
@ -177,3 +202,65 @@ class TestNotification(unittest.TestCase):
frappe.db.sql("""delete from `tabUser` where email='test_jinja@example.com'""")
frappe.db.sql("""delete from `tabEmail Queue`""")
frappe.db.sql("""delete from `tabEmail Queue Recipient`""")
def test_notification_to_assignee(self):
todo = frappe.new_doc('ToDo')
todo.description = 'Test Notification'
todo.save()
assign_to.add({
"assign_to": ["test2@example.com"],
"doctype": todo.doctype,
"name": todo.name,
"description": "Close this Todo"
})
assign_to.add({
"assign_to": ["test1@example.com"],
"doctype": todo.doctype,
"name": todo.name,
"description": "Close this Todo"
})
#change status of todo
todo.status = 'Closed'
todo.save()
email_queue = frappe.get_doc('Email Queue', {'reference_doctype': 'ToDo',
'reference_name': todo.name})
self.assertTrue(email_queue)
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)
def test_notification_by_child_table_field(self):
contact = frappe.new_doc('Contact')
contact.first_name = 'John Doe'
contact.status = 'Open'
contact.append('email_ids', {
'email_id': 'test2@example.com',
'is_primary': 1
})
contact.append('email_ids', {
'email_id': 'test1@example.com'
})
contact.save()
#change status of contact
contact.status = 'Replied'
contact.save()
email_queue = frappe.get_doc('Email Queue', {'reference_doctype': 'Contact',
'reference_name': contact.name})
self.assertTrue(email_queue)
recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients)
self.assertTrue('test1@example.com' in recipients)

View file

@ -46,9 +46,10 @@
"options": "Role"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-02-21 11:18:40.125233",
"modified": "2020-09-01 17:40:27.289105",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification Recipient",

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