Merge branch 'develop' into patch-attach-develop
This commit is contained in:
commit
b38c051a3d
1103 changed files with 46637 additions and 49500 deletions
|
|
@ -1,2 +0,0 @@
|
|||
exclude_paths:
|
||||
- '**.sql'
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
version = 1
|
||||
|
||||
test_patterns = [
|
||||
"**/test_*.py"
|
||||
]
|
||||
|
||||
exclude_patterns = [
|
||||
"frappe/patches/**",
|
||||
"*.min.js"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "python"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
runtime_version = "3.x.x"
|
||||
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Root editor config file
|
||||
root = true
|
||||
|
||||
# Common settings
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
charset = utf-8
|
||||
|
||||
# python, js indentation settings
|
||||
[{*.py,*.js}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
|
@ -5,5 +5,4 @@ frappe/core/doctype/doctype/boilerplate/*
|
|||
frappe/core/doctype/report/boilerplate/*
|
||||
frappe/public/js/frappe/class.js
|
||||
frappe/templates/includes/*
|
||||
frappe/tests/testcafe/*
|
||||
frappe/www/website_script.js
|
||||
frappe/www/website_script.js
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
"open_url_post": true,
|
||||
"toTitle": true,
|
||||
"lstrip": true,
|
||||
"rstrip": true,
|
||||
"strip": true,
|
||||
"strip_html": true,
|
||||
"replace_all": true,
|
||||
|
|
@ -146,6 +147,7 @@
|
|||
"context": true,
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"qz": true
|
||||
"qz": true,
|
||||
"localforage": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos
|
|||
### General Issue Guidelines
|
||||
|
||||
1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created.
|
||||
2. **Report each issue separately:** Don't club multiple, unreleated issues in one note.
|
||||
2. **Report each issue separately:** Don't club multiple, unrelated issues in one note.
|
||||
3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs.
|
||||
|
||||
### Bug Report Guidelines
|
||||
|
|
|
|||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forum
|
||||
url: https://discuss.erpnext.com/
|
||||
about: For general QnA, discussions and community help.
|
||||
8
.github/helper/documentation.py
vendored
8
.github/helper/documentation.py
vendored
|
|
@ -4,7 +4,7 @@ from urllib.parse import urlparse
|
|||
|
||||
|
||||
docs_repos = [
|
||||
"frappe_docs"
|
||||
"frappe_docs",
|
||||
"erpnext_documentation",
|
||||
"erpnext_com",
|
||||
"frappe_io",
|
||||
|
|
@ -17,12 +17,12 @@ def uri_validator(x):
|
|||
|
||||
def docs_link_exists(body):
|
||||
for line in body.splitlines():
|
||||
for word in line:
|
||||
for word in line.split():
|
||||
if word.startswith('http') and uri_validator(word):
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc == "github.com":
|
||||
_, org, repo, _type, ref = parsed_url.path.split('/')
|
||||
if org == "frappe" and repo in docs_repos:
|
||||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
36
.github/helper/translation.py
vendored
36
.github/helper/translation.py
vendored
|
|
@ -2,8 +2,11 @@ import re
|
|||
import sys
|
||||
|
||||
errors_encounter = 0
|
||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
|
||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
|
||||
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
|
||||
f_string_pattern = re.compile(r"_\(f[\"']")
|
||||
starts_with_f_pattern = re.compile(r"_\(f")
|
||||
|
||||
# skip first argument
|
||||
files = sys.argv[1:]
|
||||
|
|
@ -14,21 +17,44 @@ for _file in files_to_scan:
|
|||
print(f'Checking: {_file}')
|
||||
file_lines = f.readlines()
|
||||
for line_number, line in enumerate(file_lines, 1):
|
||||
if 'frappe-lint: disable-translate' in line:
|
||||
continue
|
||||
|
||||
start_matches = start_pattern.search(line)
|
||||
if start_matches:
|
||||
starts_with_f = starts_with_f_pattern.search(line)
|
||||
|
||||
if starts_with_f:
|
||||
has_f_string = f_string_pattern.search(line)
|
||||
if has_f_string:
|
||||
errors_encounter += 1
|
||||
print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}')
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
match = pattern.search(line)
|
||||
if not match and line.endswith(',\n'):
|
||||
error_found = False
|
||||
|
||||
if not match and line.endswith((',\n', '[\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:
|
||||
error_found = True
|
||||
print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}')
|
||||
|
||||
if not error_found and not words_pattern.search(line):
|
||||
error_found = True
|
||||
print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}')
|
||||
|
||||
if error_found:
|
||||
errors_encounter += 1
|
||||
print(f'\nTranslation syntax error at line number: {line_number + 1}\n{line.strip()[:100]}')
|
||||
|
||||
if errors_encounter > 0:
|
||||
print('\nYou can visit "https://frappeframework.com/docs/user/en/translations" to resolve this error.')
|
||||
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
print('\nGood To Go!')
|
||||
|
|
|
|||
3
.github/workflows/docker-release.yml
vendored
3
.github/workflows/docker-release.yml
vendored
|
|
@ -1,9 +1,10 @@
|
|||
name: Trigger Docker build on release
|
||||
name: 'Trigger Docker build on release'
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
curl:
|
||||
name: 'Trigger Docker build on release'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: alpine:latest
|
||||
|
|
|
|||
5
.github/workflows/docs-checker.yml
vendored
5
.github/workflows/docs-checker.yml
vendored
|
|
@ -1,10 +1,11 @@
|
|||
name: 'Documentation Required'
|
||||
name: 'Documentation Check'
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, edited ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docs-required:
|
||||
name: 'Documentation Required'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
|||
5
.github/workflows/publish-assets-develop.yml
vendored
5
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -1,11 +1,12 @@
|
|||
name: Build and Publish Assets for Development
|
||||
name: 'Frappe Assets'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-dev-and-publish:
|
||||
name: 'Build and Publish Assets for Development'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
name: Build and Publish Assets built for Releases
|
||||
name: 'Frappe Assets'
|
||||
|
||||
on:
|
||||
release:
|
||||
|
|
@ -8,7 +8,8 @@ env:
|
|||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-release-and-publish:
|
||||
name: 'Build and Publish Assets built for Releases'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
@ -44,4 +45,3 @@ jobs:
|
|||
asset_path: build/assets.tar.gz
|
||||
asset_name: assets.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -7,7 +7,7 @@ locale
|
|||
*.swp
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
# build/
|
||||
frappe/docs/current
|
||||
.vscode
|
||||
node_modules
|
||||
|
|
@ -28,7 +28,7 @@ __pycache__/
|
|||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
# build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pull_request_rules:
|
|||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label!=dont-merge
|
||||
- label!=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
|
|
@ -14,10 +14,9 @@ pull_request_rules:
|
|||
- name: Automatic squash on CI success and review
|
||||
conditions:
|
||||
- status-success=Sider
|
||||
- status-success=Semantic Pull Request
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=security/snyk (frappe)
|
||||
- label!=don't-merge
|
||||
- label!=dont-merge
|
||||
- label=squash
|
||||
- "#approved-reviews-by>=1"
|
||||
actions:
|
||||
|
|
|
|||
9
.stylelintrc
Normal file
9
.stylelintrc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": ["stylelint-config-recommended"],
|
||||
"plugins": ["stylelint-scss"],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null,
|
||||
"scss/at-rule-no-unknown": true,
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
17
.travis.yml
17
.travis.yml
|
|
@ -43,7 +43,6 @@ matrix:
|
|||
env: DB=mariadb TYPE=ui
|
||||
before_script:
|
||||
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
- bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard
|
||||
script: bench --site test_site run-ui-tests frappe --headless
|
||||
|
||||
before_install:
|
||||
|
|
@ -75,8 +74,10 @@ install:
|
|||
- mkdir ~/frappe-bench/sites/test_site
|
||||
- cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/site_config.json
|
||||
|
||||
- mkdir ~/frappe-bench/sites/test_site_producer
|
||||
- cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json
|
||||
- if [ $TYPE == "server" ]; then
|
||||
mkdir ~/frappe-bench/sites/test_site_producer;
|
||||
cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json;
|
||||
fi
|
||||
|
||||
- if [ $DB == "mariadb" ];then
|
||||
mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
|
|
@ -104,11 +105,11 @@ install:
|
|||
|
||||
- cd ./frappe-bench
|
||||
|
||||
- sed -i 's/watch:/# watch:/g' Procfile
|
||||
- sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
- sed -i 's/^watch:/# watch:/g' Procfile
|
||||
- sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
|
|
@ -119,7 +120,7 @@ install:
|
|||
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
- bench --site test_site_producer reinstall --yes
|
||||
- if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
- bench build --app frappe
|
||||
|
||||
after_script:
|
||||
|
|
|
|||
14
CODEOWNERS
14
CODEOWNERS
|
|
@ -4,14 +4,14 @@
|
|||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
website/ @prssanna
|
||||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @nextchamp-saqib
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
email/ @saurabh6790
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
|
||||
## Contributing
|
||||
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Pull-Request-Guidelines)
|
||||
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
1. [Translations](https://translate.erpnext.com)
|
||||
|
||||
### Website
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# stolen from http://cgit.drupalcode.org/octopus/commit/?id=db4f837
|
||||
includedir=`mysql_config --variable=pkgincludedir`
|
||||
thiscwd=`pwd`
|
||||
_THIS_DB_VERSION=`mysql -V 2>&1 | tr -d "\n" | cut -d" " -f6 | awk '{ print $1}' | cut -d"-" -f1 | awk '{ print $1}' | sed "s/[\,']//g"`
|
||||
if [ "$_THIS_DB_VERSION" = "5.5.40" ] && [ ! -e "$includedir-$_THIS_DB_VERSION-fixed.log" ] ; then
|
||||
cd $includedir
|
||||
sudo patch -p1 < $thiscwd/ci/my_config.h.patch &> /dev/null
|
||||
sudo touch $includedir-$_THIS_DB_VERSION-fixed.log
|
||||
fi
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
diff -burp a/my_config.h b/my_config.h
|
||||
--- a/my_config.h 2014-10-09 19:32:46.000000000 -0400
|
||||
+++ b/my_config.h 2014-10-09 19:35:12.000000000 -0400
|
||||
@@ -641,17 +641,4 @@
|
||||
#define SIZEOF_TIME_T 8
|
||||
/* #undef TIME_T_UNSIGNED */
|
||||
|
||||
-/*
|
||||
- stat structure (from <sys/stat.h>) is conditionally defined
|
||||
- to have different layout and size depending on the defined macros.
|
||||
- The correct macro is defined in my_config.h, which means it MUST be
|
||||
- included first (or at least before <features.h> - so, practically,
|
||||
- before including any system headers).
|
||||
-
|
||||
- __GLIBC__ is defined in <features.h>
|
||||
-*/
|
||||
-#ifdef __GLIBC__
|
||||
-#error <my_config.h> MUST be included first!
|
||||
-#endif
|
||||
-
|
||||
#endif
|
||||
|
||||
|
|
@ -3,5 +3,9 @@
|
|||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
"pageLoadTimeout": 15000,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ context('API Resources', () => {
|
|||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Creates two Comments', () => {
|
||||
cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"});
|
||||
cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"});
|
||||
cy.insert_doc('Comment', { comment_type: 'Comment', content: "hello" });
|
||||
cy.insert_doc('Comment', { comment_type: 'Comment', content: "world" });
|
||||
});
|
||||
|
||||
it('Lists the Comments', () => {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ context('Awesome Bar', () => {
|
|||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('.navbar-header .navbar-home').click();
|
||||
cy.get('.navbar .navbar-home').click();
|
||||
});
|
||||
|
||||
it('navigates to doctype list', () => {
|
||||
|
|
@ -14,16 +14,16 @@ context('Awesome Bar', () => {
|
|||
cy.get('#navbar-search + ul').should('be.visible');
|
||||
cy.get('#navbar-search').type('{downarrow}{enter}', { delay: 100 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.location('hash').should('eq', '#List/ToDo/List');
|
||||
cy.location('pathname').should('eq', '/app/todo');
|
||||
});
|
||||
|
||||
it('find text in doctype list', () => {
|
||||
cy.get('#navbar-search')
|
||||
.type('test in todo{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('h1').should('contain', 'To Do');
|
||||
cy.get('.title-text').should('contain', 'To Do');
|
||||
|
||||
cy.get('[data-original-title="Name"] > .input-with-feedback')
|
||||
.should('have.value', '%test%');
|
||||
|
|
@ -33,7 +33,7 @@ context('Awesome Bar', () => {
|
|||
cy.get('#navbar-search')
|
||||
.type('new blog post{downarrow}{enter}', { delay: 200 });
|
||||
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post 1');
|
||||
cy.get('.title-text:visible').should('have.text', 'New Blog Post');
|
||||
});
|
||||
|
||||
it('calculates math expressions', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('Control Barcode', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_barcode() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
context('Control Duration', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_duration(hide_days=0, hide_seconds=0) {
|
||||
function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) {
|
||||
return cy.dialog({
|
||||
title: 'Duration',
|
||||
fields: [{
|
||||
|
|
@ -22,11 +22,11 @@ context('Control Duration', () => {
|
|||
.first()
|
||||
.click();
|
||||
cy.get('.duration-input[data-duration=days]')
|
||||
.type(45, {force: true})
|
||||
.blur({force: true});
|
||||
.type(45, { force: true })
|
||||
.blur({ force: true });
|
||||
cy.get('.duration-input[data-duration=minutes]')
|
||||
.type(30)
|
||||
.blur({force: true});
|
||||
.blur({ force: true });
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().should('have.value', '45d 30m');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().blur();
|
||||
cy.get('.duration-picker').should('not.be.visible');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
context('Control Link', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
|
|
@ -29,8 +29,7 @@ context('Control Link', () => {
|
|||
it('should set the valid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
|
|
@ -50,8 +49,7 @@ context('Control Link', () => {
|
|||
it('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
|
|
@ -63,9 +61,8 @@ context('Control Link', () => {
|
|||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.get('.frappe-control[data-fieldname=link] input').as('input');
|
||||
|
|
@ -77,7 +74,7 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=link] .link-btn')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.location('hash').should('eq', `#Form/ToDo/${todos[0]}`);
|
||||
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('Control Rating', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_rating() {
|
||||
|
|
@ -18,7 +18,7 @@ context('Control Rating', () => {
|
|||
get_dialog_with_rating().as('dialog');
|
||||
|
||||
cy.get('div.rating')
|
||||
.children('i.fa')
|
||||
.children('svg')
|
||||
.first()
|
||||
.click()
|
||||
.should('have.class', 'star-click');
|
||||
|
|
@ -33,11 +33,11 @@ context('Control Rating', () => {
|
|||
get_dialog_with_rating();
|
||||
|
||||
cy.get('div.rating')
|
||||
.children('i.fa')
|
||||
.children('svg')
|
||||
.first()
|
||||
.invoke('trigger', 'mouseenter')
|
||||
.should('have.class', 'star-hover')
|
||||
.invoke('trigger', 'mouseleave')
|
||||
.should('not.have.class', 'star-hover');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
36
cypress/integration/control_select.js
Normal file
36
cypress/integration/control_select.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
context('Control Select', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
function get_dialog_with_select() {
|
||||
return cy.dialog({
|
||||
title: 'Select',
|
||||
fields: [{
|
||||
'fieldname': 'select_control',
|
||||
'fieldtype': 'Select',
|
||||
'placeholder': 'Select an Option',
|
||||
'options': ['', 'Option 1', 'Option 2', 'Option 2'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('toggles placholder on clicking an option', () => {
|
||||
get_dialog_with_select().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=select_control] .control-input').as('control');
|
||||
cy.get('.frappe-control[data-fieldname=select_control] .control-input select').as('select');
|
||||
cy.get('@control').get('.select-icon').should('exist');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
cy.get('@select').select('Option 1');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'none');
|
||||
cy.get('@select').invoke('val', '');
|
||||
cy.get('@control').get('.placeholder').should('have.css', 'display', 'block');
|
||||
|
||||
|
||||
cy.get('@dialog').then(dialog => {
|
||||
dialog.hide();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name;
|
|||
context('Control Date, Time and DateTime', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', datetime_doctype, true);
|
||||
});
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ context('Control Date, Time and DateTime', () => {
|
|||
.should('be.visible');
|
||||
cy.get(
|
||||
'.datepickers-container .datepicker.active .datepicker--cell-day.-current-'
|
||||
).click();
|
||||
).click({ force: true });
|
||||
|
||||
cy.window()
|
||||
.its('cur_frm')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,33 @@
|
|||
context('Depends On', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
|
||||
name: 'Child Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
"label": "Child Test Field",
|
||||
"fieldname": "child_test_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Dependant Field",
|
||||
"fieldname": "child_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
{
|
||||
"label": "Child Display Dependant Field",
|
||||
"fieldname": "child_display_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
},
|
||||
]
|
||||
});
|
||||
}).then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -24,6 +48,13 @@ context('Depends On', () => {
|
|||
"fieldtype": "Data",
|
||||
'depends_on': "eval:doc.test_field=='Value'"
|
||||
},
|
||||
{
|
||||
"label": "Child Test Depends On Field",
|
||||
"fieldname": "child_test_depends_on_field",
|
||||
"fieldtype": "Table",
|
||||
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
|
||||
'options': "Child Test Depends On"
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
@ -33,7 +64,7 @@ context('Depends On', () => {
|
|||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.get('body').click();
|
||||
cy.hide_dialog();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
|
|
@ -48,6 +79,30 @@ context('Depends On', () => {
|
|||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
|
||||
});
|
||||
it('should set the table and its fields as read only depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('dependant_field', 'Some Value');
|
||||
//cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('[data-idx="1"]').as('row1');
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
|
||||
//cy.get('@row1-form_in_grid').find('')
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
|
||||
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.grid-collapse-row').click();
|
||||
|
||||
// set the table to read-only
|
||||
cy.fill_field('test_field', 'Some Other Value');
|
||||
|
||||
// grid row form fields should be read-only
|
||||
cy.get('@row1').find('.btn-open-row').click();
|
||||
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
|
||||
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
|
||||
});
|
||||
it('should display the field depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
context('FileUploader', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
function open_upload_dialog() {
|
||||
|
|
@ -19,40 +19,36 @@ context('FileUploader', () => {
|
|||
it('should accept dropped files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.fixture('example.json').then(fileContent => {
|
||||
cy.get_open_dialog().find('.file-upload-area').upload(
|
||||
{ fileContent, fileName: 'example.json', mimeType: 'application/json' },
|
||||
{ subjectType: 'drag-n-drop' },
|
||||
);
|
||||
cy.get_open_dialog().find('.file-info').should('contain', 'example.json');
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('status').should('be', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
cy.get_open_dialog().find('.file-upload-area').attachFile('example.json', {
|
||||
subjectType: 'drag-n-drop',
|
||||
});
|
||||
|
||||
cy.get_open_dialog().find('.file-name').should('contain', 'example.json');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-modal-primary').click();
|
||||
cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should accept uploaded files', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("uploaded file")').click();
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click();
|
||||
cy.get('.file-filter').type('example.json');
|
||||
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', '/private/files/example.json');
|
||||
.should('have.property', 'file_name', 'example.json');
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
it('should accept web links', () => {
|
||||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("web link")').click();
|
||||
cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.intercept('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
cy.wait('@upload_file').its('response.body.message')
|
||||
.should('have.property', 'file_url', 'https://github.com');
|
||||
|
|
|
|||
|
|
@ -1,56 +1,50 @@
|
|||
context('Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
|
||||
});
|
||||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
||||
cy.visit('/app/todo/new');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.wait(300);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.server();
|
||||
cy.route({
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.form.save.savedocs'
|
||||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('status').should('eq', 200);
|
||||
cy.visit('/desk#List/ToDo');
|
||||
cy.location('hash').should('eq', '#List/ToDo/List');
|
||||
cy.get('h1').should('be.visible').and('contain', 'To Do');
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
it('navigates between documents with child table list filters applied', () => {
|
||||
cy.visit('/desk#List/Contact');
|
||||
cy.location('hash').should('eq', '#List/Contact/List');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
|
||||
cy.visit('/app/contact');
|
||||
cy.add_filter();
|
||||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
|
||||
cy.visit('/desk#Form/Contact/Test Form Contact 3');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.visit('/app/contact/Test Form Contact 3');
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.get('.btn-modal-close:visible').click();
|
||||
cy.hide_dialog();
|
||||
cy.get('.next-doc').click();
|
||||
cy.wait(200);
|
||||
cy.hide_dialog();
|
||||
cy.contains('Test Form Contact 2').should('not.exist');
|
||||
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
|
||||
cy.get('.title-text').should('contain', 'Test Form Contact 3');
|
||||
// clear filters
|
||||
cy.window().its('frappe').then((frappe) => {
|
||||
let list_view = frappe.get_list_view('Contact');
|
||||
list_view.filter_area.filter_list.clear_filters();
|
||||
});
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
});
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let expectBackgroundColor = 'rgb(255, 220, 220)';
|
||||
let expectBackgroundColor = 'rgb(255, 245, 245)';
|
||||
|
||||
cy.visit('/desk#Form/Contact/New Contact 1');
|
||||
cy.visit('/app/contact/new');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
context('Grid Pagination', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
|
||||
});
|
||||
});
|
||||
it('creates pages for child table', () => {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
|
||||
});
|
||||
it('goes to the next and previous page', () => {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.next-page').click();
|
||||
cy.get('@table').find('.current-page-number').should('contain', '2');
|
||||
|
|
@ -27,21 +27,21 @@ context('Grid Pagination', () => {
|
|||
cy.get('@table').find('.current-page-number').should('contain', '1');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
|
||||
});
|
||||
it('adds and deletes rows and changes page', ()=> {
|
||||
cy.visit('/desk#Form/Contact/Test Contact');
|
||||
it('adds and deletes rows and changes page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '21');
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true});
|
||||
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({ force: true });
|
||||
cy.get('@table').find('button.grid-remove-rows').click();
|
||||
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
|
||||
cy.get('@table').find('.current-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
});
|
||||
// it('deletes all rows', ()=> {
|
||||
// cy.visit('/desk#Form/Contact/Test Contact');
|
||||
// cy.visit('/app/contact/Test Contact');
|
||||
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
// cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
|
||||
// cy.get('@table').find('button.grid-remove-all-rows').click();
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
context('List View', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
|
||||
});
|
||||
});
|
||||
it('enables "Actions" button', () => {
|
||||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete'];
|
||||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete'];
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.btn.btn-primary.btn-sm.dropdown-toggle').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible').should('have.length', 7).each((el, index) => {
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => {
|
||||
cy.wrap(el).contains(actions[index]);
|
||||
}).then((elements) => {
|
||||
cy.server();
|
||||
cy.route({
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url:'api/method/frappe.model.workflow.bulk_workflow_approval'
|
||||
url: 'api/method/frappe.model.workflow.bulk_workflow_approval'
|
||||
}).as('bulk-approval');
|
||||
cy.route({
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url:'api/method/frappe.desk.reportview.get'
|
||||
url: 'api/method/frappe.desk.reportview.get'
|
||||
}).as('real-time-update');
|
||||
cy.wrap(elements).contains('Approve').click();
|
||||
cy.wait(['@bulk-approval', '@real-time-update']);
|
||||
cy.hide_dialog();
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row-container:visible').should('contain', 'Approved');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
context('List View Settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
it('Default settings', () => {
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.get('.list-count').should('contain', "20 of");
|
||||
cy.get('.sidebar-stat').should('contain', "Tags");
|
||||
cy.get('.list-stats').should('contain', "Tags");
|
||||
});
|
||||
it('disable count and sidebar stats then verify', () => {
|
||||
cy.wait(300);
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.wait(300);
|
||||
cy.get('.list-count').should('contain', "20 of");
|
||||
cy.get('button').contains('Menu').click();
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'Settings');
|
||||
cy.get('.menu-btn-group button').click();
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
|
||||
cy.get('input[data-fieldname="disable_count"]').check({force: true});
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true});
|
||||
cy.get('input[data-fieldname="disable_count"]').check({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
|
||||
cy.reload();
|
||||
cy.reload({ force: true });
|
||||
|
||||
cy.get('.list-count').should('be.empty');
|
||||
cy.get('.list-sidebar .sidebar-stat').should('not.exist');
|
||||
cy.get('.list-sidebar .list-tags').should('not.exist');
|
||||
|
||||
cy.get('button').contains('Menu').click({force: true});
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'Settings');
|
||||
cy.get('input[data-fieldname="disable_count"]').uncheck({force: true});
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({force: true});
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click();
|
||||
cy.get('.modal-dialog').should('contain', 'DocType Settings');
|
||||
cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true });
|
||||
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true });
|
||||
cy.get('button').filter(':visible').contains('Save').click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ context('Login', () => {
|
|||
beforeEach(() => {
|
||||
cy.request('/api/method/logout');
|
||||
cy.visit('/login');
|
||||
cy.location().should('be', '/login');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('greets with login screen', () => {
|
||||
|
|
@ -11,13 +11,13 @@ context('Login', () => {
|
|||
|
||||
it('validates password', () => {
|
||||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
it('validates email', () => {
|
||||
cy.get('#login_password').type('qwe');
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -25,8 +25,8 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type('qwer');
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.page-card-head').contains('Invalid Login. Try again.');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.get('.btn-login:visible').contains('Invalid Login. Try again.');
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
});
|
||||
|
||||
|
|
@ -34,8 +34,8 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.location('pathname').should('eq', '/desk');
|
||||
cy.get('.btn-login:visible').click();
|
||||
cy.location('pathname').should('eq', '/app');
|
||||
cy.window().its('frappe.session.user').should('eq', 'Administrator');
|
||||
});
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ context('Login', () => {
|
|||
cy.get('#login_email').type('Administrator');
|
||||
cy.get('#login_password').type(Cypress.config('adminPassword'));
|
||||
|
||||
cy.get('.btn-login').click();
|
||||
cy.get('.btn-login:visible').click();
|
||||
|
||||
// verify redirected location and url params after login
|
||||
cy.url().should('include', '/me?' + payload.toString().replace('+', '%20'));
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
context('Query Report', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('add custom column in report', () => {
|
||||
cy.visit('/desk#query-report/Permitted Documents For User');
|
||||
cy.visit('/app/query-report/Permitted Documents For User');
|
||||
|
||||
cy.get('div[class="page-form flex"]', {timeout: 60000}).should('have.length', 1).then(()=>{
|
||||
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
|
||||
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 });
|
||||
|
||||
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.wait(300);
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
|
||||
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
|
||||
|
||||
cy.get('.datatable').should('exist');
|
||||
cy.get('button').contains('Menu').click({force: true});
|
||||
cy.get('.dropdown-menu li').contains('Add Column').click({force: true});
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Add Column');
|
||||
cy.get('select[data-fieldname="doctype"]').select("Role", {force: true});
|
||||
cy.get('select[data-fieldname="field"]').select("Role Name", {force: true});
|
||||
cy.get('select[data-fieldname="insert_after"]').select("Name", {force: true});
|
||||
cy.get('button').contains('Submit').click({force: true});
|
||||
cy.get('button').contains('Menu').click({force: true});
|
||||
cy.get('.dropdown-menu li').contains('Save').click({force: true});
|
||||
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
|
||||
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
|
||||
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
|
||||
cy.get('button').contains('Submit').click({ force: true });
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type("Test Report", {delay:100, force: true});
|
||||
cy.get('button').contains('Submit').click({timeout:1000, force: true});
|
||||
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
|
||||
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,17 +4,17 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Navigate to Recorder', () => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('recorder');
|
||||
cy.get('h1').should('contain', 'Recorder');
|
||||
cy.location('hash').should('eq', '#recorder');
|
||||
cy.get('h3').should('contain', 'Recorder');
|
||||
cy.url().should('include', '/recorder/detail');
|
||||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.visit('/desk#recorder');
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
|
||||
cy.get('.indicator').should('contain', 'Inactive').should('have.class', 'red');
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
||||
cy.get('.primary-action').should('contain', 'Start');
|
||||
cy.get('.btn-secondary').should('contain', 'Clear');
|
||||
|
|
@ -24,53 +24,49 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.visit('/desk#recorder');
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.indicator').should('contain', 'Active').should('have.class', 'green');
|
||||
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'No Requests');
|
||||
|
||||
cy.server();
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/desk#recorder');
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(500);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.visit('/desk#recorder');
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
|
||||
cy.server();
|
||||
cy.visit('/desk#List/DocType/List');
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
// temporarily commenting out theses tests as they seem to be
|
||||
// randomly failing maybe due a backround event
|
||||
cy.visit('/app/recorder');
|
||||
|
||||
// cy.visit('/desk#recorder');
|
||||
cy.get('.list-row-container span').contains('/api/method/frappe').click();
|
||||
|
||||
// cy.get('.list-row-container span').contains('/api/method/frappe').click();
|
||||
cy.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
|
||||
// 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');
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(200);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -4,46 +4,44 @@ context('Relative Timeframe', () => {
|
|||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_todo_records");
|
||||
});
|
||||
});
|
||||
it('sets relative timespan filter for last week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area').should('exist');
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("last week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row-container').should('contain', 'this is second todo');
|
||||
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.get('.remove-filter.btn').click();
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
it('sets relative timespan filter for next week and filters list', () => {
|
||||
cy.visit('/desk#List/ToDo/List');
|
||||
cy.visit('/app/List/ToDo/List');
|
||||
cy.clear_filters();
|
||||
cy.get('.list-row:contains("this is fourth todo")').should('exist');
|
||||
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
|
||||
cy.add_filter();
|
||||
cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 });
|
||||
cy.get('select.condition.form-control').select("Timespan");
|
||||
cy.get('.filter-field select.input-with-feedback.form-control').select("next week");
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-box .btn:contains("Apply")').click();
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.get('.filter-popover .apply-filters').click({ force: true });
|
||||
cy.wait('@list_refresh');
|
||||
cy.get('.list-row-container').its('length').should('eq', 1);
|
||||
cy.get('.list-row').should('contain', 'this is first todo');
|
||||
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save')
|
||||
.as('save_user_settings');
|
||||
cy.get('.remove-filter.btn').click();
|
||||
cy.clear_filters();
|
||||
cy.wait('@save_user_settings');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name;
|
|||
context('Report View', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
cy.clear_cache();
|
||||
cy.insert_doc(doctype_name, {
|
||||
|
|
@ -16,15 +16,14 @@ context('Report View', () => {
|
|||
}, true).as('doc');
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.server();
|
||||
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/desk#List/${doctype_name}/Report`);
|
||||
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/app/List/${doctype_name}/Report`);
|
||||
// check status column added from docstatus
|
||||
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
|
||||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.find('input[data-fieldname="enabled"]').check({force: true});
|
||||
cell.find('input[data-fieldname="enabled"]').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
|
|
|
|||
|
|
@ -8,20 +8,19 @@ context('Table MultiSelect', () => {
|
|||
it('select value from multiselect dropdown', () => {
|
||||
cy.new_form('Assignment Rule');
|
||||
cy.fill_field('__newname', name);
|
||||
cy.fill_field('document_type', 'ToDo');
|
||||
cy.fill_field('document_type', 'Blog Post');
|
||||
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
|
||||
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||
cy.get('@input').type('test{enter}', { delay: 100 });
|
||||
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value')
|
||||
.first().as('selected-value');
|
||||
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value .btn-link-to-form')
|
||||
.as('selected-value');
|
||||
cy.get('@selected-value').should('contain', 'test@erpnext.com');
|
||||
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
|
||||
// trigger save
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@save_form').its('status').should('eq', 200);
|
||||
cy.wait('@save_form').its('response.statusCode').should('eq', 200);
|
||||
cy.get('@selected-value').should('contain', 'test@erpnext.com');
|
||||
});
|
||||
|
||||
|
|
@ -46,6 +45,6 @@ context('Table MultiSelect', () => {
|
|||
cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click();
|
||||
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value');
|
||||
cy.get('@existing_value').find('.btn-link-to-form').click();
|
||||
cy.location('hash').should('contain', 'Form/User/test@erpnext.com');
|
||||
cy.location('pathname').should('contain', '/user/test@erpnext.com');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
|
|||
|
||||
Cypress.Commands.add('create_records', doc => {
|
||||
return cy
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, { waitForAnimations: false, force: true });
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
|
@ -204,19 +204,54 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
|||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
|
||||
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');
|
||||
|
||||
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
|
||||
cy.get('@input').click().wait(200);
|
||||
cy.get('.datepickers-container .datepicker.active').should('exist');
|
||||
}
|
||||
if (fieldtype === 'Time') {
|
||||
cy.get('@input').clear().wait(200);
|
||||
}
|
||||
|
||||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, {waitForAnimations: false, force: true});
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
|
||||
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
|
||||
selector += ` [data-idx="${row_idx}"]`;
|
||||
selector += ` .form-in-grid`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
|
||||
} else if (fieldtype === 'Code') {
|
||||
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
|
||||
} else {
|
||||
selector += ` .form-control[data-fieldname="${fieldname}"]`;
|
||||
}
|
||||
|
||||
return cy.get(selector);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('awesomebar', text => {
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
|
||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('new_form', doctype => {
|
||||
let route = `Form/${doctype}/New ${doctype} 1`;
|
||||
cy.visit(`/desk#${route}`);
|
||||
cy.get('body').should('have.attr', 'data-route', route);
|
||||
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-');
|
||||
cy.visit(`/app/${dt_in_route}/new`);
|
||||
cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`);
|
||||
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('go_to_list', doctype => {
|
||||
cy.visit(`/desk#List/${doctype}/List`);
|
||||
cy.visit(`/app/list/${doctype}/list`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_cache', () => {
|
||||
|
|
@ -240,9 +275,8 @@ Cypress.Commands.add('get_open_dialog', () => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('hide_dialog', () => {
|
||||
cy.get_open_dialog()
|
||||
.find('.btn-modal-close')
|
||||
.click();
|
||||
cy.wait(300);
|
||||
cy.get_open_dialog().find('.btn-modal-close').click();
|
||||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
|
|
@ -272,4 +306,21 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
|||
return res.body.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('add_filter', () => {
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.wait(300);
|
||||
cy.get('.filter-popover').should('exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_filters', () => {
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.wait(300);
|
||||
cy.get('.filter-popover').should('exist');
|
||||
cy.get('.filter-popover').find('.clear-filters').click();
|
||||
cy.get('.filter-section .filter-button').click();
|
||||
cy.window().its('cur_list').then(cur_list => {
|
||||
cur_list && cur_list.filter_area && cur_list.filter_area.clear();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,5 +21,5 @@ import './commands';
|
|||
// require('./commands')
|
||||
|
||||
Cypress.Cookies.defaults({
|
||||
whitelist: 'sid'
|
||||
preserve: 'sid'
|
||||
});
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
"""
|
||||
globals attached to frappe module
|
||||
+ some utility functions that should probably be moved
|
||||
Frappe - Low Code Open Source Framework in Python and JS
|
||||
|
||||
Frappe, pronounced fra-pay, is a full stack, batteries-included, web
|
||||
framework written in Python and Javascript with MariaDB as the database.
|
||||
It is the framework which powers ERPNext. It is pretty generic and can
|
||||
be used to build database driven apps.
|
||||
|
||||
Read the documentation: https://frappeframework.com/docs
|
||||
"""
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
|
|
@ -11,7 +17,6 @@ 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
|
||||
from .exceptions import *
|
||||
|
|
@ -27,6 +32,7 @@ __version__ = '13.0.0-dev'
|
|||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
controllers = {}
|
||||
|
||||
class _dict(dict):
|
||||
"""dict like object that exposes keys as attributes"""
|
||||
|
|
@ -148,6 +154,7 @@ def init(site, sites_path=None, new_site=False):
|
|||
"new_site": new_site
|
||||
})
|
||||
local.rollback_observers = []
|
||||
local.before_commit = []
|
||||
local.test_objects = {}
|
||||
|
||||
local.site = site
|
||||
|
|
@ -188,17 +195,20 @@ def init(site, sites_path=None, new_site=False):
|
|||
|
||||
local.initialised = True
|
||||
|
||||
def connect(site=None, db_name=None):
|
||||
def connect(site=None, db_name=None, set_admin_as_user=True):
|
||||
"""Connect to site database instance.
|
||||
|
||||
:param site: If site is given, calls `frappe.init`.
|
||||
:param db_name: Optional. Will use from `site_config.json`."""
|
||||
:param db_name: Optional. Will use from `site_config.json`.
|
||||
:param set_admin_as_user: Set Administrator as current user.
|
||||
"""
|
||||
from frappe.database import get_db
|
||||
if site:
|
||||
init(site)
|
||||
|
||||
local.db = get_db(user=db_name or local.conf.db_name)
|
||||
set_user("Administrator")
|
||||
if set_admin_as_user:
|
||||
set_user("Administrator")
|
||||
|
||||
def connect_replica():
|
||||
from frappe.database import get_db
|
||||
|
|
@ -312,7 +322,7 @@ def log(msg):
|
|||
|
||||
debug_log.append(as_unicode(msg))
|
||||
|
||||
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
|
||||
def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, indicator=None, alert=False, primary_action=None, is_minimizable=None, wide=None):
|
||||
"""Print a message to the user (via HTTP response).
|
||||
Messages are sent in the `__server_messages` property in the
|
||||
response JSON and shown in a pop-up / modal.
|
||||
|
|
@ -321,11 +331,12 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
:param title: [optional] Message title.
|
||||
:param raise_exception: [optional] Raise given exception and show message.
|
||||
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
|
||||
:param as_list: [optional] If `msg` is a list, render as un-ordered list.
|
||||
:param primary_action: [optional] Bind a primary server/client side action.
|
||||
:param is_minimizable: [optional] Allow users to minimize the modal
|
||||
:param wide: [optional] Show wide modal
|
||||
"""
|
||||
from frappe.utils import encode
|
||||
from frappe.utils import strip_html_tags
|
||||
|
||||
msg = safe_decode(msg)
|
||||
out = _dict(message=msg)
|
||||
|
|
@ -346,19 +357,13 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
return
|
||||
|
||||
if as_table and type(msg) in (list, tuple):
|
||||
out.as_table = 1
|
||||
|
||||
table_rows = ''
|
||||
for row in msg:
|
||||
table_row_data = ''
|
||||
for data in row:
|
||||
table_row_data += '<td>{}</td>'.format(data)
|
||||
table_rows += '<tr>{}</tr>'.format(table_row_data)
|
||||
|
||||
out.message = '''<table class="table table-bordered"
|
||||
style="margin: 0;">{}</table>'''.format(table_rows)
|
||||
if as_list and type(msg) in (list, tuple) and len(msg) > 1:
|
||||
out.as_list = 1
|
||||
|
||||
if flags.print_messages and out.message:
|
||||
print(f"Message: {repr(out.message).encode('utf-8')}")
|
||||
print(f"Message: {strip_html_tags(out.message)}")
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
|
|
@ -405,12 +410,12 @@ def clear_last_message():
|
|||
if len(local.message_log) > 0:
|
||||
local.message_log = local.message_log[:-1]
|
||||
|
||||
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None):
|
||||
def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None, as_list=False):
|
||||
"""Throw execption and show message (`msgprint`).
|
||||
|
||||
:param msg: Message.
|
||||
:param exc: Exception class. Default `frappe.ValidationError`"""
|
||||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide)
|
||||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
if user == False:
|
||||
|
|
@ -465,11 +470,11 @@ def get_request_header(key, default=None):
|
|||
|
||||
def sendmail(recipients=[], sender="", subject="No Subject", message="No Message",
|
||||
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None,
|
||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
|
||||
attachments=None, content=None, doctype=None, name=None, reply_to=None,
|
||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, add_unsubscribe_link=1,
|
||||
attachments=None, content=None, doctype=None, name=None, reply_to=None, queue_separately=False,
|
||||
cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None,
|
||||
send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False,
|
||||
inline_images=None, template=None, args=None, header=None, print_letterhead=False):
|
||||
inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False):
|
||||
"""Send email using user's default **Email Account** or global default **Email Account**.
|
||||
|
||||
|
||||
|
|
@ -495,6 +500,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
:param template: Name of html template from templates/emails folder
|
||||
:param args: Arguments for rendering the template
|
||||
:param header: Append header in email
|
||||
:param with_container: Wraps email inside a styled container
|
||||
"""
|
||||
text_content = None
|
||||
if template:
|
||||
|
|
@ -512,12 +518,12 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
|
|||
from frappe.email import queue
|
||||
queue.send(recipients=recipients, sender=sender,
|
||||
subject=subject, message=message, text_content=text_content,
|
||||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
|
||||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
|
||||
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
|
||||
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
|
||||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority,
|
||||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
|
||||
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
|
||||
inline_images=inline_images, header=header, print_letterhead=print_letterhead)
|
||||
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)
|
||||
|
||||
whitelisted = []
|
||||
guest_methods = []
|
||||
|
|
@ -632,6 +638,21 @@ def clear_cache(user=None, doctype=None):
|
|||
|
||||
local.role_permissions = {}
|
||||
|
||||
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
|
||||
if ignore_permissions:
|
||||
return False
|
||||
|
||||
if not user:
|
||||
user = local.session.user
|
||||
|
||||
import frappe.permissions
|
||||
permissions = frappe.permissions.get_role_permissions(doctype, user=user)
|
||||
|
||||
if permissions.get('select') and not permissions.get('read'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
|
||||
"""Raises `frappe.PermissionError` if not permitted.
|
||||
|
||||
|
|
@ -801,11 +822,17 @@ def get_doc(*args, **kwargs):
|
|||
|
||||
return doc
|
||||
|
||||
def get_last_doc(doctype):
|
||||
def get_last_doc(doctype, filters=None, order_by="creation desc"):
|
||||
"""Get last created document of this type."""
|
||||
d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1)
|
||||
d = get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
limit_page_length=1,
|
||||
order_by=order_by,
|
||||
pluck="name"
|
||||
)
|
||||
if d:
|
||||
return get_doc(doctype, d[0].name)
|
||||
return get_doc(doctype, d[0])
|
||||
else:
|
||||
raise DoesNotExistError
|
||||
|
||||
|
|
@ -944,7 +971,7 @@ def get_installed_apps(sort=False, frappe_last=False):
|
|||
connect()
|
||||
|
||||
if not local.all_apps:
|
||||
local.all_apps = get_all_apps(True)
|
||||
local.all_apps = cache().get_value('all_apps', get_all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
|
|
@ -1159,6 +1186,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
|
|||
'doctype_or_field': args.doctype_or_field,
|
||||
'doc_type': doctype,
|
||||
'field_name': args.fieldname,
|
||||
'row_name': args.row_name,
|
||||
'property': args.property,
|
||||
'value': args.value,
|
||||
'property_type': args.property_type or "Data",
|
||||
|
|
@ -1609,7 +1637,7 @@ def log_error(message=None, title=_("Error")):
|
|||
method=title)).insert(ignore_permissions=True)
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
html = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
html = '<a href="/app/Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
return html.format(
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
|
|
@ -1721,6 +1749,8 @@ def parse_json(val):
|
|||
return parse_json(val)
|
||||
|
||||
def mock(type, size=1, locale='en'):
|
||||
from faker import Faker
|
||||
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import os
|
|||
from six import iteritems
|
||||
import logging
|
||||
|
||||
from werkzeug.wrappers import Request
|
||||
from werkzeug.local import LocalManager
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.exceptions import HTTPException, NotFound
|
||||
from werkzeug.middleware.profiler import ProfilerMiddleware
|
||||
from werkzeug.middleware.shared_data import SharedDataMiddleware
|
||||
|
|
@ -57,19 +57,22 @@ def application(request):
|
|||
frappe.monitor.start()
|
||||
frappe.rate_limiter.apply()
|
||||
|
||||
if frappe.local.form_dict.cmd:
|
||||
if request.method == "OPTIONS":
|
||||
response = Response()
|
||||
|
||||
elif frappe.form_dict.cmd:
|
||||
response = frappe.handler.handle()
|
||||
|
||||
elif frappe.request.path.startswith("/api/"):
|
||||
elif request.path.startswith("/api/"):
|
||||
response = frappe.api.handle()
|
||||
|
||||
elif frappe.request.path.startswith('/backups'):
|
||||
elif request.path.startswith('/backups'):
|
||||
response = frappe.utils.response.download_backup(request.path)
|
||||
|
||||
elif frappe.request.path.startswith('/private/files/'):
|
||||
elif request.path.startswith('/private/files/'):
|
||||
response = frappe.utils.response.download_private_file(request.path)
|
||||
|
||||
elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
|
||||
elif request.method in ('GET', 'HEAD', 'POST'):
|
||||
response = frappe.website.render.render()
|
||||
|
||||
else:
|
||||
|
|
@ -88,13 +91,9 @@ def application(request):
|
|||
rollback = after_request(rollback)
|
||||
|
||||
finally:
|
||||
if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
|
||||
if request.method in ("POST", "PUT") and frappe.db and rollback:
|
||||
frappe.db.rollback()
|
||||
|
||||
# set cookies
|
||||
if response and hasattr(frappe.local, 'cookie_manager'):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
frappe.rate_limiter.update()
|
||||
frappe.monitor.stop(response)
|
||||
frappe.recorder.dump()
|
||||
|
|
@ -110,9 +109,7 @@ def application(request):
|
|||
"http_status_code": getattr(response, "status_code", "NOTFOUND")
|
||||
})
|
||||
|
||||
if response and hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
process_response(response)
|
||||
frappe.destroy()
|
||||
|
||||
return response
|
||||
|
|
@ -131,10 +128,51 @@ def init_request(request):
|
|||
if frappe.local.conf.get('maintenance_mode'):
|
||||
frappe.connect()
|
||||
raise frappe.SessionStopped('Session Stopped')
|
||||
else:
|
||||
frappe.connect(set_admin_as_user=False)
|
||||
|
||||
make_form_dict(request)
|
||||
|
||||
frappe.local.http_request = frappe.auth.HTTPRequest()
|
||||
if request.method != "OPTIONS":
|
||||
frappe.local.http_request = frappe.auth.HTTPRequest()
|
||||
|
||||
def process_response(response):
|
||||
if not response:
|
||||
return
|
||||
|
||||
# set cookies
|
||||
if hasattr(frappe.local, 'cookie_manager'):
|
||||
frappe.local.cookie_manager.flush_cookies(response=response)
|
||||
|
||||
# rate limiter headers
|
||||
if hasattr(frappe.local, 'rate_limiter'):
|
||||
response.headers.extend(frappe.local.rate_limiter.headers())
|
||||
|
||||
# CORS headers
|
||||
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
|
||||
set_cors_headers(response)
|
||||
|
||||
def set_cors_headers(response):
|
||||
origin = frappe.request.headers.get('Origin')
|
||||
allow_cors = frappe.conf.allow_cors
|
||||
if not (origin and allow_cors):
|
||||
return
|
||||
|
||||
if allow_cors != "*":
|
||||
if not isinstance(allow_cors, list):
|
||||
allow_cors = [allow_cors]
|
||||
|
||||
if origin not in allow_cors:
|
||||
return
|
||||
|
||||
response.headers.extend({
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
|
||||
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
|
||||
'Cache-Control,Content-Type')
|
||||
})
|
||||
|
||||
def make_form_dict(request):
|
||||
import json
|
||||
|
|
@ -145,6 +183,9 @@ def make_form_dict(request):
|
|||
else:
|
||||
args = request.form or request.args
|
||||
|
||||
if not isinstance(args, dict):
|
||||
frappe.throw("Invalid request arguments")
|
||||
|
||||
try:
|
||||
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
|
||||
for k, v in iteritems(args) })
|
||||
|
|
@ -160,6 +201,10 @@ def handle_exception(e):
|
|||
http_status_code = getattr(e, "http_status_code", 500)
|
||||
return_as_message = False
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
# don't fail silently
|
||||
print(frappe.get_traceback())
|
||||
|
||||
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
|
||||
# handle ajax responses first
|
||||
# if the request is ajax, send back the trace or error message
|
||||
|
|
|
|||
177
frappe/auth.py
177
frappe/auth.py
|
|
@ -173,7 +173,7 @@ class LoginManager:
|
|||
frappe.local.cookie_manager.set_cookie("system_user", "yes")
|
||||
if not resume:
|
||||
frappe.local.response['message'] = 'Logged In'
|
||||
frappe.local.response["home_page"] = "/desk"
|
||||
frappe.local.response["home_page"] = "/app"
|
||||
|
||||
if not resume:
|
||||
frappe.response["full_name"] = self.full_name
|
||||
|
|
@ -207,23 +207,44 @@ class LoginManager:
|
|||
if frappe.session.user != "Guest":
|
||||
clear_sessions(frappe.session.user, keep_current=True)
|
||||
|
||||
def authenticate(self, user=None, pwd=None):
|
||||
def authenticate(self, user: str = None, pwd: str = None):
|
||||
from frappe.core.doctype.user.user import User
|
||||
|
||||
if not (user and pwd):
|
||||
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
|
||||
if not (user and pwd):
|
||||
self.fail(_('Incomplete login details'), user=user)
|
||||
|
||||
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
|
||||
user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
|
||||
# Ignore password check if tmp_id is set, 2FA takes care of authentication.
|
||||
validate_password = not bool(frappe.form_dict.get('tmp_id'))
|
||||
user = User.find_by_credentials(user, pwd, validate_password=validate_password)
|
||||
|
||||
if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_user_name")):
|
||||
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
|
||||
if not user:
|
||||
self.fail('Invalid login credentials')
|
||||
|
||||
self.check_if_enabled(user)
|
||||
if not frappe.form_dict.get('tmp_id'):
|
||||
self.user = self.check_password(user, pwd)
|
||||
sys_settings = frappe.get_doc("System Settings")
|
||||
track_login_attempts = (sys_settings.allow_consecutive_login_attempts >0)
|
||||
|
||||
tracker_kwargs = {}
|
||||
if track_login_attempts:
|
||||
tracker_kwargs['lock_interval'] = sys_settings.allow_login_after_fail
|
||||
tracker_kwargs['max_consecutive_login_attempts'] = sys_settings.allow_consecutive_login_attempts
|
||||
|
||||
tracker = LoginAttemptTracker(user.name, **tracker_kwargs)
|
||||
|
||||
if track_login_attempts and not tracker.is_user_allowed():
|
||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
|
||||
.format(sys_settings.allow_login_after_fail), frappe.SecurityException)
|
||||
|
||||
if not user.is_authenticated:
|
||||
tracker.add_failure_attempt()
|
||||
self.fail('Invalid login credentials', user=user.name)
|
||||
elif not (user.name == 'Administrator' or user.enabled):
|
||||
tracker.add_failure_attempt()
|
||||
self.fail('User disabled or missing', user=user.name)
|
||||
else:
|
||||
self.user = user
|
||||
tracker.add_success_attempt()
|
||||
self.user = user.name
|
||||
|
||||
def force_user_to_reset_password(self):
|
||||
if not self.user:
|
||||
|
|
@ -245,23 +266,12 @@ class LoginManager:
|
|||
if last_pwd_reset_days > reset_pwd_after_days:
|
||||
return True
|
||||
|
||||
def check_if_enabled(self, user):
|
||||
"""raise exception if user not enabled"""
|
||||
doc = frappe.get_doc("System Settings")
|
||||
if cint(doc.allow_consecutive_login_attempts) > 0:
|
||||
check_consecutive_login_attempts(user, doc)
|
||||
|
||||
if user=='Administrator': return
|
||||
if not cint(frappe.db.get_value('User', user, 'enabled')):
|
||||
self.fail('User disabled or missing', user=user)
|
||||
|
||||
def check_password(self, user, pwd):
|
||||
"""check password"""
|
||||
try:
|
||||
# returns user in correct case
|
||||
return check_password(user, pwd)
|
||||
except frappe.AuthenticationError:
|
||||
self.update_invalid_login(user)
|
||||
self.fail('Incorrect password', user=user)
|
||||
|
||||
def fail(self, message, user=None):
|
||||
|
|
@ -272,15 +282,6 @@ class LoginManager:
|
|||
frappe.db.commit()
|
||||
raise frappe.AuthenticationError
|
||||
|
||||
def update_invalid_login(self, user):
|
||||
last_login_tried = get_last_tried_login_data(user)
|
||||
|
||||
failed_count = 0
|
||||
if last_login_tried > get_datetime():
|
||||
failed_count = get_login_failed_count(user)
|
||||
|
||||
frappe.cache().hset('login_failed_count', user, failed_count + 1)
|
||||
|
||||
def run_trigger(self, event='on_login'):
|
||||
for method in frappe.get_hooks().get(event, []):
|
||||
frappe.call(frappe.get_attr(method), login_manager=self)
|
||||
|
|
@ -383,38 +384,6 @@ def clear_cookies():
|
|||
frappe.session.sid = ""
|
||||
frappe.local.cookie_manager.delete_cookie(["full_name", "user_id", "sid", "user_image", "system_user"])
|
||||
|
||||
def get_last_tried_login_data(user, get_last_login=False):
|
||||
locked_account_time = frappe.cache().hget('locked_account_time', user)
|
||||
if get_last_login and locked_account_time:
|
||||
return locked_account_time
|
||||
|
||||
last_login_tried = frappe.cache().hget('last_login_tried', user)
|
||||
if not last_login_tried or last_login_tried < get_datetime():
|
||||
last_login_tried = get_datetime() + datetime.timedelta(seconds=60)
|
||||
|
||||
frappe.cache().hset('last_login_tried', user, last_login_tried)
|
||||
|
||||
return last_login_tried
|
||||
|
||||
def get_login_failed_count(user):
|
||||
return cint(frappe.cache().hget('login_failed_count', user)) or 0
|
||||
|
||||
def check_consecutive_login_attempts(user, doc):
|
||||
login_failed_count = get_login_failed_count(user)
|
||||
last_login_tried = (get_last_tried_login_data(user, True)
|
||||
+ datetime.timedelta(seconds=doc.allow_login_after_fail))
|
||||
|
||||
if login_failed_count >= cint(doc.allow_consecutive_login_attempts):
|
||||
locked_account_time = frappe.cache().hget('locked_account_time', user)
|
||||
if not locked_account_time:
|
||||
frappe.cache().hset('locked_account_time', user, get_datetime())
|
||||
|
||||
if last_login_tried > get_datetime():
|
||||
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
|
||||
.format(doc.allow_login_after_fail), frappe.SecurityException)
|
||||
else:
|
||||
delete_login_failed_cache(user)
|
||||
|
||||
def validate_ip_address(user):
|
||||
"""check if IP Address is valid"""
|
||||
user = frappe.get_cached_doc("User", user) if not frappe.flags.in_test else frappe.get_doc("User", user)
|
||||
|
|
@ -436,3 +405,87 @@ def validate_ip_address(user):
|
|||
return
|
||||
|
||||
frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError)
|
||||
|
||||
|
||||
class LoginAttemptTracker(object):
|
||||
"""Track login attemts of a user.
|
||||
|
||||
Lock the account for s number of seconds if there have been n consecutive unsuccessful attempts to log in.
|
||||
"""
|
||||
def __init__(self, user_name: str, max_consecutive_login_attempts: int=3, lock_interval:int = 5*60):
|
||||
""" Initialize the tracker.
|
||||
|
||||
:param user_name: Name of the loggedin user
|
||||
:param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts
|
||||
:param lock_interval: Locking interval incase of maximum failed attempts
|
||||
"""
|
||||
self.user_name = user_name
|
||||
self.lock_interval = datetime.timedelta(seconds=lock_interval)
|
||||
self.max_failed_logins = max_consecutive_login_attempts
|
||||
|
||||
@property
|
||||
def login_failed_count(self):
|
||||
return frappe.cache().hget('login_failed_count', self.user_name)
|
||||
|
||||
@login_failed_count.setter
|
||||
def login_failed_count(self, count):
|
||||
frappe.cache().hset('login_failed_count', self.user_name, count)
|
||||
|
||||
@login_failed_count.deleter
|
||||
def login_failed_count(self):
|
||||
frappe.cache().hdel('login_failed_count', self.user_name)
|
||||
|
||||
@property
|
||||
def login_failed_time(self):
|
||||
"""First failed login attempt time within lock interval.
|
||||
|
||||
For every user we track only First failed login attempt time within lock interval of time.
|
||||
"""
|
||||
return frappe.cache().hget('login_failed_time', self.user_name)
|
||||
|
||||
@login_failed_time.setter
|
||||
def login_failed_time(self, timestamp):
|
||||
frappe.cache().hset('login_failed_time', self.user_name, timestamp)
|
||||
|
||||
@login_failed_time.deleter
|
||||
def login_failed_time(self):
|
||||
frappe.cache().hdel('login_failed_time', self.user_name)
|
||||
|
||||
def add_failure_attempt(self):
|
||||
""" Log user failure attempts into the system.
|
||||
|
||||
Increase the failure count if new failure is with in current lock interval time period, if not reset the login failure count.
|
||||
"""
|
||||
login_failed_time = self.login_failed_time
|
||||
login_failed_count = self.login_failed_count # Consecutive login failure count
|
||||
current_time = get_datetime()
|
||||
|
||||
if not (login_failed_time and login_failed_count):
|
||||
login_failed_time, login_failed_count = current_time, 0
|
||||
|
||||
if login_failed_time + self.lock_interval > current_time:
|
||||
login_failed_count += 1
|
||||
else:
|
||||
login_failed_time, login_failed_count = current_time, 1
|
||||
|
||||
self.login_failed_time = login_failed_time
|
||||
self.login_failed_count = login_failed_count
|
||||
|
||||
def add_success_attempt(self):
|
||||
"""Reset login failures.
|
||||
"""
|
||||
del self.login_failed_count
|
||||
del self.login_failed_time
|
||||
|
||||
def is_user_allowed(self) -> bool:
|
||||
"""Is user allowed to login
|
||||
|
||||
User is not allowed to login if login failures are greater than threshold within in lock interval from first login failure.
|
||||
"""
|
||||
login_failed_time = self.login_failed_time
|
||||
login_failed_count = self.login_failed_count or 0
|
||||
current_time = get_datetime()
|
||||
|
||||
if login_failed_time and login_failed_time + self.lock_interval > current_time and login_failed_count > self.max_failed_logins:
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Tools",
|
||||
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Email",
|
||||
"links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Automation",
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Event Streaming",
|
||||
"links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
"charts": [],
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Desk Page",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 0,
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Tools",
|
||||
"modified": "2020-07-21 19:32:18.480700",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
"link_to": "ToDo",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Note",
|
||||
"link_to": "Note",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "File",
|
||||
"link_to": "File",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Assignment Rule",
|
||||
"link_to": "Assignment Rule",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Auto Repeat",
|
||||
"link_to": "Auto Repeat",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,14 +3,80 @@
|
|||
|
||||
frappe.ui.form.on('Assignment Rule', {
|
||||
refresh: function(frm) {
|
||||
frm.trigger('setup_assignment_days_buttons');
|
||||
frm.trigger('set_options');
|
||||
// refresh description
|
||||
frm.events.rule(frm);
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
frm.set_query("document_type", () => {
|
||||
return {
|
||||
filters: {
|
||||
name: ["!=", "ToDo"]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
document_type: function(frm) {
|
||||
frm.trigger('set_options');
|
||||
},
|
||||
|
||||
setup_assignment_days_buttons: function(frm) {
|
||||
const labels = ['Weekends', 'Weekdays', 'All Days'];
|
||||
let get_days = (label) => {
|
||||
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
|
||||
const weekends = ['Saturday', 'Sunday'];
|
||||
return {
|
||||
'All Days': weekdays.concat(weekends),
|
||||
'Weekdays': weekdays,
|
||||
'Weekends': weekends,
|
||||
}[label];
|
||||
};
|
||||
|
||||
let set_days = (e) => {
|
||||
frm.clear_table('assignment_days');
|
||||
const label = $(e.currentTarget).text();
|
||||
get_days(label).forEach((day) =>
|
||||
frm.add_child('assignment_days', { day: day })
|
||||
);
|
||||
frm.refresh_field('assignment_days');
|
||||
};
|
||||
|
||||
labels.forEach(label =>
|
||||
frm.fields_dict['assignment_days'].grid.add_custom_button(
|
||||
label,
|
||||
set_days,
|
||||
'top'
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
rule: function(frm) {
|
||||
if (frm.doc.rule === 'Round Robin') {
|
||||
frm.get_field('rule').set_description(__('Assign one by one, in sequence'));
|
||||
} else {
|
||||
frm.get_field('rule').set_description(__('Assign to the one who has the least assignments'));
|
||||
const description_map = {
|
||||
'Round Robin': __('Assign one by one, in sequence'),
|
||||
'Load Balancing': __('Assign to the one who has the least assignments'),
|
||||
'Based on Field': __('Assign to the user set in this field'),
|
||||
};
|
||||
frm.get_field('rule').set_description(description_map[frm.doc.rule]);
|
||||
},
|
||||
|
||||
set_options(frm) {
|
||||
const doctype = frm.doc.document_type;
|
||||
frm.set_fields_as_options(
|
||||
'field',
|
||||
doctype,
|
||||
(df) => ['Dynamic Link', 'Data'].includes(df.fieldtype)
|
||||
|| (df.fieldtype == 'Link' && df.options == 'User'),
|
||||
[{ label: 'Owner', value: 'owner' }]
|
||||
);
|
||||
if (doctype) {
|
||||
frm.set_fields_as_options(
|
||||
'due_date_based_on',
|
||||
doctype,
|
||||
(df) => ['Date', 'Datetime'].includes(df.fieldtype)
|
||||
).then(options => frm.set_df_property('due_date_based_on', 'hidden', !options.length));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-02-28 17:12:18.815830",
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"due_date_based_on",
|
||||
"priority",
|
||||
"disabled",
|
||||
"column_break_4",
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
"assignment_days",
|
||||
"assign_to_users_section",
|
||||
"rule",
|
||||
"field",
|
||||
"users",
|
||||
"last_user"
|
||||
],
|
||||
|
|
@ -91,15 +94,16 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Rule",
|
||||
"options": "Round Robin\nLoad Balancing",
|
||||
"options": "Round Robin\nLoad Balancing\nBased on Field",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.rule !== 'Based on Field'",
|
||||
"fieldname": "users",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Users",
|
||||
"options": "Assignment Rule User",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval: doc.rule !== 'Based on Field'",
|
||||
"options": "Assignment Rule User"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_user",
|
||||
|
|
@ -129,9 +133,25 @@
|
|||
"label": "Assignment Days",
|
||||
"options": "Assignment Rule Day",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "document_type",
|
||||
"description": "Value from this field will be set as the due date in the ToDo",
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.rule == 'Based on Field'",
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Select",
|
||||
"label": "Field",
|
||||
"mandatory_depends_on": "eval: doc.rule == 'Based on Field'"
|
||||
}
|
||||
],
|
||||
"modified": "2019-09-25 14:52:12.214514",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-20 14:47:20.662954",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Assignment Rule",
|
||||
|
|
|
|||
|
|
@ -18,15 +18,17 @@ class AssignmentRule(Document):
|
|||
if not len(set(assignment_days)) == len(assignment_days):
|
||||
repeated_days = get_repeated(assignment_days)
|
||||
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
|
||||
if self.document_type == 'ToDo':
|
||||
frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo")))
|
||||
|
||||
def on_update(self): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
|
||||
def on_update(self):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
||||
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
|
||||
def after_rename(self, old, new, merge):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
||||
def on_trash(self): # pylint: disable=no-self-use
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
|
||||
def on_trash(self):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
||||
def apply_unassign(self, doc, assignments):
|
||||
if (self.unassign_condition and
|
||||
|
|
@ -38,26 +40,30 @@ class AssignmentRule(Document):
|
|||
|
||||
def apply_assign(self, doc):
|
||||
if self.safe_eval('assign_condition', doc):
|
||||
self.do_assignment(doc)
|
||||
return True
|
||||
return self.do_assignment(doc)
|
||||
|
||||
def do_assignment(self, doc):
|
||||
# clear existing assignment, to reassign
|
||||
assign_to.clear(doc.get('doctype'), doc.get('name'))
|
||||
|
||||
user = self.get_user()
|
||||
user = self.get_user(doc)
|
||||
|
||||
assign_to.add(dict(
|
||||
assign_to = [user],
|
||||
doctype = doc.get('doctype'),
|
||||
name = doc.get('name'),
|
||||
description = frappe.render_template(self.description, doc),
|
||||
assignment_rule = self.name,
|
||||
notify = True
|
||||
))
|
||||
if user:
|
||||
assign_to.add(dict(
|
||||
assign_to = [user],
|
||||
doctype = doc.get('doctype'),
|
||||
name = doc.get('name'),
|
||||
description = frappe.render_template(self.description, doc),
|
||||
assignment_rule = self.name,
|
||||
notify = True,
|
||||
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
|
||||
))
|
||||
|
||||
# set for reference in round robin
|
||||
self.db_set('last_user', user)
|
||||
# set for reference in round robin
|
||||
self.db_set('last_user', user)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def clear_assignment(self, doc):
|
||||
'''Clear assignments'''
|
||||
|
|
@ -69,7 +75,7 @@ class AssignmentRule(Document):
|
|||
if self.safe_eval('close_condition', doc):
|
||||
return assign_to.close_all_assignments(doc.get('doctype'), doc.get('name'))
|
||||
|
||||
def get_user(self):
|
||||
def get_user(self, doc):
|
||||
'''
|
||||
Get the next user for assignment
|
||||
'''
|
||||
|
|
@ -77,6 +83,8 @@ class AssignmentRule(Document):
|
|||
return self.get_user_round_robin()
|
||||
elif self.rule == 'Load Balancing':
|
||||
return self.get_user_load_balancing()
|
||||
elif self.rule == 'Based on Field':
|
||||
return self.get_user_based_on_field(doc)
|
||||
|
||||
def get_user_round_robin(self):
|
||||
'''
|
||||
|
|
@ -113,6 +121,11 @@ class AssignmentRule(Document):
|
|||
# pick the first user
|
||||
return sorted_counts[0].get('user')
|
||||
|
||||
def get_user_based_on_field(self, doc):
|
||||
val = doc.get(self.field)
|
||||
if frappe.db.exists('User', val):
|
||||
return val
|
||||
|
||||
def safe_eval(self, fieldname, doc):
|
||||
try:
|
||||
if self.get(fieldname):
|
||||
|
|
@ -188,7 +201,7 @@ def apply(doc, method=None, doctype=None, name=None):
|
|||
|
||||
# multiple auto assigns
|
||||
for d in assignment_rules:
|
||||
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.get('name')))
|
||||
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
|
||||
|
||||
if not assignment_rule_docs:
|
||||
return
|
||||
|
|
@ -237,6 +250,40 @@ def apply(doc, method=None, doctype=None, name=None):
|
|||
break
|
||||
assignment_rule.close_assignments(doc)
|
||||
|
||||
def update_due_date(doc, state=None):
|
||||
# called from hook
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
return
|
||||
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
|
||||
document_type = doc.doctype,
|
||||
disabled = 0,
|
||||
due_date_based_on = ['is', 'set']
|
||||
))
|
||||
for rule in assignment_rules:
|
||||
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
|
||||
due_date_field = rule_doc.due_date_based_on
|
||||
if doc.meta.has_field(due_date_field) and \
|
||||
doc.has_value_changed(due_date_field) and rule.get('name'):
|
||||
assignment_todos = frappe.get_all('ToDo', {
|
||||
'assignment_rule': rule.get('name'),
|
||||
'status': 'Open',
|
||||
'reference_type': doc.doctype,
|
||||
'reference_name': doc.name
|
||||
})
|
||||
for todo in assignment_todos:
|
||||
todo_doc = frappe.get_doc('ToDo', todo.name)
|
||||
todo_doc.date = doc.get(due_date_field)
|
||||
todo_doc.flags.updater_reference = {
|
||||
'doctype': 'Assignment Rule',
|
||||
'docname': rule.get('name'),
|
||||
'label': _('via Assignment Rule')
|
||||
}
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
|
||||
def get_assignment_rules():
|
||||
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
|
||||
|
||||
|
|
@ -250,3 +297,7 @@ def get_repeated(values):
|
|||
if value not in diff:
|
||||
diff.append(str(value))
|
||||
return " ".join(diff)
|
||||
|
||||
def clear_assignment_rule_cache(rule):
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
dict(day = 'Friday'),
|
||||
dict(day = 'Saturday'),
|
||||
]
|
||||
self.days = days
|
||||
self.assignment_rule = get_assignment_rule([days, days])
|
||||
clear_assignments()
|
||||
|
||||
|
|
@ -87,6 +88,30 @@ class TestAutoAssign(unittest.TestCase):
|
|||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
|
||||
|
||||
def test_based_on_field(self):
|
||||
self.assignment_rule.rule = 'Based on Field'
|
||||
self.assignment_rule.field = 'owner'
|
||||
self.assignment_rule.save()
|
||||
|
||||
frappe.set_user('test1@example.com')
|
||||
note = make_note(dict(public=1))
|
||||
# check if auto assigned to doc owner, test1@example.com
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test1@example.com')
|
||||
|
||||
frappe.set_user('test2@example.com')
|
||||
note = make_note(dict(public=1))
|
||||
# check if auto assigned to doc owner, test2@example.com
|
||||
self.assertEqual(frappe.db.get_value('ToDo', dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test2@example.com')
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
def test_assign_condition(self):
|
||||
# check condition
|
||||
|
|
@ -180,6 +205,55 @@ class TestAutoAssign(unittest.TestCase):
|
|||
status = 'Open'
|
||||
), 'owner'), ['test3@example.com'])
|
||||
|
||||
def test_assignment_rule_condition(self):
|
||||
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
|
||||
|
||||
# Add expiry_date custom field
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
|
||||
create_custom_field('Note', df)
|
||||
|
||||
assignment_rule = frappe.get_doc(dict(
|
||||
name = 'Assignment with Due Date',
|
||||
doctype = 'Assignment Rule',
|
||||
document_type = 'Note',
|
||||
assign_condition = 'public == 0',
|
||||
due_date_based_on = 'expiry_date',
|
||||
assignment_days = self.days,
|
||||
users = [
|
||||
dict(user = 'test@example.com'),
|
||||
]
|
||||
)).insert()
|
||||
|
||||
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
|
||||
note1 = make_note({'expiry_date': expiry_date})
|
||||
note2 = make_note({'expiry_date': expiry_date})
|
||||
|
||||
note1_todo = frappe.get_all('ToDo', filters=dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note1.name,
|
||||
status = 'Open'
|
||||
))[0]
|
||||
|
||||
note1_todo_doc = frappe.get_doc('ToDo', note1_todo.name)
|
||||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), expiry_date)
|
||||
|
||||
# due date should be updated if the reference doc's date is updated.
|
||||
note1.expiry_date = frappe.utils.add_days(expiry_date, 2)
|
||||
note1.save()
|
||||
note1_todo_doc.reload()
|
||||
self.assertEqual(frappe.utils.get_date_str(note1_todo_doc.date), note1.expiry_date)
|
||||
|
||||
# saving one note's expiry should not update other note todo's due date
|
||||
note2_todo = frappe.get_all('ToDo', filters=dict(
|
||||
reference_type = 'Note',
|
||||
reference_name = note2.name,
|
||||
status = 'Open'
|
||||
), fields=['name', 'date'])[0]
|
||||
self.assertNotEqual(frappe.utils.get_date_str(note2_todo.date), note1.expiry_date)
|
||||
self.assertEqual(frappe.utils.get_date_str(note2_todo.date), expiry_date)
|
||||
assignment_rule.delete()
|
||||
|
||||
def clear_assignments():
|
||||
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
|
||||
|
||||
|
|
@ -237,4 +311,4 @@ def make_note(values=None):
|
|||
|
||||
note.insert()
|
||||
|
||||
return note
|
||||
return note
|
||||
|
|
|
|||
|
|
@ -1,76 +1,34 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"actions": [],
|
||||
"allow_read": 1,
|
||||
"creation": "2019-02-27 11:41:46.602400",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-02-27 17:16:41.399261",
|
||||
"links": [],
|
||||
"modified": "2020-09-29 20:12:14.456785",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Assignment Rule User",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
refresh: function(frm) {
|
||||
// auto repeat message
|
||||
if (frm.is_new()) {
|
||||
let customize_form_link = `<a href="#Form/Customize Form">${__('Customize Form')}</a>`;
|
||||
let customize_form_link = `<a href="/app/customize form">${__('Customize Form')}</a>`;
|
||||
frm.dashboard.set_headline(__('To configure Auto Repeat, enable "Allow Auto Repeat" from {0}.', [customize_form_link]));
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +44,22 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
// auto repeat schedule
|
||||
frappe.auto_repeat.render_schedule(frm);
|
||||
|
||||
frm.trigger('toggle_submit_on_creation');
|
||||
},
|
||||
|
||||
reference_doctype: function(frm) {
|
||||
frm.trigger('toggle_submit_on_creation');
|
||||
},
|
||||
|
||||
toggle_submit_on_creation: function(frm) {
|
||||
// submit on creation checkbox
|
||||
if (frm.doc.reference_doctype) {
|
||||
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
|
||||
let meta = frappe.get_meta(frm.doc.reference_doctype);
|
||||
frm.toggle_display('submit_on_creation', meta.is_submittable);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
template: function(frm) {
|
||||
|
|
@ -86,15 +102,13 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
|
||||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
|
||||
frappe.call({
|
||||
method: "get_auto_repeat_schedule",
|
||||
doc: frm.doc
|
||||
}).done((r) => {
|
||||
frm.call("get_auto_repeat_schedule").then(r => {
|
||||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
schedule_details : r.message || []
|
||||
})
|
||||
schedule_details: r.message || []
|
||||
}),
|
||||
__('Auto Repeat Schedule')
|
||||
);
|
||||
frm.dashboard.show();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:AUT-AR-{#####}",
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
"section_break_3",
|
||||
"reference_doctype",
|
||||
"reference_document",
|
||||
"submit_on_creation",
|
||||
"column_break_5",
|
||||
"start_date",
|
||||
"end_date",
|
||||
|
|
@ -21,6 +23,8 @@
|
|||
"repeat_on_last_day",
|
||||
"column_break_12",
|
||||
"next_schedule_date",
|
||||
"section_break_16",
|
||||
"repeat_on_days",
|
||||
"notification",
|
||||
"notify_by_email",
|
||||
"recipients",
|
||||
|
|
@ -186,9 +190,28 @@
|
|||
"fieldname": "repeat_on_last_day",
|
||||
"fieldtype": "Check",
|
||||
"label": "Repeat on Last Day of the Month"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "repeat_on_days",
|
||||
"fieldtype": "Table",
|
||||
"label": "Repeat on Days",
|
||||
"options": "Auto Repeat Day"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_on_creation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit on Creation"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"modified": "2019-07-17 11:30:51.412317",
|
||||
"links": [],
|
||||
"modified": "2021-01-12 09:24:49.719611",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
|
@ -13,16 +14,21 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_
|
|||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
|
||||
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to
|
||||
|
||||
month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
|
||||
|
||||
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}
|
||||
|
||||
class AutoRepeat(Document):
|
||||
def validate(self):
|
||||
self.update_status()
|
||||
self.validate_reference_doctype()
|
||||
self.validate_submit_on_creation()
|
||||
self.validate_dates()
|
||||
self.validate_email_id()
|
||||
self.validate_auto_repeat_days()
|
||||
self.set_dates()
|
||||
self.update_auto_repeat_id()
|
||||
self.unlink_if_applicable()
|
||||
|
|
@ -48,7 +54,7 @@ class AutoRepeat(Document):
|
|||
if self.disabled:
|
||||
self.next_schedule_date = None
|
||||
else:
|
||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
|
||||
self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)
|
||||
|
||||
def unlink_if_applicable(self):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
|
|
@ -60,6 +66,11 @@ class AutoRepeat(Document):
|
|||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
|
||||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))
|
||||
|
||||
def validate_submit_on_creation(self):
|
||||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
|
||||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
|
||||
frappe.bold('Submit on Creation')))
|
||||
|
||||
def validate_dates(self):
|
||||
if frappe.flags.in_patch:
|
||||
return
|
||||
|
|
@ -82,6 +93,12 @@ class AutoRepeat(Document):
|
|||
else:
|
||||
frappe.throw(_("'Recipients' not specified"))
|
||||
|
||||
def validate_auto_repeat_days(self):
|
||||
auto_repeat_days = self.get_auto_repeat_days()
|
||||
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
|
||||
repeated_days = get_repeated(auto_repeat_days)
|
||||
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
#check if document is already on auto repeat
|
||||
auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
|
||||
|
|
@ -107,7 +124,7 @@ class AutoRepeat(Document):
|
|||
end_date = getdate(self.end_date)
|
||||
|
||||
if not self.end_date:
|
||||
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
|
||||
next_date = self.get_next_schedule_date(schedule_date=start_date)
|
||||
row = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
|
|
@ -116,8 +133,7 @@ class AutoRepeat(Document):
|
|||
schedule_details.append(row)
|
||||
|
||||
if self.end_date:
|
||||
next_date = get_next_schedule_date(
|
||||
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
|
||||
next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)
|
||||
|
||||
while (getdate(next_date) < getdate(end_date)):
|
||||
row = {
|
||||
|
|
@ -126,8 +142,7 @@ class AutoRepeat(Document):
|
|||
"next_scheduled_date" : next_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
next_date = get_next_schedule_date(
|
||||
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
|
||||
next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)
|
||||
|
||||
return schedule_details
|
||||
|
||||
|
|
@ -150,6 +165,9 @@ class AutoRepeat(Document):
|
|||
self.update_doc(new_doc, reference_doc)
|
||||
new_doc.insert(ignore_permissions = True)
|
||||
|
||||
if self.submit_on_creation:
|
||||
new_doc.submit()
|
||||
|
||||
return new_doc
|
||||
|
||||
def update_doc(self, new_doc, reference_doc):
|
||||
|
|
@ -160,7 +178,7 @@ class AutoRepeat(Document):
|
|||
if new_doc.meta.get_field('auto_repeat'):
|
||||
new_doc.set('auto_repeat', self.name)
|
||||
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']:
|
||||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
|
||||
if new_doc.meta.get_field(fieldname):
|
||||
new_doc.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
|
|
@ -202,6 +220,75 @@ class AutoRepeat(Document):
|
|||
new_doc.set('from_date', from_date)
|
||||
new_doc.set('to_date', to_date)
|
||||
|
||||
def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
|
||||
"""
|
||||
Returns the next schedule date for auto repeat after a recurring document has been created.
|
||||
Adds required offset to the schedule_date param and returns the next schedule date.
|
||||
|
||||
:param schedule_date: The date when the last recurring document was created.
|
||||
:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
|
||||
"""
|
||||
if month_map.get(self.frequency):
|
||||
month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
|
||||
else:
|
||||
month_count = 0
|
||||
|
||||
day_count = 0
|
||||
if month_count and self.repeat_on_last_day:
|
||||
day_count = 31
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
elif month_count and self.repeat_on_day:
|
||||
day_count = self.repeat_on_day
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
elif month_count:
|
||||
next_date = get_next_date(self.start_date, month_count)
|
||||
else:
|
||||
days = self.get_days(schedule_date)
|
||||
next_date = add_days(schedule_date, days)
|
||||
|
||||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
if month_count:
|
||||
month_count += month_map.get(self.frequency, 0)
|
||||
next_date = get_next_date(self.start_date, month_count, day_count)
|
||||
else:
|
||||
days = self.get_days(next_date)
|
||||
next_date = add_days(next_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
def get_days(self, schedule_date):
|
||||
if self.frequency == "Weekly":
|
||||
days = self.get_offset_for_weekly_frequency(schedule_date)
|
||||
else:
|
||||
# daily frequency
|
||||
days = 1
|
||||
|
||||
return days
|
||||
|
||||
def get_offset_for_weekly_frequency(self, schedule_date):
|
||||
# if weekdays are not set, offset is 7 from current schedule date
|
||||
if not self.repeat_on_days:
|
||||
return 7
|
||||
|
||||
repeat_on_days = self.get_auto_repeat_days()
|
||||
current_schedule_day = getdate(schedule_date).weekday()
|
||||
weekdays = list(week_map.keys())
|
||||
|
||||
# if repeats on more than 1 day or
|
||||
# start date's weekday is not in repeat days, then get next weekday
|
||||
# else offset is 7
|
||||
if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
|
||||
weekday = get_next_weekday(current_schedule_day, repeat_on_days)
|
||||
next_weekday_number = week_map.get(weekday, 0)
|
||||
# offset for upcoming weekday
|
||||
return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
|
||||
return 7
|
||||
|
||||
def get_auto_repeat_days(self):
|
||||
return [d.day for d in self.get('repeat_on_days', [])]
|
||||
|
||||
def send_notification(self, new_doc):
|
||||
"""Notify concerned people about recurring document generation"""
|
||||
subject = self.subject or ''
|
||||
|
|
@ -243,13 +330,8 @@ class AutoRepeat(Document):
|
|||
|
||||
def fetch_linked_contacts(self):
|
||||
if self.reference_doctype and self.reference_document:
|
||||
res = frappe.db.get_all('Contact',
|
||||
fields=['email_id'],
|
||||
filters=[
|
||||
['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
|
||||
['Dynamic Link', 'link_name', '=', self.reference_document]
|
||||
])
|
||||
|
||||
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
|
||||
email_ids = list(set([d.email_id for d in res]))
|
||||
if not email_ids:
|
||||
frappe.msgprint(_('No contacts linked to document'), alert=True)
|
||||
|
|
@ -282,42 +364,24 @@ class AutoRepeat(Document):
|
|||
)
|
||||
|
||||
|
||||
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
|
||||
if month_map.get(frequency):
|
||||
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
|
||||
else:
|
||||
month_count = 0
|
||||
|
||||
day_count = 0
|
||||
if month_count and repeat_on_last_day:
|
||||
day_count = 31
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif month_count and repeat_on_day:
|
||||
day_count = repeat_on_day
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif month_count:
|
||||
next_date = get_next_date(start_date, month_count)
|
||||
else:
|
||||
days = 7 if frequency == 'Weekly' else 1
|
||||
next_date = add_days(schedule_date, days)
|
||||
|
||||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
if month_count:
|
||||
month_count += month_map.get(frequency)
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
elif days:
|
||||
next_date = add_days(next_date, days)
|
||||
|
||||
return next_date
|
||||
|
||||
|
||||
def get_next_date(dt, mcount, day=None):
|
||||
dt = getdate(dt)
|
||||
dt += relativedelta(months=mcount, day=day)
|
||||
return dt
|
||||
|
||||
|
||||
def get_next_weekday(current_schedule_day, weekdays):
|
||||
days = list(week_map.keys())
|
||||
if current_schedule_day > 0:
|
||||
days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
|
||||
else:
|
||||
days = days[(current_schedule_day + 1):]
|
||||
|
||||
for entry in days:
|
||||
if entry in weekdays:
|
||||
return entry
|
||||
|
||||
|
||||
#called through hooks
|
||||
def make_auto_repeat_entry():
|
||||
enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
|
||||
|
|
@ -328,6 +392,7 @@ def make_auto_repeat_entry():
|
|||
data = get_auto_repeat_entries(date)
|
||||
frappe.enqueue(enqueued_method, data=data)
|
||||
|
||||
|
||||
def create_repeated_entries(data):
|
||||
for d in data:
|
||||
doc = frappe.get_doc('Auto Repeat', d.name)
|
||||
|
|
@ -337,10 +402,11 @@ def create_repeated_entries(data):
|
|||
|
||||
if schedule_date == current_date and not doc.disabled:
|
||||
doc.create_documents()
|
||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
|
||||
schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
|
||||
if schedule_date and not doc.disabled:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
|
||||
|
||||
def get_auto_repeat_entries(date=None):
|
||||
if not date:
|
||||
date = getdate(today())
|
||||
|
|
@ -349,6 +415,7 @@ def get_auto_repeat_entries(date=None):
|
|||
['status', '=', 'Active']
|
||||
])
|
||||
|
||||
|
||||
#called through hooks
|
||||
def set_auto_repeat_as_completed():
|
||||
auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
|
||||
|
|
@ -358,6 +425,7 @@ def set_auto_repeat_as_completed():
|
|||
doc.status = 'Completed'
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
|
||||
if not start_date:
|
||||
|
|
|
|||
|
|
@ -7,10 +7,9 @@ import unittest
|
|||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries
|
||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map
|
||||
from frappe.utils import today, add_days, getdate, add_months
|
||||
|
||||
|
||||
def add_custom_fields():
|
||||
df = dict(
|
||||
fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
|
||||
|
|
@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
|
||||
def test_weekly_auto_repeat(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert()
|
||||
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7))
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
new_todo = frappe.db.get_value('ToDo',
|
||||
{'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
|
||||
|
||||
new_todo = frappe.get_doc('ToDo', new_todo)
|
||||
|
||||
self.assertEqual(todo.get('description'), new_todo.get('description'))
|
||||
|
||||
def test_weekly_auto_repeat_with_weekdays(self):
|
||||
todo = frappe.get_doc(
|
||||
dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert()
|
||||
|
||||
weekdays = list(week_map.keys())
|
||||
current_weekday = getdate().weekday()
|
||||
days = [
|
||||
{'day': weekdays[current_weekday]},
|
||||
{'day': weekdays[(current_weekday + 2) % 7]}
|
||||
]
|
||||
doc = make_auto_repeat(reference_doctype='ToDo',
|
||||
frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days)
|
||||
|
||||
self.assertEqual(doc.next_schedule_date, today())
|
||||
data = get_auto_repeat_entries(getdate(today()))
|
||||
create_repeated_entries(data)
|
||||
frappe.db.commit()
|
||||
|
||||
todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
|
||||
self.assertEqual(todo.auto_repeat, doc.name)
|
||||
|
||||
doc.reload()
|
||||
self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2))
|
||||
|
||||
def test_monthly_auto_repeat(self):
|
||||
start_date = today()
|
||||
end_date = add_months(start_date, 12)
|
||||
|
|
@ -111,6 +156,25 @@ class TestAutoRepeat(unittest.TestCase):
|
|||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2))
|
||||
self.assertEqual(getdate(doc.next_schedule_date), current_date)
|
||||
|
||||
def test_submit_on_creation(self):
|
||||
doctype = 'Test Submittable DocType'
|
||||
create_submittable_doctype(doctype)
|
||||
|
||||
current_date = getdate()
|
||||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert()
|
||||
submittable_doc.submit()
|
||||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name,
|
||||
start_date=add_days(current_date, -1), submit_on_creation=1)
|
||||
|
||||
data = get_auto_repeat_entries(current_date)
|
||||
create_repeated_entries(data)
|
||||
docnames = frappe.db.get_all(doc.reference_doctype,
|
||||
filters={'auto_repeat': doc.name},
|
||||
fields=['docstatus'],
|
||||
limit=1
|
||||
)
|
||||
self.assertEquals(docnames[0].docstatus, 1)
|
||||
|
||||
|
||||
def make_auto_repeat(**args):
|
||||
args = frappe._dict(args)
|
||||
|
|
@ -118,13 +182,46 @@ def make_auto_repeat(**args):
|
|||
'doctype': 'Auto Repeat',
|
||||
'reference_doctype': args.reference_doctype or 'ToDo',
|
||||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'),
|
||||
'submit_on_creation': args.submit_on_creation or 0,
|
||||
'frequency': args.frequency or 'Daily',
|
||||
'start_date': args.start_date or add_days(today(), -1),
|
||||
'end_date': args.end_date or "",
|
||||
'notify_by_email': args.notify or 0,
|
||||
'recipients': args.recipients or "",
|
||||
'subject': args.subject or "",
|
||||
'message': args.message or ""
|
||||
'message': args.message or "",
|
||||
'repeat_on_days': args.days or []
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def create_submittable_doctype(doctype):
|
||||
if frappe.db.exists('DocType', doctype):
|
||||
return
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'__newname': doctype,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'is_submittable': 1,
|
||||
'fields': [{
|
||||
'fieldname': 'test',
|
||||
'label': 'Test',
|
||||
'fieldtype': 'Data'
|
||||
}],
|
||||
'permissions': [{
|
||||
'role': 'System Manager',
|
||||
'read': 1,
|
||||
'write': 1,
|
||||
'create': 1,
|
||||
'delete': 1,
|
||||
'submit': 1,
|
||||
'cancel': 1,
|
||||
'amend': 1
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
doc.allow_auto_repeat = 1
|
||||
doc.save()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-11-10 22:30:53.690228",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"day"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "day",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Day",
|
||||
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-10 22:30:53.690228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat Day",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DeskShortcut(Document):
|
||||
class AutoRepeatDay(Document):
|
||||
pass
|
||||
229
frappe/automation/workspace/tools/tools.json
Normal file
229
frappe/automation/workspace/tools/tools.json
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
{
|
||||
"category": "Administration",
|
||||
"charts": [],
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 0,
|
||||
"icon": "tool",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Tools",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Tools",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "To Do",
|
||||
"link_to": "ToDo",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Calendar",
|
||||
"link_to": "Event",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Note",
|
||||
"link_to": "Note",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Files",
|
||||
"link_to": "File",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Activity",
|
||||
"link_to": "activity",
|
||||
"link_type": "Page",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Newsletter",
|
||||
"link_to": "Newsletter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Group",
|
||||
"link_to": "Email Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Automation",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Assignment Rule",
|
||||
"link_to": "Assignment Rule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Milestone",
|
||||
"link_to": "Milestone",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Auto Repeat",
|
||||
"link_to": "Auto Repeat",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Streaming",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Producer",
|
||||
"link_to": "Event Producer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Consumer",
|
||||
"link_to": "Event Consumer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Update Log",
|
||||
"link_to": "Event Update Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Event Sync Log",
|
||||
"link_to": "Event Sync Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Document Type Mapping",
|
||||
"link_to": "Document Type Mapping",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:39.950350",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
"link_to": "ToDo",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Note",
|
||||
"link_to": "Note",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "File",
|
||||
"link_to": "File",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Assignment Rule",
|
||||
"link_to": "Assignment Rule",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Auto Repeat",
|
||||
"link_to": "Auto Repeat",
|
||||
"type": "DocType"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl
|
|||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
||||
|
||||
def get_bootinfo():
|
||||
"""build and return boot info"""
|
||||
|
|
@ -39,7 +39,7 @@ def get_bootinfo():
|
|||
bootinfo.server_date = frappe.utils.nowdate()
|
||||
|
||||
if frappe.session['user'] != 'Guest':
|
||||
bootinfo.user_info = get_fullnames()
|
||||
bootinfo.user_info = get_user_info()
|
||||
bootinfo.sid = frappe.session['sid']
|
||||
|
||||
bootinfo.modules = {}
|
||||
|
|
@ -48,6 +48,7 @@ def get_bootinfo():
|
|||
bootinfo.letter_heads = get_letter_heads()
|
||||
bootinfo.active_domains = frappe.get_active_domains()
|
||||
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")]
|
||||
add_layouts(bootinfo)
|
||||
|
||||
bootinfo.module_app = frappe.local.module_app
|
||||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})]
|
||||
|
|
@ -61,6 +62,7 @@ def get_bootinfo():
|
|||
doclist.extend(get_meta_bundle("Page"))
|
||||
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
|
||||
bootinfo.navbar_settings = get_navbar_settings()
|
||||
bootinfo.notification_settings = get_notification_settings()
|
||||
|
||||
# ipinfo
|
||||
if frappe.session.data.get('ipinfo'):
|
||||
|
|
@ -88,6 +90,8 @@ def get_bootinfo():
|
|||
bootinfo.frequently_visited_links = frequently_visited_links()
|
||||
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
|
||||
bootinfo.additional_filters_config = get_additional_filters_from_hooks()
|
||||
bootinfo.desk_settings = get_desk_settings()
|
||||
bootinfo.app_logo_url = get_app_logo()
|
||||
|
||||
return bootinfo
|
||||
|
||||
|
|
@ -106,11 +110,9 @@ def load_conf_settings(bootinfo):
|
|||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False)
|
||||
bootinfo.module_page_map = get_controller("Desk Page").get_module_page_map()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items()
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
def get_allowed_pages(cache=False):
|
||||
|
|
@ -222,19 +224,18 @@ def load_translations(bootinfo):
|
|||
|
||||
bootinfo["__messages"] = messages
|
||||
|
||||
def get_fullnames():
|
||||
"""map of user fullnames"""
|
||||
ret = frappe.db.sql("""select `name`, full_name as fullname,
|
||||
user_image as image, gender, email, username, bio, location, interest, banner_image, allowed_in_mentions
|
||||
from tabUser where enabled=1 and user_type!='Website User'""", as_dict=1)
|
||||
def get_user_info():
|
||||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image',
|
||||
'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'],
|
||||
filters=dict(enabled=1))
|
||||
|
||||
d = {}
|
||||
for r in ret:
|
||||
# if not r.image:
|
||||
# r.image = get_gravatar(r.name)
|
||||
d[r.name] = r
|
||||
user_info_map = {d.name: d for d in user_info}
|
||||
|
||||
return d
|
||||
admin_data = user_info_map.get('Administrator')
|
||||
if admin_data:
|
||||
user_info_map[admin_data.email] = admin_data
|
||||
|
||||
return user_info_map
|
||||
|
||||
def get_user(bootinfo):
|
||||
"""get user info"""
|
||||
|
|
@ -251,13 +252,12 @@ def add_home_page(bootinfo, docs):
|
|||
|
||||
try:
|
||||
page = frappe.desk.desk_page.get(home_page)
|
||||
docs.append(page)
|
||||
bootinfo['home_page'] = page.name
|
||||
except (frappe.DoesNotExistError, frappe.PermissionError):
|
||||
if frappe.message_log:
|
||||
frappe.message_log.pop()
|
||||
page = frappe.desk.desk_page.get('workspace')
|
||||
|
||||
bootinfo['home_page'] = page.name
|
||||
docs.append(page)
|
||||
bootinfo['home_page'] = 'Workspaces'
|
||||
|
||||
def add_timezone_info(bootinfo):
|
||||
system = bootinfo.sysdefaults.get("time_zone")
|
||||
|
|
@ -273,7 +273,7 @@ def load_print(bootinfo, doclist):
|
|||
|
||||
def load_print_css(bootinfo, print_settings):
|
||||
import frappe.www.printview
|
||||
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Modern", for_legacy=True)
|
||||
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Redesign", for_legacy=True)
|
||||
|
||||
def get_unseen_notes():
|
||||
return frappe.db.sql('''select `name`, title, content, notify_on_every_login from `tabNote` where notify_on_login=1
|
||||
|
|
@ -308,3 +308,24 @@ def get_additional_filters_from_hooks():
|
|||
filter_config.update(frappe.get_attr(hook)())
|
||||
|
||||
return filter_config
|
||||
|
||||
def add_layouts(bootinfo):
|
||||
# add routes for readable doctypes
|
||||
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type'])
|
||||
|
||||
def get_desk_settings():
|
||||
role_list = frappe.get_all('Role', fields=['*'], filters=dict(
|
||||
name=['in', frappe.get_roles()]
|
||||
))
|
||||
desk_settings = {}
|
||||
|
||||
from frappe.core.doctype.role.role import desk_properties
|
||||
|
||||
for role in role_list:
|
||||
for key in desk_properties:
|
||||
desk_settings[key] = desk_settings.get(key) or role.get(key)
|
||||
|
||||
return desk_settings
|
||||
|
||||
def get_notification_settings():
|
||||
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import frappe
|
|||
from frappe.utils.minify import JavascriptMinify
|
||||
|
||||
import click
|
||||
from requests import get
|
||||
import psutil
|
||||
from six import iteritems, text_type
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
|
|
@ -26,6 +26,8 @@ sites_path = os.path.abspath(os.getcwd())
|
|||
|
||||
|
||||
def download_file(url, prefix):
|
||||
from requests import get
|
||||
|
||||
filename = urlparse(url).path.split("/")[-1]
|
||||
local_filename = os.path.join(prefix, filename)
|
||||
with get(url, stream=True, allow_redirects=True) as r:
|
||||
|
|
@ -40,6 +42,7 @@ def build_missing_files():
|
|||
# check which files dont exist yet from the build.json and tell build.js to build only those!
|
||||
missing_assets = []
|
||||
current_asset_files = []
|
||||
frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json")
|
||||
|
||||
for type in ["css", "js"]:
|
||||
current_asset_files.extend(
|
||||
|
|
@ -49,7 +52,7 @@ def build_missing_files():
|
|||
]
|
||||
)
|
||||
|
||||
with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f:
|
||||
with open(frappe_build) as f:
|
||||
all_asset_files = json.load(f).keys()
|
||||
|
||||
for asset in all_asset_files:
|
||||
|
|
@ -104,20 +107,28 @@ def download_frappe_assets(verbose=True):
|
|||
if frappe_head:
|
||||
try:
|
||||
url = get_assets_link(frappe_head)
|
||||
click.secho("Retreiving assets...", fg="yellow")
|
||||
click.secho("Retrieving 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
|
||||
directories_created = set()
|
||||
|
||||
click.secho("\nExtracting assets...\n", fg="yellow")
|
||||
with tarfile.open(assets_archive) as tar:
|
||||
for file in tar:
|
||||
if not file.isdir():
|
||||
dest = "." + file.name.replace("./frappe-bench/sites", "")
|
||||
asset_directory = os.path.dirname(dest)
|
||||
show = dest.replace("./assets/", "")
|
||||
|
||||
if asset_directory not in directories_created:
|
||||
if not os.path.exists(asset_directory):
|
||||
os.makedirs(asset_directory, exist_ok=True)
|
||||
directories_created.add(asset_directory)
|
||||
|
||||
tar.makefile(file, dest)
|
||||
print("{0} Restored {1}".format(green('✔'), show))
|
||||
|
||||
|
|
@ -216,7 +227,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False,
|
|||
|
||||
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
|
||||
check_yarn()
|
||||
frappe.commands.popen(command, cwd=frappe_app_path)
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
|
||||
|
||||
def watch(no_compress):
|
||||
|
|
@ -228,13 +239,32 @@ def watch(no_compress):
|
|||
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.commands.popen("{pacman} run watch".format(pacman=pacman),
|
||||
cwd=frappe_app_path, env=get_node_env())
|
||||
|
||||
|
||||
def check_yarn():
|
||||
if not find_executable("yarn"):
|
||||
print("Please install yarn using below command and try again.\nnpm install -g yarn")
|
||||
|
||||
def get_node_env():
|
||||
node_env = {
|
||||
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
|
||||
}
|
||||
return node_env
|
||||
|
||||
def get_safe_max_old_space_size():
|
||||
safe_max_old_space_size = 0
|
||||
try:
|
||||
total_memory = psutil.virtual_memory().total / (1024 * 1024)
|
||||
# reference for the safe limit assumption
|
||||
# https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes
|
||||
# set minimum value 1GB
|
||||
safe_max_old_space_size = max(1024, int(total_memory * 0.75))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return safe_max_old_space_size
|
||||
|
||||
def make_asset_dirs(make_copy=False, restore=False):
|
||||
# don't even think of making assets_path absolute - rm -rf ahead.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ common_default_keys = ["__default", "__global"]
|
|||
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
|
||||
'milestone_tracker_map', 'event_consumer_document_type_map')
|
||||
|
||||
global_cache_keys = ("app_hooks", "installed_apps",
|
||||
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
|
||||
"app_modules", "module_app", "system_settings",
|
||||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
|
|
@ -67,19 +67,18 @@ def clear_defaults_cache(user=None):
|
|||
elif frappe.flags.in_install!="frappe":
|
||||
frappe.cache().delete_key("defaults")
|
||||
|
||||
def clear_document_cache():
|
||||
frappe.local.document_cache = {}
|
||||
frappe.cache().delete_key("document_cache")
|
||||
|
||||
def clear_doctype_cache(doctype=None):
|
||||
clear_controller_cache(doctype)
|
||||
cache = frappe.cache()
|
||||
|
||||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
|
||||
del frappe.local.meta_cache[doctype]
|
||||
|
||||
for key in ('is_table', 'doctype_modules'):
|
||||
for key in ('is_table', 'doctype_modules', 'document_cache'):
|
||||
cache.delete_value(key)
|
||||
|
||||
frappe.local.document_cache = {}
|
||||
|
||||
def clear_single(dt):
|
||||
for name in doctype_cache_keys:
|
||||
cache.hdel(name, dt)
|
||||
|
|
@ -101,8 +100,14 @@ def clear_doctype_cache(doctype=None):
|
|||
for name in doctype_cache_keys:
|
||||
cache.delete_value(name)
|
||||
|
||||
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
|
||||
clear_document_cache()
|
||||
def clear_controller_cache(doctype=None):
|
||||
if not doctype:
|
||||
del frappe.controllers
|
||||
frappe.controllers = {}
|
||||
return
|
||||
|
||||
for site_controllers in frappe.controllers.values():
|
||||
site_controllers.pop(doctype, None)
|
||||
|
||||
def get_doctype_map(doctype, name, filters=None, order_by=None):
|
||||
cache = frappe.cache()
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - standard imports
|
||||
import json
|
||||
from collections.abc import MutableMapping, MutableSequence, Sequence
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
|
||||
# imports - compatibility imports
|
||||
import six
|
||||
|
||||
# imports - standard imports
|
||||
from collections import Sequence, MutableSequence, Mapping, MutableMapping
|
||||
if six.PY2:
|
||||
from urlparse import urlparse # PY2
|
||||
else:
|
||||
from urllib.parse import urlparse # PY3
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe.exceptions import DuplicateEntryError
|
||||
from frappe import _dict
|
||||
import frappe
|
||||
from frappe.exceptions import DuplicateEntryError
|
||||
from frappe.model.document import Document
|
||||
|
||||
session = frappe.session
|
||||
|
||||
|
||||
def get_user_doc(user = None):
|
||||
if isinstance(user, Document):
|
||||
return user
|
||||
|
|
@ -38,12 +32,12 @@ def squashify(what):
|
|||
return what
|
||||
|
||||
def safe_json_loads(*args):
|
||||
results = [ ]
|
||||
results = []
|
||||
|
||||
for arg in args:
|
||||
try:
|
||||
arg = json.loads(arg)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
results.append(arg)
|
||||
|
|
@ -81,7 +75,7 @@ def dictify(arg):
|
|||
for i, a in enumerate(arg):
|
||||
arg[i] = dictify(a)
|
||||
elif isinstance(arg, MutableMapping):
|
||||
arg = _dict(arg)
|
||||
arg = frappe._dict(arg)
|
||||
|
||||
return arg
|
||||
|
||||
|
|
@ -113,4 +107,4 @@ def get_emojis():
|
|||
emojis = resp.json()
|
||||
redis.hset('frappe_emojis', 'emojis', emojis)
|
||||
|
||||
return dictify(emojis)
|
||||
return dictify(emojis)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import frappe.utils
|
|||
import subprocess # nosec
|
||||
from functools import wraps
|
||||
from six import StringIO
|
||||
from os import environ
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
|
|
@ -53,16 +54,20 @@ def get_site(context, raise_err=True):
|
|||
return None
|
||||
|
||||
def popen(command, *args, **kwargs):
|
||||
output = kwargs.get('output', True)
|
||||
cwd = kwargs.get('cwd')
|
||||
shell = kwargs.get('shell', True)
|
||||
output = kwargs.get('output', True)
|
||||
cwd = kwargs.get('cwd')
|
||||
shell = kwargs.get('shell', True)
|
||||
raise_err = kwargs.get('raise_err')
|
||||
env = kwargs.get('env')
|
||||
if env:
|
||||
env = dict(environ, **env)
|
||||
|
||||
proc = subprocess.Popen(command,
|
||||
stdout = None if output else subprocess.PIPE,
|
||||
stderr = None if output else subprocess.PIPE,
|
||||
shell = shell,
|
||||
cwd = cwd
|
||||
stdout=None if output else subprocess.PIPE,
|
||||
stderr=None if output else subprocess.PIPE,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=env
|
||||
)
|
||||
|
||||
return_ = proc.wait()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
# imports - standard imports
|
||||
import atexit
|
||||
import compileall
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# imports - third party imports
|
||||
|
|
@ -13,10 +8,7 @@ import click
|
|||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.exceptions import SiteNotSpecifiedError
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import get_site_path, touch_file
|
||||
|
||||
|
||||
@click.command('new-site')
|
||||
|
|
@ -38,6 +30,8 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
|
||||
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None):
|
||||
"Create a new site"
|
||||
from frappe.installer import _new_site
|
||||
|
||||
frappe.init(site=site, new_site=True)
|
||||
|
||||
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
|
|
@ -49,54 +43,6 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
if len(frappe.utils.get_sites()) == 1:
|
||||
use(site)
|
||||
|
||||
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
|
||||
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
|
||||
no_mariadb_socket=False, reinstall=False, db_password=None, db_type=None, db_host=None,
|
||||
db_port=None, new_site=False):
|
||||
"""Install a new Frappe site"""
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
print('Site {0} already exists'.format(site))
|
||||
sys.exit(1)
|
||||
|
||||
if no_mariadb_socket and not db_type == "mariadb":
|
||||
print('--no-mariadb-socket requires db_type to be set to mariadb.')
|
||||
sys.exit(1)
|
||||
|
||||
if not db_name:
|
||||
db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
|
||||
|
||||
from frappe.installer import install_db, make_site_dirs
|
||||
from frappe.installer import install_app as _install_app
|
||||
import frappe.utils.scheduler
|
||||
|
||||
frappe.init(site=site)
|
||||
|
||||
try:
|
||||
# enable scheduler post install?
|
||||
enable_scheduler = _is_scheduler_enabled()
|
||||
except Exception:
|
||||
enable_scheduler = False
|
||||
|
||||
make_site_dirs()
|
||||
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
for app in apps_to_install:
|
||||
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
|
||||
|
||||
os.remove(installing)
|
||||
|
||||
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
|
||||
frappe.db.commit()
|
||||
|
||||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
|
||||
|
||||
@click.command('restore')
|
||||
@click.argument('sql-file-path')
|
||||
|
|
@ -107,36 +53,46 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
@click.option('--install-app', multiple=True, help='Install app after installation')
|
||||
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
|
||||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
|
||||
@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done')
|
||||
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
|
||||
"Restore site database from an sql file"
|
||||
from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade
|
||||
from frappe.installer import (
|
||||
_new_site,
|
||||
extract_sql_from_archive,
|
||||
extract_files,
|
||||
is_downgrade,
|
||||
is_partial,
|
||||
validate_database_sql
|
||||
)
|
||||
|
||||
force = context.force or force
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
|
||||
if not os.path.exists(sql_file_path):
|
||||
base_path = '..'
|
||||
sql_file_path = os.path.join(base_path, sql_file_path)
|
||||
if not os.path.exists(sql_file_path):
|
||||
print('Invalid path {0}'.format(sql_file_path[3:]))
|
||||
sys.exit(1)
|
||||
elif sql_file_path.startswith(os.sep):
|
||||
base_path = os.sep
|
||||
else:
|
||||
base_path = '.'
|
||||
# check if partial backup
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if sql_file_path.endswith('sql.gz'):
|
||||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path))
|
||||
else:
|
||||
decompressed_file_name = sql_file_path
|
||||
# check if valid SQL file
|
||||
validate_database_sql(decompressed_file_name, _raise=not force)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
if not force and is_downgrade(decompressed_file_name, verbose=True):
|
||||
warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?"
|
||||
warn_message = (
|
||||
"This is not recommended and may lead to unexpected behaviour. "
|
||||
"Do you want to continue anyway?"
|
||||
)
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
|
|
@ -146,22 +102,39 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
with_public_files = os.path.join(base_path, with_public_files)
|
||||
public = extract_tar_files(site, with_public_files, 'public')
|
||||
public = extract_files(site, with_public_files)
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
with_private_files = os.path.join(base_path, with_private_files)
|
||||
private = extract_tar_files(site, with_private_files, 'private')
|
||||
private = extract_files(site, with_private_files)
|
||||
os.remove(private)
|
||||
|
||||
# Removing temporarily created file
|
||||
if decompressed_file_name != sql_file_path:
|
||||
os.remove(decompressed_file_name)
|
||||
|
||||
success_message = "Site {0} has been restored{1}".format(site, " with files" if (with_public_files or with_private_files) else "")
|
||||
success_message = "Site {0} has been restored{1}".format(
|
||||
site,
|
||||
" with files" if (with_public_files or with_private_files) else ""
|
||||
)
|
||||
click.secho(success_message, fg="green")
|
||||
|
||||
|
||||
@click.command('partial-restore')
|
||||
@click.argument('sql-file-path')
|
||||
@click.option("--verbose", "-v", is_flag=True)
|
||||
@pass_context
|
||||
def partial_restore(context, sql_file_path, verbose):
|
||||
from frappe.installer import partial_restore
|
||||
verbose = context.verbose or verbose
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
frappe.connect(site=site)
|
||||
partial_restore(sql_file_path, verbose)
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('reinstall')
|
||||
@click.option('--admin-password', help='Administrator Password for reinstalled site')
|
||||
@click.option('--mariadb-root-username', help='Root username for MariaDB')
|
||||
|
|
@ -174,6 +147,8 @@ def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_
|
|||
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
|
||||
|
||||
def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
|
||||
from frappe.installer import _new_site
|
||||
|
||||
if not yes:
|
||||
click.confirm('This will wipe your database. Are you sure you want to reinstall?', abort=True)
|
||||
try:
|
||||
|
|
@ -226,15 +201,51 @@ def install_app(context, apps):
|
|||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('list-apps')
|
||||
@click.command("list-apps")
|
||||
@pass_context
|
||||
def list_apps(context):
|
||||
"List apps in site"
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
print("\n".join(frappe.get_installed_apps()))
|
||||
frappe.destroy()
|
||||
|
||||
def fix_whitespaces(text):
|
||||
if site == context.sites[-1]:
|
||||
text = text.rstrip()
|
||||
if len(context.sites) == 1:
|
||||
text = text.lstrip()
|
||||
return text
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
site_title = (
|
||||
click.style(f"{site}", fg="green") if len(context.sites) > 1 else ""
|
||||
)
|
||||
apps = frappe.get_single("Installed Applications").installed_applications
|
||||
|
||||
if apps:
|
||||
name_len, ver_len = [
|
||||
max([len(x.get(y)) for x in apps])
|
||||
for y in ["app_name", "app_version"]
|
||||
]
|
||||
template = "{{0:{0}}} {{1:{1}}} {{2}}".format(name_len, ver_len)
|
||||
|
||||
installed_applications = [
|
||||
template.format(app.app_name, app.app_version, app.git_branch)
|
||||
for app in apps
|
||||
]
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
|
||||
else:
|
||||
applications_summary = "\n".join(frappe.get_installed_apps())
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
|
||||
summary = fix_whitespaces(summary)
|
||||
|
||||
if applications_summary and summary:
|
||||
print(summary)
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
@click.command('add-system-manager')
|
||||
@click.argument('email')
|
||||
|
|
@ -269,13 +280,13 @@ def disable_user(context, email):
|
|||
user.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@click.command('migrate')
|
||||
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
|
||||
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
|
||||
@pass_context
|
||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||
"Run patches, sync schema and rebuild files/translations"
|
||||
import re
|
||||
from frappe.migrate import migrate
|
||||
|
||||
for site in context.sites:
|
||||
|
|
@ -293,9 +304,6 @@ def migrate(context, skip_failing=False, skip_search_index=False):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
print("Compiling Python files...")
|
||||
compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
|
||||
|
||||
@click.command('migrate-to')
|
||||
@click.argument('frappe_provider')
|
||||
@pass_context
|
||||
|
|
@ -312,15 +320,16 @@ def migrate_to(context, frappe_provider):
|
|||
|
||||
@click.command('run-patch')
|
||||
@click.argument('module')
|
||||
@click.option('--force', is_flag=True)
|
||||
@pass_context
|
||||
def run_patch(context, module):
|
||||
def run_patch(context, module, force):
|
||||
"Run a particular patch"
|
||||
import frappe.modules.patch_handler
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
try:
|
||||
frappe.connect()
|
||||
frappe.modules.patch_handler.run_single(module, force=context.force)
|
||||
frappe.modules.patch_handler.run_single(module, force=force or context.force)
|
||||
finally:
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
|
|
@ -385,35 +394,54 @@ def use(site, sites_path='.'):
|
|||
|
||||
@click.command('backup')
|
||||
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
|
||||
@click.option('--verbose', default=False, is_flag=True)
|
||||
@click.option('--include', '--only', '-i', default="", type=str, help="Specify the DocTypes to backup seperated by commas")
|
||||
@click.option('--exclude', '-e', default="", type=str, help="Specify the DocTypes to not backup seperated by commas")
|
||||
@click.option('--backup-path', default=None, help="Set path for saving all the files in this operation")
|
||||
@click.option('--backup-path-db', default=None, help="Set path for saving database file")
|
||||
@click.option('--backup-path-files', default=None, help="Set path for saving public file")
|
||||
@click.option('--backup-path-private-files', default=None, help="Set path for saving private file")
|
||||
@click.option('--backup-path-conf', default=None, help="Set path for saving config file")
|
||||
@click.option('--ignore-backup-conf', default=False, is_flag=True, help="Ignore excludes/includes set in config")
|
||||
@click.option('--verbose', default=False, is_flag=True, help="Add verbosity")
|
||||
@click.option('--compress', default=False, is_flag=True, help="Compress private and public files")
|
||||
@pass_context
|
||||
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
|
||||
backup_path_private_files=None, quiet=False, verbose=False):
|
||||
def backup(context, with_files=False, backup_path=None, backup_path_db=None, backup_path_files=None,
|
||||
backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
|
||||
compress=False, include="", exclude=""):
|
||||
"Backup"
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = verbose or context.verbose
|
||||
exit_code = 0
|
||||
|
||||
for site in context.sites:
|
||||
try:
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
|
||||
except Exception as e:
|
||||
odb = scheduled_backup(
|
||||
ignore_files=not with_files,
|
||||
backup_path=backup_path,
|
||||
backup_path_db=backup_path_db,
|
||||
backup_path_files=backup_path_files,
|
||||
backup_path_private_files=backup_path_private_files,
|
||||
backup_path_conf=backup_path_conf,
|
||||
ignore_conf=ignore_backup_conf,
|
||||
include_doctypes=include,
|
||||
exclude_doctypes=exclude,
|
||||
compress=compress,
|
||||
verbose=verbose,
|
||||
force=True
|
||||
)
|
||||
except Exception:
|
||||
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
|
||||
if verbose:
|
||||
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
|
||||
print(frappe.get_traceback())
|
||||
exit_code = 1
|
||||
continue
|
||||
|
||||
if verbose:
|
||||
from frappe.utils import now
|
||||
summary_title = "Backup Summary at {0}".format(now())
|
||||
print(summary_title + "\n" + "-" * len(summary_title))
|
||||
print("Database backup:", odb.backup_path_db)
|
||||
if with_files:
|
||||
print("Public files: ", odb.backup_path_files)
|
||||
print("Private files: ", odb.backup_path_private_files)
|
||||
|
||||
odb.print_summary()
|
||||
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
|
|
@ -482,13 +510,14 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
if force:
|
||||
pass
|
||||
else:
|
||||
click.echo("="*80)
|
||||
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
|
||||
click.echo("Reason: {reason}{sep}".format(reason=str(err), sep="\n"))
|
||||
click.echo("Fix the issue and try again.")
|
||||
click.echo(
|
||||
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
|
||||
)
|
||||
messages = [
|
||||
"=" * 80,
|
||||
"Error: The operation has stopped because backup of {0}'s database failed.".format(site),
|
||||
"Reason: {0}\n".format(str(err)),
|
||||
"Fix the issue and try again.",
|
||||
"Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site)
|
||||
]
|
||||
click.echo("\n".join(messages))
|
||||
sys.exit(1)
|
||||
|
||||
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
|
||||
|
|
@ -618,8 +647,10 @@ def browse(context, site):
|
|||
@click.command('start-recording')
|
||||
@pass_context
|
||||
def start_recording(context):
|
||||
import frappe.recorder
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.set_user("Administrator")
|
||||
frappe.recorder.start()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
|
@ -628,8 +659,10 @@ def start_recording(context):
|
|||
@click.command('stop-recording')
|
||||
@pass_context
|
||||
def stop_recording(context):
|
||||
import frappe.recorder
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
frappe.set_user("Administrator")
|
||||
frappe.recorder.stop()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
|
@ -700,5 +733,6 @@ commands = [
|
|||
stop_recording,
|
||||
add_to_hosts,
|
||||
start_ngrok,
|
||||
build_search_index
|
||||
build_search_index,
|
||||
partial_restore
|
||||
]
|
||||
|
|
|
|||
|
|
@ -460,11 +460,21 @@ def console(context):
|
|||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
frappe.local.lang = frappe.db.get_default("lang")
|
||||
|
||||
import IPython
|
||||
all_apps = frappe.get_installed_apps()
|
||||
failed_to_import = []
|
||||
|
||||
for app in all_apps:
|
||||
locals()[app] = __import__(app)
|
||||
try:
|
||||
locals()[app] = __import__(app)
|
||||
except ModuleNotFoundError:
|
||||
failed_to_import.append(app)
|
||||
|
||||
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
|
||||
if failed_to_import:
|
||||
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
|
||||
|
||||
IPython.embed(display_banner="", header="", colors="neutral")
|
||||
|
||||
|
||||
|
|
@ -554,10 +564,25 @@ def run_ui_tests(context, app, headless=False):
|
|||
site_env = 'CYPRESS_baseUrl={}'.format(site_url)
|
||||
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
|
||||
|
||||
os.chdir(app_base_path)
|
||||
|
||||
node_bin = subprocess.getoutput("npm bin")
|
||||
cypress_path = "{0}/cypress".format(node_bin)
|
||||
plugin_path = "{0}/cypress-file-upload".format(node_bin)
|
||||
|
||||
# check if cypress in path...if not, install it.
|
||||
if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \
|
||||
or not subprocess.getoutput("npm view cypress version").startswith("6."):
|
||||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
|
||||
command = '{site_env} {password_env} {cypress} {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
|
||||
|
||||
click.secho("Running Cypress...", fg="yellow")
|
||||
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -108,4 +108,4 @@ def is_domain(module):
|
|||
return module.get("category") == "Domains"
|
||||
|
||||
def is_module(module):
|
||||
return module.get("type") == "module"
|
||||
return module.get("type") == "module"
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
data = [
|
||||
{
|
||||
"label": _("Automation"),
|
||||
"icon": "fa fa-random",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Assignment Rule",
|
||||
"description": _("Set up rules for user assignments.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Milestone",
|
||||
"description": _("Tracks milestones on the lifecycle of a document if it undergoes multiple stages.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Auto Repeat",
|
||||
"description": _("Automatically generates recurring documents.")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Event Streaming"),
|
||||
"icon": "fa fa-random",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Event Producer",
|
||||
"description": _("The site you want to subscribe to for consuming events.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Event Consumer",
|
||||
"description": _("The site which is consuming your events.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Event Update Log",
|
||||
"description": _("Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Event Sync Log",
|
||||
"description": _("Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Document Type Mapping",
|
||||
"description": _("The mapping configuration between two doctypes.")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return data
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Documents"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "DocType",
|
||||
"description": _("Models (building blocks) of the Application"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Module Def",
|
||||
"description": _("Groups of DocTypes"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Page",
|
||||
"description": _("Pages in Desk (place holders)"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Report",
|
||||
"description": _("Script or Query reports"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Format",
|
||||
"description": _("Customized Formats for Printing, Email"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Custom Script",
|
||||
"description": _("Client side script extensions in Javascript"),
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Logs"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Log",
|
||||
"description": _("Errors in Background Events"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Queue",
|
||||
"description": _("Background Email Queue"),
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("Background Jobs"),
|
||||
"name": "background_jobs",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Snapshot",
|
||||
"description": _("A log of request errors"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Form Customization"),
|
||||
"icon": "fa fa-glass",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Customize Form",
|
||||
"description": _("Change field properties (hide, readonly, permission etc.)")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Custom Field",
|
||||
"description": _("Add fields to forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Custom Script",
|
||||
"description": _("Add custom javascript to forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "DocType",
|
||||
"description": _("Add custom forms.")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Dashboards"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard Chart",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dashboard Chart Source",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Other"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"label": _("Custom Translations"),
|
||||
"name": "Translation",
|
||||
"description": _("Add your own translations")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"label": _("Package"),
|
||||
"name": "Package",
|
||||
"description": _("Import and Export Packages.")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Tools"),
|
||||
"icon": "octicon octicon-briefcase",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "ToDo",
|
||||
"label": _("To Do"),
|
||||
"description": _("Documents assigned to you and by you."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Event",
|
||||
"label": _("Calendar"),
|
||||
"link": "List/Event/Calendar",
|
||||
"description": _("Event and other calendars."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Note",
|
||||
"description": _("Private and public Notes."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "File",
|
||||
"label": _("Files"),
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("Chat"),
|
||||
"name": "chat",
|
||||
"description": _("Chat messages and other notifications."),
|
||||
"data_doctype": "Communication"
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("Activity"),
|
||||
"name": "activity",
|
||||
"description": _("Activity log of all users."),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': _('Email'),
|
||||
'items': [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Newsletter",
|
||||
"description": _("Newsletters to contacts, leads."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Group",
|
||||
"description": _("Email Group List"),
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
# Administration
|
||||
{
|
||||
"module_name": "Desk",
|
||||
"category": "Administration",
|
||||
"label": _("Tools"),
|
||||
"color": "#FFF5A7",
|
||||
"reverse": 1,
|
||||
"icon": "octicon octicon-calendar",
|
||||
"type": "module",
|
||||
"description": "Todos, notes, calendar and newsletter."
|
||||
},
|
||||
{
|
||||
"module_name": "Settings",
|
||||
"category": "Administration",
|
||||
"label": _("Settings"),
|
||||
"color": "#bdc3c7",
|
||||
"reverse": 1,
|
||||
"icon": "octicon octicon-settings",
|
||||
"type": "module",
|
||||
"description": "Data import, printing, email and workflows."
|
||||
},
|
||||
{
|
||||
"module_name": "Automation",
|
||||
"category": "Administration",
|
||||
"label": _("Automation"),
|
||||
"color": "#bdc3c7",
|
||||
"reverse": 1,
|
||||
"icon": "octicon octicon-gist",
|
||||
"type": "module",
|
||||
"description": "Auto Repeat, Assignment Rule, Milestone Tracking and Event Streaming."
|
||||
},
|
||||
{
|
||||
"module_name": "Users and Permissions",
|
||||
"category": "Administration",
|
||||
"label": _("Users and Permissions"),
|
||||
"color": "#bdc3c7",
|
||||
"reverse": 1,
|
||||
"icon": "octicon octicon-settings",
|
||||
"type": "module",
|
||||
"description": "Setup roles and permissions for users on documents."
|
||||
},
|
||||
{
|
||||
"module_name": "Customization",
|
||||
"category": "Administration",
|
||||
"label": _("Customization"),
|
||||
"color": "#bdc3c7",
|
||||
"reverse": 1,
|
||||
"icon": "octicon octicon-settings",
|
||||
"type": "module",
|
||||
"description": "Customize forms, custom fields, scripts and translations."
|
||||
},
|
||||
{
|
||||
"module_name": "Integrations",
|
||||
"category": "Administration",
|
||||
"label": _("Integrations"),
|
||||
"color": "#16a085",
|
||||
"icon": "octicon octicon-globe",
|
||||
"type": "module",
|
||||
"description": "DropBox, Woocomerce, AWS, Shopify and GoCardless."
|
||||
},
|
||||
{
|
||||
"module_name": 'Contacts',
|
||||
"category": "Administration",
|
||||
"label": _("Contacts"),
|
||||
"type": 'module',
|
||||
"icon": "octicon octicon-book",
|
||||
"color": '#ffaedb',
|
||||
"description": "People Contacts and Address Book."
|
||||
},
|
||||
{
|
||||
"module_name": "Core",
|
||||
"category": "Administration",
|
||||
"_label": _("Developer"),
|
||||
"label": "Developer",
|
||||
"color": "#589494",
|
||||
"icon": "octicon octicon-circuit-board",
|
||||
"type": "module",
|
||||
"system_manager": 1,
|
||||
"condition": getattr(frappe.local.conf, 'developer_mode', 0),
|
||||
"description": "Doctypes, dev tools and logs."
|
||||
},
|
||||
|
||||
# Places
|
||||
{
|
||||
"module_name": "Website",
|
||||
"category": "Places",
|
||||
"label": _("Website"),
|
||||
"_label": _("Website"),
|
||||
"color": "#16a085",
|
||||
"icon": "octicon octicon-globe",
|
||||
"type": "module",
|
||||
"description": "Webpages, webforms, blogs and website theme."
|
||||
},
|
||||
{
|
||||
"module_name": 'Social',
|
||||
"category": "Places",
|
||||
"label": _('Social'),
|
||||
"icon": "octicon octicon-heart",
|
||||
"type": 'link',
|
||||
"link": '#social/home',
|
||||
"color": '#FF4136',
|
||||
'standard': 1,
|
||||
'idx': 15,
|
||||
"description": "Build your profile and share posts with other users."
|
||||
},
|
||||
{
|
||||
"module_name": 'Leaderboard',
|
||||
"category": "Places",
|
||||
"label": _('Leaderboard'),
|
||||
"icon": "fa fa-trophy",
|
||||
"type": 'link',
|
||||
"link": '#leaderboard/User',
|
||||
"color": '#FF4136',
|
||||
'standard': 1,
|
||||
},
|
||||
{
|
||||
"module_name": 'dashboard',
|
||||
"category": "Places",
|
||||
"label": _('Dashboard'),
|
||||
"icon": "octicon octicon-graph",
|
||||
"type": "link",
|
||||
"link": "#dashboard",
|
||||
"color": '#FF4136',
|
||||
'standard': 1,
|
||||
'idx': 10
|
||||
},
|
||||
]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
source_link = "https://github.com/frappe/frappe_io"
|
||||
docs_base_url = "/docs"
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Payments"),
|
||||
"icon": "fa fa-star",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Braintree Settings",
|
||||
"description": _("Braintree payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "PayPal Settings",
|
||||
"description": _("PayPal payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Razorpay Settings",
|
||||
"description": _("Razorpay Payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Stripe Settings",
|
||||
"description": _("Stripe payment gateway settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Paytm Settings",
|
||||
"description": _("Paytm payment gateway settings"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Backup"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Dropbox Settings",
|
||||
"description": _("Dropbox backup settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "S3 Backup Settings",
|
||||
"description": _("S3 Backup Settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Drive",
|
||||
"description": _("Google Drive Backup."),
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Authentication"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Social Login Key",
|
||||
"description": _("Enter keys to enable login via Facebook, Google, GitHub."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "LDAP Settings",
|
||||
"description": _("Ldap settings"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "OAuth Client",
|
||||
"description": _("Register OAuth Client App"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "OAuth Provider Settings",
|
||||
"description": _("Settings for OAuth Provider"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Webhook"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Webhook",
|
||||
"description": _("Webhooks calling API requests into web apps"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Slack Webhook URL",
|
||||
"description": _("Slack Webhooks for internal integration"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Google Services"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Settings",
|
||||
"description": _("Google API Settings."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Contacts",
|
||||
"description": _("Google Contacts Integration."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Calendar",
|
||||
"description": _("Google Calendar Integration."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Google Drive",
|
||||
"description": _("Google Drive Integration."),
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.moduleview import add_setup_section
|
||||
|
||||
def get_data():
|
||||
data = [
|
||||
{
|
||||
"label": _("Core"),
|
||||
"icon": "fa fa-wrench",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "System Settings",
|
||||
"label": _("System Settings"),
|
||||
"description": _("Language, Date and Time settings"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Global Defaults",
|
||||
"label": _("Global Defaults"),
|
||||
"description": _("Company, Fiscal Year and Currency defaults"),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Log",
|
||||
"description": _("Log of error on automated events (scheduler).")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Error Snapshot",
|
||||
"description": _("Log of error during requests.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Domain Settings",
|
||||
"label": _("Domain Settings"),
|
||||
"description": _("Enable / Disable Domains"),
|
||||
"hide_count": True
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Data"),
|
||||
"icon": "fa fa-th",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Data Import",
|
||||
"label": _("Import Data"),
|
||||
"icon": "octicon octicon-cloud-upload",
|
||||
"description": _("Import Data from CSV / Excel files.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Data Export",
|
||||
"label": _("Export Data"),
|
||||
"icon": "octicon octicon-cloud-upload",
|
||||
"description": _("Export Data in CSV / Excel format.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Naming Series",
|
||||
"description": _("Set numbering series for transactions."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Rename Tool",
|
||||
"label": _("Bulk Rename"),
|
||||
"description": _("Rename many items by uploading a .csv file."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Bulk Update",
|
||||
"label": _("Bulk Update"),
|
||||
"description": _("Update many values at one time."),
|
||||
"hide_count": True
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"name": "backups",
|
||||
"label": _("Download Backups"),
|
||||
"description": _("List of backups available for download"),
|
||||
"icon": "fa fa-download"
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Deleted Document",
|
||||
"label": _("Deleted Documents"),
|
||||
"description": _("Restore or permanently delete a document.")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Email / Notifications"),
|
||||
"icon": "fa fa-envelope",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Account",
|
||||
"description": _("Add / Manage Email Accounts.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Domain",
|
||||
"description": _("Add / Manage Email Domains.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Notification",
|
||||
"description": _("Setup Notifications based on various criteria.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Email Template",
|
||||
"description": _("Email Templates for common queries.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Auto Email Report",
|
||||
"description": _("Setup Reports to be emailed at regular intervals"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Newsletter",
|
||||
"description": _("Create and manage newsletter")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"route": "Form/Notification Settings/{}".format(frappe.session.user),
|
||||
"name": "Notification Settings",
|
||||
"description": _("Configure notifications for mentions, assignments, energy points and more.")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Printing"),
|
||||
"icon": "fa fa-print",
|
||||
"items": [
|
||||
{
|
||||
"type": "page",
|
||||
"label": _("Print Format Builder"),
|
||||
"name": "print-format-builder",
|
||||
"description": _("Drag and Drop tool to build and customize Print Formats.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Settings",
|
||||
"description": _("Set default format, page size, print style etc.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Format",
|
||||
"description": _("Customized HTML Templates for printing transactions.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Print Style",
|
||||
"description": _("Stylesheets for Print Formats")
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Workflow"),
|
||||
"icon": "fa fa-random",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow",
|
||||
"description": _("Define workflows for forms.")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow State",
|
||||
"description": _("States for workflow (e.g. Draft, Approved, Cancelled).")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Workflow Action",
|
||||
"description": _("Actions for workflow (e.g. Approve, Cancel).")
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe")
|
||||
return data
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Users"),
|
||||
"icon": "fa fa-group",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "User",
|
||||
"description": _("System and Website Users")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Role",
|
||||
"description": _("User Roles")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Role Profile",
|
||||
"description": _("Role Profile")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Permissions"),
|
||||
"icon": "fa fa-lock",
|
||||
"items": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "permission-manager",
|
||||
"label": _("Role Permissions Manager"),
|
||||
"icon": "fa fa-lock",
|
||||
"description": _("Set Permissions on Document Types and Roles")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "User Permission",
|
||||
"label": _("User Permissions"),
|
||||
"icon": "fa fa-lock",
|
||||
"description": _("Restrict user for specific document")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Role Permission for Page and Report",
|
||||
"description": _("Set custom roles for page and report")
|
||||
},
|
||||
{
|
||||
"type": "report",
|
||||
"is_query_report": True,
|
||||
"doctype": "User",
|
||||
"icon": "fa fa-eye-open",
|
||||
"name": "Permitted Documents For User",
|
||||
"description": _("Check which Documents are readable by a User")
|
||||
},
|
||||
{
|
||||
"type": "report",
|
||||
"doctype": "DocShare",
|
||||
"icon": "fa fa-share",
|
||||
"name": "Document Share Report",
|
||||
"description": _("Report of all document shares")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Logs"),
|
||||
"icon": "fa fa-group",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Activity Log",
|
||||
"label": _("Activity Log"),
|
||||
"description": _("Activity Log by ")
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Access Log",
|
||||
"label": _("Access Log"),
|
||||
"description": _("View Log of all print, download and export events")
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{
|
||||
"label": _("Web Site"),
|
||||
"icon": "fa fa-star",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Web Page",
|
||||
"description": _("Content web page."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Web Form",
|
||||
"description": _("User editable form on Website."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Sidebar",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Slideshow",
|
||||
"description": _("Embed image slideshows in website pages."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Route Meta",
|
||||
"description": _("Add meta tags to your web pages"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Blog"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Blog Post",
|
||||
"description": _("Single Post (article)."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Blogger",
|
||||
"description": _("A user who posts blogs."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Blog Category",
|
||||
"description": _("Categorize blog posts."),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Setup"),
|
||||
"icon": "fa fa-cog",
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Settings",
|
||||
"description": _("Setup of top navigation bar, footer and logo."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Theme",
|
||||
"description": _("List of themes for Website."),
|
||||
"onboard": 1,
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Website Script",
|
||||
"description": _("Javascript to append to the head section of the page."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "About Us Settings",
|
||||
"description": _("Settings for About Us Page."),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Contact Us Settings",
|
||||
"description": _("Settings for Contact Us Page."),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Portal"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Portal Settings",
|
||||
"label": _("Portal Settings"),
|
||||
"onboard": 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": _("Knowledge Base"),
|
||||
"items": [
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Help Category",
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Help Article",
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-01-10 16:34:32",
|
||||
|
|
@ -24,7 +25,6 @@
|
|||
"is_shipping_address",
|
||||
"disabled",
|
||||
"linked_with",
|
||||
"is_your_company_address",
|
||||
"links"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
|
|
@ -138,12 +138,6 @@
|
|||
"label": "Reference",
|
||||
"options": "fa fa-pushpin"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Your Company Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "links",
|
||||
"fieldtype": "Table",
|
||||
|
|
@ -153,7 +147,8 @@
|
|||
],
|
||||
"icon": "fa fa-map-marker",
|
||||
"idx": 5,
|
||||
"modified": "2019-09-08 11:41:04.145589",
|
||||
"links": [],
|
||||
"modified": "2020-10-21 16:14:37.284830",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Address",
|
||||
|
|
|
|||
|
|
@ -39,14 +39,13 @@ class Address(Document):
|
|||
|
||||
def validate(self):
|
||||
self.link_address()
|
||||
self.validate_reference()
|
||||
self.validate_preferred_address()
|
||||
set_link_title(self)
|
||||
deduplicate_dynamic_links(self)
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
if not self.links and not self.is_your_company_address:
|
||||
if not self.links:
|
||||
contact_name = frappe.db.get_value("Contact", {"email_id": self.owner})
|
||||
if contact_name:
|
||||
contact = frappe.get_cached_doc('Contact', contact_name)
|
||||
|
|
@ -56,12 +55,6 @@ class Address(Document):
|
|||
|
||||
return False
|
||||
|
||||
def validate_reference(self):
|
||||
if self.is_your_company_address:
|
||||
if not [row for row in self.links if row.link_doctype == "Company"]:
|
||||
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table below."),
|
||||
title =_("Company not Linked"))
|
||||
|
||||
def validate_preferred_address(self):
|
||||
preferred_fields = ['is_primary_address', 'is_shipping_address']
|
||||
|
||||
|
|
@ -204,25 +197,6 @@ def get_address_templates(address):
|
|||
else:
|
||||
return result
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_shipping_address(company, address = None):
|
||||
filters = [
|
||||
["Dynamic Link", "link_doctype", "=", "Company"],
|
||||
["Dynamic Link", "link_name", "=", company],
|
||||
["Address", "is_your_company_address", "=", 1]
|
||||
]
|
||||
fields = ["*"]
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
filters.append(["Address", "name", "=", address])
|
||||
|
||||
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||
|
||||
if address:
|
||||
address_as_dict = address[0]
|
||||
name, address_template = get_address_templates(address_as_dict)
|
||||
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
|
||||
|
||||
def get_company_address(company):
|
||||
ret = frappe._dict()
|
||||
ret.company_address = get_default_address('Company', company)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@
|
|||
"email_ids",
|
||||
"phone_nos",
|
||||
"contact_details",
|
||||
"is_primary_contact",
|
||||
"links",
|
||||
"is_primary_contact",
|
||||
"more_info",
|
||||
"department",
|
||||
"unsubscribed"
|
||||
|
|
@ -248,8 +248,9 @@
|
|||
"icon": "fa fa-user",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-06 18:25:28.223693",
|
||||
"modified": "2020-08-27 14:12:09.906719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Contacts",
|
||||
"name": "Contact",
|
||||
|
|
|
|||
|
|
@ -256,3 +256,27 @@ def get_contact_with_phone_number(number):
|
|||
def get_contact_name(email_id):
|
||||
contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
|
||||
return contact[0].parent if contact else None
|
||||
|
||||
def get_contacts_linking_to(doctype, docname, fields=None):
|
||||
"""Return a list of contacts containing a link to the given document."""
|
||||
return frappe.get_list('Contact', fields=fields, filters=[
|
||||
['Dynamic Link', 'link_doctype', '=', doctype],
|
||||
['Dynamic Link', 'link_name', '=', docname]
|
||||
])
|
||||
|
||||
def get_contacts_linked_from(doctype, docname, fields=None):
|
||||
"""Return a list of contacts that are contained in (linked from) the given document."""
|
||||
link_fields = frappe.get_meta(doctype).get('fields', {
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Contact'
|
||||
})
|
||||
if not link_fields:
|
||||
return []
|
||||
|
||||
contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields])
|
||||
if not contact_names:
|
||||
return []
|
||||
|
||||
return frappe.get_list('Contact', fields=fields, filters={
|
||||
'name': ('in', contact_names)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Data",
|
||||
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Email / Notifications",
|
||||
"links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Website",
|
||||
"links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Core",
|
||||
"links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company, Fiscal Year and Currency defaults\",\n \"hide_count\": true,\n \"label\": \"Global Defaults\",\n \"name\": \"Global Defaults\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Printing",
|
||||
"links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Workflow",
|
||||
"links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Modules",
|
||||
"charts": [],
|
||||
"creation": "2020-03-02 15:09:40.527211",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 1,
|
||||
"docstatus": 0,
|
||||
"doctype": "Desk Page",
|
||||
"extends_another_page": 0,
|
||||
"hide_custom": 0,
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Settings",
|
||||
"modified": "2020-07-14 10:09:09.520557",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Settings",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 1,
|
||||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"icon": "octicon octicon-settings",
|
||||
"label": "System Settings",
|
||||
"link_to": "System Settings",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-print",
|
||||
"label": "Print Settings",
|
||||
"link_to": "Print Settings",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"icon": "fa fa-globe",
|
||||
"label": "Website Settings",
|
||||
"link_to": "Website Settings",
|
||||
"type": "DocType"
|
||||
}
|
||||
],
|
||||
"shortcuts_label": "Settings"
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"cards": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Users",
|
||||
"links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Logs",
|
||||
"links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"label": "Permissions",
|
||||
"links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]"
|
||||
}
|
||||
],
|
||||
"category": "Administration",
|
||||
"charts": [],
|
||||
"creation": "2020-03-02 15:12:16.754449",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Desk Page",
|
||||
"extends_another_page": 0,
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Users",
|
||||
"modified": "2020-04-26 22:36:14.311554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
"owner": "Administrator",
|
||||
"pin_to_bottom": 0,
|
||||
"pin_to_top": 0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "User",
|
||||
"link_to": "User",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Role",
|
||||
"link_to": "Role",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Permission Manager",
|
||||
"link_to": "permission-manager",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"label": "User Profile",
|
||||
"link_to": "user-profile",
|
||||
"type": "Page"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -40,7 +40,11 @@ def add_authentication_log(subject, user, operation="Login", status="Success"):
|
|||
"operation": operation,
|
||||
}).insert(ignore_permissions=True, ignore_links=True)
|
||||
|
||||
def clear_authentication_logs():
|
||||
"""clear 100 day old authentication logs"""
|
||||
def clear_activity_logs(days=None):
|
||||
"""clear 90 day old authentication logs or configured in log settings"""
|
||||
|
||||
if not days:
|
||||
days = 90
|
||||
|
||||
frappe.db.sql("""delete from `tabActivity Log` where \
|
||||
creation< (NOW() - INTERVAL '100' DAY)""")
|
||||
creation< (NOW() - INTERVAL '{0}' DAY)""".format(days))
|
||||
|
|
@ -77,6 +77,10 @@ class TestActivityLog(unittest.TestCase):
|
|||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
|
||||
# REMOVE ME: current logic allows allow_consecutive_login_attempts+1 attempts
|
||||
# before raising security exception, remove below line when that is fixed.
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
self.assertRaises(frappe.SecurityException, LoginManager)
|
||||
time.sleep(5)
|
||||
self.assertRaises(frappe.AuthenticationError, LoginManager)
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ from frappe.exceptions import ImplicitCommitError
|
|||
class Comment(Document):
|
||||
def after_insert(self):
|
||||
self.notify_mentions()
|
||||
|
||||
frappe.publish_realtime('new_communication', self.as_dict(),
|
||||
doctype=self.reference_doctype, docname=self.reference_name,
|
||||
after_commit=True)
|
||||
self.notify_change('add')
|
||||
|
||||
def validate(self):
|
||||
if not self.comment_email:
|
||||
|
|
@ -30,12 +27,30 @@ class Comment(Document):
|
|||
|
||||
def on_update(self):
|
||||
update_comment_in_doc(self)
|
||||
if self.is_new():
|
||||
self.notify_change('update')
|
||||
|
||||
def on_trash(self):
|
||||
self.remove_comment_from_cache()
|
||||
frappe.publish_realtime('delete_communication', self.as_dict(),
|
||||
doctype= self.reference_doctype, docname = self.reference_name,
|
||||
after_commit=True)
|
||||
self.notify_change('delete')
|
||||
|
||||
def notify_change(self, action):
|
||||
key_map = {
|
||||
'Like': 'like_logs',
|
||||
'Assigned': 'assignment_logs',
|
||||
'Assignment Completed': 'assignment_logs',
|
||||
'Comment': 'comments',
|
||||
'Attachment': 'attachment_logs',
|
||||
'Attachment Removed': 'attachment_logs',
|
||||
}
|
||||
key = key_map.get(self.comment_type)
|
||||
if not key: return
|
||||
|
||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
|
||||
'doc': self.as_dict(),
|
||||
'key': key,
|
||||
'action': action
|
||||
}, after_commit=True)
|
||||
|
||||
def remove_comment_from_cache(self):
|
||||
_comments = get_comments_from_parent(self)
|
||||
|
|
@ -150,7 +165,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
|
|||
try:
|
||||
# use sql, so that we do not mess with the timestamp
|
||||
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
|
||||
(json.dumps(_comments[-50:]), reference_name))
|
||||
(json.dumps(_comments[-100:]), reference_name))
|
||||
|
||||
except Exception as e:
|
||||
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
|
||||
|
|
|
|||
|
|
@ -99,8 +99,7 @@ frappe.ui.form.on("Communication", {
|
|||
}
|
||||
},
|
||||
|
||||
show_relink_dialog: function(frm){
|
||||
var lib = "frappe.email";
|
||||
show_relink_dialog: function(frm) {
|
||||
var d = new frappe.ui.Dialog ({
|
||||
title: __("Relink Communication"),
|
||||
fields: [{
|
||||
|
|
@ -138,8 +137,10 @@ frappe.ui.form.on("Communication", {
|
|||
}
|
||||
});
|
||||
},
|
||||
function () {
|
||||
frappe.show_alert('Document not Relinked')
|
||||
function() {
|
||||
frappe.show_alert({
|
||||
message: __('Document not Relinked'), 'indicator': 'info'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,10 +99,7 @@ class Communication(Document):
|
|||
frappe.db.set_value("Communication", self.reference_name, "status", "Replied")
|
||||
|
||||
if self.communication_type == "Communication":
|
||||
# send new comment to listening clients
|
||||
frappe.publish_realtime('new_communication', self.as_dict(),
|
||||
doctype=self.reference_doctype, docname=self.reference_name,
|
||||
after_commit=True)
|
||||
self.notify_change('add')
|
||||
|
||||
elif self.communication_type in ("Chat", "Notification", "Bot"):
|
||||
if self.reference_name == frappe.session.user:
|
||||
|
|
@ -125,10 +122,14 @@ class Communication(Document):
|
|||
|
||||
def on_trash(self):
|
||||
if self.communication_type == "Communication":
|
||||
# send delete comment to listening clients
|
||||
frappe.publish_realtime('delete_communication', self.as_dict(),
|
||||
doctype= self.reference_doctype, docname = self.reference_name,
|
||||
after_commit=True)
|
||||
self.notify_change('delete')
|
||||
|
||||
def notify_change(self, action):
|
||||
frappe.publish_realtime('update_docinfo_for_{}_{}'.format(self.reference_doctype, self.reference_name), {
|
||||
'doc': self.as_dict(),
|
||||
'key': 'communications',
|
||||
'action': action
|
||||
}, after_commit=True)
|
||||
|
||||
def set_status(self):
|
||||
if not self.is_new():
|
||||
|
|
@ -244,9 +245,7 @@ class Communication(Document):
|
|||
|
||||
if delivery_status:
|
||||
self.db_set('delivery_status', delivery_status)
|
||||
|
||||
frappe.publish_realtime('update_communication', self.as_dict(),
|
||||
doctype=self.reference_doctype, docname=self.reference_name, after_commit=True)
|
||||
self.notify_change('update')
|
||||
|
||||
# for list views and forms
|
||||
self.notify_update()
|
||||
|
|
@ -260,10 +259,8 @@ class Communication(Document):
|
|||
# Timeline Links
|
||||
def set_timeline_links(self):
|
||||
contacts = []
|
||||
if (self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")) or \
|
||||
frappe.flags.in_test:
|
||||
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
|
||||
create_contact_enabled = self.email_account and frappe.db.get_value("Email Account", self.email_account, "create_contact")
|
||||
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc], auto_create_contact=create_contact_enabled)
|
||||
|
||||
for contact_name in contacts:
|
||||
self.add_link('Contact', contact_name)
|
||||
|
|
@ -342,7 +339,7 @@ def get_permission_query_conditions_for_communication(user):
|
|||
return """`tabCommunication`.email_account in ({email_accounts})"""\
|
||||
.format(email_accounts=','.join(email_accounts))
|
||||
|
||||
def get_contacts(email_strings):
|
||||
def get_contacts(email_strings, auto_create_contact=False):
|
||||
email_addrs = []
|
||||
|
||||
for email_string in email_strings:
|
||||
|
|
@ -357,7 +354,7 @@ def get_contacts(email_strings):
|
|||
email = get_email_without_link(email)
|
||||
contact_name = get_contact_name(email)
|
||||
|
||||
if not contact_name and email:
|
||||
if not contact_name and email and auto_create_contact:
|
||||
email_parts = email.split("@")
|
||||
first_name = frappe.unscrub(email_parts[0])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2017-01-11 04:21:35.217943",
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
"column_break_2",
|
||||
"permlevel",
|
||||
"section_break_4",
|
||||
"select",
|
||||
"read",
|
||||
"write",
|
||||
"create",
|
||||
|
|
@ -211,9 +213,16 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Reference Document Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "select",
|
||||
"fieldtype": "Check",
|
||||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-31 16:58:16.157079",
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:20:48.296730",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Custom DocPerm",
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
|
||||
Test 26 ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
|
||||
Title,Description,Number,another_number,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
|
||||
Test 26,test description,1,2,"",child title,child description,child title,14-08-2019,4,child title again,22-09-2020,5,7
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 56.
|
|
|
@ -616,7 +616,9 @@ class Row:
|
|||
id_field = get_id_field(doctype)
|
||||
id_value = doc.get(id_field.fieldname)
|
||||
if id_value and frappe.db.exists(doctype, id_value):
|
||||
doc = frappe.get_doc(doctype, id_value)
|
||||
existing_doc = frappe.get_doc(doctype, id_value)
|
||||
existing_doc.update(doc)
|
||||
doc = existing_doc
|
||||
else:
|
||||
# for table rows being inserted in update
|
||||
# create a new doc with defaults set
|
||||
|
|
@ -749,7 +751,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
|
||||
"message": _("{0} is a mandatory field").format(id_field.label),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ from __future__ import unicode_literals
|
|||
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.core.doctype.data_import.importer import Importer
|
||||
from frappe.utils import getdate, format_duration
|
||||
|
||||
doctype_name = 'DocType for Import'
|
||||
|
||||
class TestImporter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_doctype_if_not_exists(doctype_name)
|
||||
|
||||
def test_data_import_from_file(self):
|
||||
|
|
@ -71,19 +73,28 @@ class TestImporter(unittest.TestCase):
|
|||
self.assertEqual(warnings[2]['message'], "<b>Title</b> is a mandatory field")
|
||||
|
||||
def test_data_import_update(self):
|
||||
if not frappe.db.exists(doctype_name, 'Test 26'):
|
||||
frappe.get_doc(
|
||||
doctype=doctype_name,
|
||||
title='Test 26'
|
||||
).insert()
|
||||
existing_doc = frappe.get_doc(
|
||||
doctype=doctype_name,
|
||||
title=frappe.generate_hash(doctype_name, 8),
|
||||
table_field_1=[{'child_title': 'child title to update'}]
|
||||
)
|
||||
existing_doc.save()
|
||||
frappe.db.commit()
|
||||
|
||||
import_file = get_import_file('sample_import_file_for_update')
|
||||
data_import = self.get_importer(doctype_name, import_file, update=True)
|
||||
data_import.start_import()
|
||||
i = Importer(data_import.reference_doctype, data_import=data_import)
|
||||
|
||||
updated_doc = frappe.get_doc(doctype_name, 'Test 26')
|
||||
# update child table id in template date
|
||||
i.import_file.raw_data[1][4] = existing_doc.table_field_1[0].name
|
||||
i.import_file.raw_data[1][0] = existing_doc.name
|
||||
i.import_file.parse_data_from_template()
|
||||
i.import_data()
|
||||
|
||||
updated_doc = frappe.get_doc(doctype_name, existing_doc.name)
|
||||
self.assertEqual(updated_doc.description, 'test description')
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_title, 'child title')
|
||||
self.assertEqual(updated_doc.table_field_1[0].name, existing_doc.table_field_1[0].name)
|
||||
self.assertEqual(updated_doc.table_field_1[0].child_description, 'child description')
|
||||
self.assertEqual(updated_doc.table_field_1_again[0].child_title, 'child title again')
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ frappe.ui.form.on('Data Import Legacy', {
|
|||
frm.reload_doc();
|
||||
}
|
||||
if (data.progress) {
|
||||
let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar");
|
||||
let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar");
|
||||
if (progress_bar) {
|
||||
$(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped");
|
||||
$(progress_bar).css("width", data.progress + "%");
|
||||
|
|
|
|||
|
|
@ -9,15 +9,16 @@ frappe.listview_settings["Deleted Document"] = {
|
|||
args: { docnames },
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
function body(docnames) {
|
||||
let body = (docnames) => {
|
||||
const html = docnames.map(docname => {
|
||||
return `<li><a href='/desk#Form/Deleted Document/${docname}'>${docname}</a></li>`;
|
||||
return `<li><a href='/app/deleted-document/${docname}'>${docname}</a></li>`;
|
||||
});
|
||||
return "<br><ul>" + html.join("");
|
||||
}
|
||||
function message(title, docnames) {
|
||||
};
|
||||
|
||||
let message = (title, docnames) => {
|
||||
return (docnames.length > 0) ? title + body(docnames) + "</ul>": "";
|
||||
}
|
||||
};
|
||||
|
||||
const { restored, invalid, failed } = r.message;
|
||||
const restored_summary = message(__("Documents restored successfully"), restored);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"fieldname",
|
||||
"precision",
|
||||
"length",
|
||||
"non_negative",
|
||||
"hide_days",
|
||||
"hide_seconds",
|
||||
"reqd",
|
||||
|
|
@ -473,13 +474,20 @@
|
|||
"fieldname": "hide_border",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Border"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
|
||||
"fieldname": "non_negative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Negative"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-28 11:28:21.252853",
|
||||
"modified": "2020-10-29 06:09:26.454990",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -1,775 +1,229 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:27:33",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"role_and_level",
|
||||
"role",
|
||||
"if_owner",
|
||||
"column_break_2",
|
||||
"permlevel",
|
||||
"section_break_4",
|
||||
"select",
|
||||
"read",
|
||||
"write",
|
||||
"create",
|
||||
"delete",
|
||||
"column_break_8",
|
||||
"submit",
|
||||
"cancel",
|
||||
"amend",
|
||||
"additional_permissions",
|
||||
"report",
|
||||
"export",
|
||||
"import",
|
||||
"set_user_permissions",
|
||||
"column_break_19",
|
||||
"share",
|
||||
"print",
|
||||
"email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "role_and_level",
|
||||
"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": "Role and Level",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Role and Level"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "role",
|
||||
"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": "Role",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "role",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Role",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "150px",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "Apply this rule if the User is the Owner",
|
||||
"fieldname": "if_owner",
|
||||
"fieldtype": "Check",
|
||||
"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": "If user is the owner",
|
||||
"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": "If user is the owner"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"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,
|
||||
"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,
|
||||
"default": "0",
|
||||
"fieldname": "permlevel",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Level",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "permlevel",
|
||||
"oldfieldtype": "Int",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "40px",
|
||||
"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": "40px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_4",
|
||||
"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": "Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "read",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Read",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "read",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "write",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Write",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "write",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "create",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Create",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "create",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "delete",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Delete",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Delete"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_8",
|
||||
"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,
|
||||
"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,
|
||||
"default": "0",
|
||||
"fieldname": "submit",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Submit",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "submit",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "cancel",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Cancel",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "cancel",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "amend",
|
||||
"fieldtype": "Check",
|
||||
"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": "Amend",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "amend",
|
||||
"oldfieldtype": "Check",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "additional_permissions",
|
||||
"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": "Additional Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Additional Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "report",
|
||||
"fieldtype": "Check",
|
||||
"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": "Report",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "32px",
|
||||
"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": "32px"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "export",
|
||||
"fieldtype": "Check",
|
||||
"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": "Export",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Export"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "import",
|
||||
"fieldtype": "Check",
|
||||
"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": "Import",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Import"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "This role update User Permissions for a user",
|
||||
"fieldname": "set_user_permissions",
|
||||
"fieldtype": "Check",
|
||||
"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": "Set User Permissions",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Set User Permissions"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_19",
|
||||
"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,
|
||||
"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,
|
||||
"default": "1",
|
||||
"fieldname": "share",
|
||||
"fieldtype": "Check",
|
||||
"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": "Share",
|
||||
"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": "Share"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "print",
|
||||
"fieldtype": "Check",
|
||||
"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": "Print",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Print"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Check",
|
||||
"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": "Email",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"label": "Email"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "select",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Select"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-29 11:54:38.613936",
|
||||
"links": [],
|
||||
"modified": "2020-12-03 15:15:30.488212",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocPerm",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
|
|
@ -24,11 +24,11 @@ frappe.ui.form.on('DocType', {
|
|||
if (!frm.is_new() && !frm.doc.istable) {
|
||||
if (frm.doc.issingle) {
|
||||
frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => {
|
||||
frappe.set_route('Form', frm.doc.name);
|
||||
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
|
||||
});
|
||||
} else {
|
||||
frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => {
|
||||
frappe.set_route('List', frm.doc.name, 'List');
|
||||
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue