diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 4754a63e7e..0000000000 --- a/.codacy.yml +++ /dev/null @@ -1,2 +0,0 @@ -exclude_paths: - - '**.sql' \ No newline at end of file diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index f5edb47a13..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -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" diff --git a/.eslintignore b/.eslintignore index 2fd65d307d..baf9bb2cc5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 \ No newline at end of file +frappe/www/website_script.js diff --git a/.eslintrc b/.eslintrc index a2538feab5..d123023a68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,6 +147,7 @@ "context": true, "before": true, "beforeEach": true, - "qz": true + "qz": true, + "localforage": true } } diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..399b176e1d --- /dev/null +++ b/.flake8 @@ -0,0 +1,32 @@ +[flake8] +ignore = + E121, + E126, + E127, + E128, + E203, + E225, + E226, + E231, + E241, + E251, + E261, + E265, + E302, + E303, + E305, + E402, + E501, + E741, + W291, + W292, + W293, + W391, + W503, + W504, + F403, + B007, + B950, + W191, + +max-line-length = 200 \ No newline at end of file diff --git a/.travis/consumer_db/mariadb.json b/.github/helper/consumer_db/mariadb.json similarity index 89% rename from .travis/consumer_db/mariadb.json rename to .github/helper/consumer_db/mariadb.json index fb5b3bc976..2e32157e1a 100644 --- a/.travis/consumer_db/mariadb.json +++ b/.github/helper/consumer_db/mariadb.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 3306, "db_name": "test_frappe_consumer", "db_password": "test_frappe", "allow_tests": true, diff --git a/.travis/consumer_db/postgres.json b/.github/helper/consumer_db/postgres.json similarity index 90% rename from .travis/consumer_db/postgres.json rename to .github/helper/consumer_db/postgres.json index fed9fdfde2..9532670029 100644 --- a/.travis/consumer_db/postgres.json +++ b/.github/helper/consumer_db/postgres.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 5432, "db_name": "test_frappe_consumer", "db_password": "test_frappe", "db_type": "postgres", diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 0000000000..f6f0cad31a --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +cd ~ || exit + +pip install frappe-bench + +bench init frappe-bench --skip-assets --python "$(which python)" --frappe-path "${GITHUB_WORKSPACE}" + +mkdir ~/frappe-bench/sites/test_site +cp "${GITHUB_WORKSPACE}/.github/helper/consumer_db/$DB.json" ~/frappe-bench/sites/test_site/site_config.json + +if [ "$TYPE" == "server" ]; then + mkdir ~/frappe-bench/sites/test_site_producer; + cp "${GITHUB_WORKSPACE}/.github/helper/producer_db/$DB.json" ~/frappe-bench/sites/test_site_producer/site_config.json; +fi + +if [ "$DB" == "mariadb" ];then + mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_consumer"; + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe_producer"; + mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'"; + + mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; + mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"; + fi + +if [ "$DB" == "postgres" ];then + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_consumer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres; + + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe_producer" -U postgres; + echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres; +fi + +cd ./frappe-bench || exit + +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" == "ui" ]; then bench setup requirements --node; fi + +# install node-sass which is required for website theme test +cd ./apps/frappe || exit +yarn add node-sass@4.13.1 +cd ../.. + +bench start & +bench --site test_site reinstall --yes +if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi +bench build --app frappe \ No newline at end of file diff --git a/.github/helper/install_dependencies.sh b/.github/helper/install_dependencies.sh new file mode 100644 index 0000000000..9be8519d85 --- /dev/null +++ b/.github/helper/install_dependencies.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +# python "${GITHUB_WORKSPACE}/.github/helper/roulette.py" +# if [[ $? != 2 ]];then +# exit; +# fi + + # install wkhtmltopdf +wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz +tar -xf /tmp/wkhtmltox.tar.xz -C /tmp +sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf +sudo chmod o+x /usr/local/bin/wkhtmltopdf + +# install cups +sudo apt-get install libcups2-dev + +# install redis +sudo apt-get install redis-server + diff --git a/.travis/producer_db/mariadb.json b/.github/helper/producer_db/mariadb.json similarity index 89% rename from .travis/producer_db/mariadb.json rename to .github/helper/producer_db/mariadb.json index 988282a554..c1db0d765f 100644 --- a/.travis/producer_db/mariadb.json +++ b/.github/helper/producer_db/mariadb.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 3306, "db_name": "test_frappe_producer", "db_password": "test_frappe", "allow_tests": true, diff --git a/.travis/producer_db/postgres.json b/.github/helper/producer_db/postgres.json similarity index 89% rename from .travis/producer_db/postgres.json rename to .github/helper/producer_db/postgres.json index 6426e99058..8b9d2a20fd 100644 --- a/.travis/producer_db/postgres.json +++ b/.github/helper/producer_db/postgres.json @@ -1,5 +1,6 @@ { - "db_host": "localhost", + "db_host": "127.0.0.1", + "db_port": 5432, "db_name": "test_frappe_producer", "db_password": "test_frappe", "db_type": "postgres", diff --git a/.travis/roulette.py b/.github/helper/roulette.py similarity index 93% rename from .travis/roulette.py rename to .github/helper/roulette.py index 4d83137199..ba775d6794 100644 --- a/.travis/roulette.py +++ b/.github/helper/roulette.py @@ -24,10 +24,12 @@ def is_docs(file): if __name__ == "__main__": build_type = os.environ.get("TYPE") - commit_range = os.environ.get("TRAVIS_COMMIT_RANGE") + before = os.environ.get("BEFORE") + after = os.environ.get("AFTER") + commit_range = before + '...' + after print("Build Type: {}".format(build_type)) print("Commit Range: {}".format(commit_range)) - + try: files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False) except Exception: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 0000000000..665e7b6c10 --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,148 @@ +name: CI + +on: [pull_request, workflow_dispatch, push] + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + matrix: + include: + - DB: "mariadb" + TYPE: "server" + JOB_NAME: "Python MariaDB" + RUN_COMMAND: bench --site test_site run-tests --coverage + + - DB: "postgres" + TYPE: "server" + JOB_NAME: "Python PostgreSQL" + RUN_COMMAND: bench --site test_site run-tests --coverage + + - DB: "mariadb" + TYPE: "ui" + JOB_NAME: "UI MariaDB" + RUN_COMMAND: bench --site test_site run-ui-tests frappe --headless + + name: ${{ matrix.JOB_NAME }} + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + postgres: + image: postgres:12.4 + env: + POSTGRES_PASSWORD: travis + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - uses: actions/setup-node@v2 + with: + node-version: '12' + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install Dependencies + run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh + env: + BEFORE: ${{ env.GITHUB_EVENT_PATH.before }} + AFTER: ${{ env.GITHUB_EVENT_PATH.after }} + TYPE: ${{ matrix.TYPE }} + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Run Set-Up + if: matrix.TYPE == 'ui' + run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Run Tests + run: cd ~/frappe-bench/ && ${{ matrix.RUN_COMMAND }} + env: + DB: ${{ matrix.DB }} + TYPE: ${{ matrix.TYPE }} + + - name: Coverage + if: matrix.TYPE == 'server' + run: | + cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} + cd ${GITHUB_WORKSPACE} + pip install coveralls==2.2.0 + pip install coverage==4.5.4 + coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000000..321dfb567b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,22 @@ +name: Semgrep + +on: + pull_request: + branches: + - develop +jobs: + semgrep: + name: Frappe Linter + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup python3 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Run semgrep + run: | + python -m pip install -q semgrep + git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) + if [ -f .semgrep.yml ]; then semgrep --config=.semgrep.yml --quiet --error $files; fi diff --git a/.mergify.yml b/.mergify.yml index 5b0ec71b1c..82f710a5a8 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -3,7 +3,9 @@ pull_request_rules: conditions: - status-success=Sider - status-success=Semantic Pull Request - - status-success=Travis CI - Pull Request + - status-success=Python MariaDB + - status-success=Python PostgreSQL + - status-success=UI MariaDB - status-success=security/snyk (frappe) - label!=dont-merge - label!=squash @@ -14,8 +16,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=Python MariaDB + - status-success=Python PostgreSQL + - status-success=UI MariaDB - status-success=security/snyk (frappe) - label!=dont-merge - label=squash diff --git a/.semgrep.yml b/.semgrep.yml new file mode 100644 index 0000000000..99d237251e --- /dev/null +++ b/.semgrep.yml @@ -0,0 +1,29 @@ +#Reference: https://semgrep.dev/docs/writing-rules/rule-syntax/ + +rules: +- id: eval + patterns: + - pattern-not: eval("...") + - pattern: eval(...) + message: | + Detected the use of eval(). eval() can be dangerous if used to evaluate + dynamic content. Avoid it or use safe_eval(). + languages: + - python + severity: ERROR + +# translations +- id: frappe-translation-syntax-python + pattern-either: + - pattern: _(f"...") # f-strings not allowed + - pattern: _("..." + "...") # concatenation not allowed + - pattern: _("") # empty string is meaningless + - pattern: _("..." % ...) # Only positional formatters are allowed. + - pattern: _("...".format(...)) # format should not be used before translating + - pattern: _("...") + ... + _("...") # don't split strings + message: | + Incorrect use of translation function detected. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: + - python + severity: ERROR diff --git a/frappe/.stylelintrc b/.stylelintrc similarity index 100% rename from frappe/.stylelintrc rename to .stylelintrc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 53ad56a948..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,129 +0,0 @@ -language: python -dist: bionic - -addons: - hosts: - - test_site - - test_site_producer - mariadb: 10.3 - postgresql: 9.5 - chrome: stable - -services: - - xvfb - - mysql - -git: - depth: 1 - -cache: - pip: true - npm: true - yarn: true - directories: - # we also need to cache folder with Cypress binary - # https://docs.cypress.io/guides/guides/continuous-integration.html#Caching - - ~/.cache - - -matrix: - include: - - name: "Python 3.7 MariaDB" - python: 3.7 - env: DB=mariadb TYPE=server - script: bench --verbose --site test_site run-tests --coverage - - - name: "Python 3.7 PostgreSQL" - python: 3.7 - env: DB=postgres TYPE=server - script: bench --verbose --site test_site run-tests --coverage - - - name: "Cypress" - python: 3.7 - env: DB=mariadb TYPE=ui - before_script: - - bench --site test_site execute frappe.utils.install.complete_setup_wizard - script: bench --site test_site run-ui-tests frappe --headless - -before_install: - # do we really want to run travis? - - | - python ./.travis/roulette.py - if [[ $? != 2 ]];then - exit; - fi - - # install wkhtmltopdf - - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz - - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp - - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - - sudo chmod o+x /usr/local/bin/wkhtmltopdf - - # install cups - - sudo apt-get install libcups2-dev - -install: - - cd ~ - - source ./.nvm/nvm.sh - - nvm install 12 - - - pip install frappe-bench - - - bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR - - - mkdir ~/frappe-bench/sites/test_site - - cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/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'"; - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"; - - mysql -u root -e "CREATE DATABASE test_frappe_consumer"; - mysql -u root -e "CREATE USER 'test_frappe_consumer'@'localhost' IDENTIFIED BY 'test_frappe_consumer'"; - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_consumer\`.* TO 'test_frappe_consumer'@'localhost'"; - - mysql -u root -e "CREATE DATABASE test_frappe_producer"; - mysql -u root -e "CREATE USER 'test_frappe_producer'@'localhost' IDENTIFIED BY 'test_frappe_producer'"; - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe_producer\`.* TO 'test_frappe_producer'@'localhost'"; - - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"; - mysql -u root -e "FLUSH PRIVILEGES"; - fi - - - if [ $DB == "postgres" ];then - psql -c "CREATE DATABASE test_frappe_consumer" -U postgres; - psql -c "CREATE USER test_frappe_consumer WITH PASSWORD 'test_frappe'" -U postgres; - - psql -c "CREATE DATABASE test_frappe_producer" -U postgres; - psql -c "CREATE USER test_frappe_producer WITH PASSWORD 'test_frappe'" -U postgres; - fi - - - cd ./frappe-bench - - - 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 == "ui" ]; then bench setup requirements --node; fi - - # install node-sass which is required for website theme test - - cd ./apps/frappe - - yarn add node-sass@4.13.1 - - cd ../.. - - - bench start & - - bench --site test_site reinstall --yes - - if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi - - bench build --app frappe - -after_script: - - pip install coverage==4.5.4 - - pip install python-coveralls - - coveralls -b apps/frappe -d ../../sites/.coverage diff --git a/CODEOWNERS b/CODEOWNERS index 1afa3f72e3..92723ab035 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -8,10 +8,10 @@ website/ @prssanna web_form/ @prssanna templates/ @surajshetty3416 www/ @surajshetty3416 -integrations/ @nextchamp-saqib +integrations/ @leela patches/ @surajshetty3416 dashboard/ @prssanna -email/ @saurabh6790 +email/ @leela event_streaming/ @ruchamahabal data_import* @netchampfaris core/ @surajshetty3416 diff --git a/README.md b/README.md index f99988ae79..b00d291b96 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@
Custom Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started
\n\n\n// fetch local_tax_no on selection of customer \n// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname); \ncur_frm.add_fetch('customer', 'local_tax_no', 'local_tax_no');\n\n// additional validation on dates \nfrappe.ui.form.on('Task', 'validate', function(frm) {\n if (frm.doc.from_date < get_today()) {\n msgprint('You can not select past date in From Date');\n validated = false;\n } \n});\n\n// make a field read-only after saving \nfrappe.ui.form.on('Task', {\n refresh: function(frm) {\n // use the __islocal value of doc, to check if the doc is saved or not\n frm.set_df_property('myfield', 'read_only', frm.doc.__islocal ? 0 : 1);\n } \n});\n\n// additional permission check\nfrappe.ui.form.on('Task', {\n validate: function(frm) {\n if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {\n msgprint('You are only allowed Material Receipt');\n validated = false;\n }\n } \n});\n\n// calculate sales incentive\nfrappe.ui.form.on('Sales Invoice', {\n validate: function(frm) {\n // calculate incentives for each person on the deal\n total_incentive = 0\n $.each(frm.doc.sales_team, function(i, d) {\n // calculate incentive\n var incentive_percent = 2;\n if(frm.doc.base_grand_total > 400) incentive_percent = 4;\n // actual incentive\n d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;\n total_incentive += flt(d.incentives)\n });\n frm.doc.total_incentive = total_incentive;\n } \n})\n\n"
},
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
- "label": "Enabled",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Enabled"
+ },
+ {
+ "default": "Form",
+ "fieldname": "view",
+ "fieldtype": "Select",
+ "label": "Apply To",
+ "options": "List\nForm",
+ "set_only_once": 1
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-24 21:56:07.719579",
+ "modified": "2021-03-16 20:33:51.400191",
"modified_by": "Administrator",
"module": "Custom",
- "name": "Custom Script",
+ "name": "Client Script",
"owner": "Administrator",
"permissions": [
{
@@ -86,6 +88,7 @@
"write": 1
}
],
+ "sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/client_script/client_script.py b/frappe/custom/doctype/client_script/client_script.py
new file mode 100644
index 0000000000..049f979263
--- /dev/null
+++ b/frappe/custom/doctype/client_script/client_script.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+import frappe
+
+from frappe import _
+from frappe.model.document import Document
+
+
+class ClientScript(Document):
+ def autoname(self):
+ self.name = f"{self.dt}-{self.view}"
+
+ def validate(self):
+ if not self.is_new():
+ return
+
+ exists = frappe.db.exists(
+ "Client Script", {"dt": self.dt, "view": self.view}
+ )
+ if exists:
+ frappe.throw(
+ _("Client Script for {0} {1} already exists").format(frappe.bold(self.dt), self.view),
+ frappe.DuplicateEntryError,
+ )
+
+ def on_update(self):
+ frappe.clear_cache(doctype=self.dt)
+
+ def on_trash(self):
+ frappe.clear_cache(doctype=self.dt)
diff --git a/frappe/custom/doctype/custom_script/test_custom_script.py b/frappe/custom/doctype/client_script/test_client_script.py
similarity index 65%
rename from frappe/custom/doctype/custom_script/test_custom_script.py
rename to frappe/custom/doctype/client_script/test_client_script.py
index 6947e6060d..de113c1ce7 100644
--- a/frappe/custom/doctype/custom_script/test_custom_script.py
+++ b/frappe/custom/doctype/client_script/test_client_script.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
import unittest
-# test_records = frappe.get_test_records('Custom Script')
+# test_records = frappe.get_test_records('Client Script')
-class TestCustomScript(unittest.TestCase):
+class TestClientScript(unittest.TestCase):
pass
diff --git a/frappe/custom/doctype/custom_script/custom_script.js b/frappe/custom/doctype/custom_script/custom_script.js
deleted file mode 100644
index 711e7d1796..0000000000
--- a/frappe/custom/doctype/custom_script/custom_script.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (c) 2016, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Custom Script', {
- refresh(frm) {
- if (frm.doc.dt && frm.doc.script) {
- frm.add_custom_button(__('Go to {0}', [frm.doc.dt]),
- () => frappe.set_route('List', frm.doc.dt, 'List'));
- }
-
- frm.add_custom_button(__('Add script for Child Table'), () => {
- frappe.model.with_doctype(frm.doc.dt, () => {
- const child_tables = frappe.meta.get_docfields(frm.doc.dt, null, {
- fieldtype: 'Table'
- }).map(df => df.options);
-
- const d = new frappe.ui.Dialog({
- title: __('Select Child Table'),
- fields: [
- {
- label: __('Select Child Table'),
- fieldtype: 'Link',
- fieldname: 'cdt',
- options: 'DocType',
- get_query: () => {
- return {
- filters: {
- istable: 1,
- name: ['in', child_tables]
- }
- };
- }
- }
- ],
- primary_action: ({ cdt }) => {
- cdt = d.get_field('cdt').value;
- frm.events.add_script_for_doctype(frm, cdt);
- d.hide();
- }
- });
-
- d.show();
- });
- });
-
- frm.set_query('dt', {
- filters: {
- istable: 0
- }
- });
- },
-
- dt(frm) {
- if (!frm.doc.script) {
- frm.events.add_script_for_doctype(frm, frm.doc.dt);
- }
-
- if (frm.doc.script && !frm.doc.script.includes(frm.doc.dt)) {
- frm.doc.script = '';
- frm.events.add_script_for_doctype(frm, frm.doc.dt);
- }
- },
-
- add_script_for_doctype(frm, doctype) {
- let boilerplate = `
-frappe.ui.form.on('${doctype}', {
- refresh(frm) {
- // your code here
- }
-})
- `.trim();
- let script = (frm.doc.script || '');
- if (script) {
- script += '\n\n';
- }
- frm.set_value('script', script + boilerplate);
- }
-});
diff --git a/frappe/custom/doctype/custom_script/custom_script.py b/frappe/custom/doctype/custom_script/custom_script.py
deleted file mode 100644
index e15819de65..0000000000
--- a/frappe/custom/doctype/custom_script/custom_script.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-from __future__ import unicode_literals
-import frappe
-
-from frappe.model.document import Document
-
-class CustomScript(Document):
- def autoname(self):
- self.name = self.dt + "-Client"
-
- def on_update(self):
- frappe.clear_cache(doctype=self.dt)
-
- def on_trash(self):
- frappe.clear_cache(doctype=self.dt)
-
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 79978a49d7..d9d8ae196e 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -76,6 +76,7 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_sortable");
}
}
+ localStorage["customize_doctype"] = frm.doc.doc_type;
}
});
} else {
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
index 3631914249..cdc3b73366 100644
--- a/frappe/custom/workspace/customization/customization.json
+++ b/frappe/custom/workspace/customization/customization.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "customization",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Customization",
"links": [
@@ -81,8 +82,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Custom Script",
- "link_to": "Custom Script",
+ "label": "Client Script",
+ "link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -115,7 +116,7 @@
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:39.843773",
+ "modified": "2021-02-04 13:50:35.750463",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
@@ -134,8 +135,14 @@
"type": "DocType"
},
{
- "label": "Custom Script",
- "link_to": "Custom Script",
+ "label": "Client Script",
+ "link_to": "Client Script",
+ "type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "label": "Server Script",
+ "link_to": "Server Script",
"type": "DocType"
}
]
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 179206a4af..4fcf10efda 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -16,7 +16,6 @@ import frappe.model.meta
from frappe import _
from time import time
from frappe.utils import now, getdate, cast_fieldtype, get_datetime
-from frappe.utils.background_jobs import execute_job, get_queue
from frappe.model.utils.link_count import flush_local_link_count
from frappe.utils import cint
@@ -1032,6 +1031,8 @@ class Database(object):
insert_list = []
def enqueue_jobs_after_commit():
+ from frappe.utils.background_jobs import execute_job, get_queue
+
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
for job in frappe.flags.enqueue_after_commit:
q = get_queue(job.get("queue"), is_async=job.get("is_async"))
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 3cbb2e4f0e..f9997d1526 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -8,8 +8,7 @@ from pymysql.times import TimeDelta
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions
-from frappe.utils import get_datetime, cstr
-from markdown2 import UnicodeWithAttrs
+from frappe.utils import get_datetime, cstr, UnicodeWithAttrs
from frappe.database.database import Database
from six import PY2, binary_type, text_type, string_types
from frappe.database.mariadb.schema import MariaDBTable
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 0ded8e0717..5b6e2fdd21 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -601,8 +601,8 @@ def merge_cards_based_on_label(cards):
for card in cards:
label = card.get('label')
if label in cards_dict:
- links = loads(cards_dict[label].links) + loads(card.links)
- cards_dict[label].update(dict(links=dumps(links)))
+ links = cards_dict[label].links + card.links
+ cards_dict[label].update(dict(links=links))
cards_dict[label] = cards_dict.pop(label)
else:
cards_dict[label] = card
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index b19f6cf9f0..48b34e6cd9 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -171,7 +171,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
doctype = chart.document_type
datefield = chart.based_on
- aggregate_function = get_aggregate_function(chart.chart_type)
value_field = chart.value_based_on or '1'
from_date = from_date.strftime('%Y-%m-%d')
to_date = to_date
@@ -183,7 +182,8 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
doctype,
fields = [
'{} as _unit'.format(datefield),
- '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
+ 'SUM({})'.format(value_field),
+ 'COUNT(*)'
],
filters = filters,
group_by = '_unit',
@@ -192,7 +192,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
ignore_ifnull = True
)
- result = get_result(data, timegrain, from_date, to_date)
+ result = get_result(data, timegrain, from_date, to_date, chart.chart_type)
chart_config = {
"labels": [get_period(r[0], timegrain) for r in result],
@@ -288,15 +288,21 @@ def get_aggregate_function(chart_type):
}[chart_type]
-def get_result(data, timegrain, from_date, to_date):
+def get_result(data, timegrain, from_date, to_date, chart_type):
dates = get_dates_from_timegrain(from_date, to_date, timegrain)
result = [[date, 0] for date in dates]
data_index = 0
if data:
for i, d in enumerate(result):
+ count = 0
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
d[1] += data[data_index][1]
+ count += data[data_index][2]
data_index += 1
+ if chart_type == 'Average' and not count == 0:
+ d[1] = d[1]/count
+ if chart_type == 'Count':
+ d[1] = count
return result
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 3c37ad4a09..72ab18385d 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -212,19 +212,52 @@ class TestDashboardChart(unittest.TestCase):
frappe.db.rollback()
-def insert_test_records():
- create_new_communication(datetime(2018, 12, 30), 50)
- create_new_communication(datetime(2019, 1, 4), 100)
- create_new_communication(datetime(2019, 1, 6), 200)
- create_new_communication(datetime(2019, 1, 7), 400)
- create_new_communication(datetime(2019, 1, 8), 300)
- create_new_communication(datetime(2019, 1, 10), 100)
+ def test_avg_dashboard_chart(self):
+ insert_test_records()
-def create_new_communication(date, rating):
+ if frappe.db.exists('Dashboard Chart', 'Test Average Dashboard Chart'):
+ frappe.delete_doc('Dashboard Chart', 'Test Average Dashboard Chart')
+
+ frappe.get_doc(dict(
+ doctype = 'Dashboard Chart',
+ chart_name = 'Test Average Dashboard Chart',
+ chart_type = 'Average',
+ document_type = 'Communication',
+ based_on = 'communication_date',
+ value_based_on = 'rating',
+ timespan = 'Select Date Range',
+ time_interval = 'Weekly',
+ from_date = datetime(2018, 12, 30),
+ to_date = datetime(2019, 1, 15),
+ filters_json = '[]',
+ timeseries = 1
+ )).insert()
+
+ result = get(chart_name='Test Average Dashboard Chart', refresh = 1)
+
+ self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0])
+ self.assertEqual(
+ result.get('labels'),
+ ['30-12-18', '06-01-19', '13-01-19', '20-01-19']
+ )
+
+ frappe.db.rollback()
+
+def insert_test_records():
+ create_new_communication('Communication 1', datetime(2018, 12, 30), 50)
+ create_new_communication('Communication 2', datetime(2019, 1, 4), 100)
+ create_new_communication('Communication 3', datetime(2019, 1, 6), 200)
+ create_new_communication('Communication 4', datetime(2019, 1, 7), 400)
+ create_new_communication('Communication 5', datetime(2019, 1, 8), 300)
+ create_new_communication('Communication 6', datetime(2019, 1, 10), 100)
+
+def create_new_communication(subject, date, rating):
communication = {
'doctype': 'Communication',
- 'subject': 'Test Communication',
+ 'subject': subject,
'rating': rating,
'communication_date': date
}
- frappe.get_doc(communication).insert()
+ comm = frappe.get_doc(communication)
+ if not frappe.db.exists("Communication", {'subject' : comm.subject}):
+ comm.insert()
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index f1ad41db6c..a655e9e1da 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -17,6 +17,10 @@ class KanbanBoard(Document):
def on_update(self):
frappe.clear_cache(doctype=self.reference_doctype)
+ def before_insert(self):
+ for column in self.columns:
+ column.order = get_order_for_column(self, column.column_name)
+
def validate_column_name(self):
for column in self.columns:
if not column.column_name:
@@ -125,6 +129,53 @@ def update_order(board_name, order):
board.save()
return board, updated_cards
+@frappe.whitelist()
+def update_order_for_single_card(board_name, docname, from_colname, to_colname, old_index, new_index):
+ '''Save the order of cards in columns'''
+ board = frappe.get_doc('Kanban Board', board_name)
+ doctype = board.reference_doctype
+ fieldname = board.field_name
+ old_index = frappe.parse_json(old_index)
+ new_index = frappe.parse_json(new_index)
+
+ # save current order and index of columns to be updated
+ from_col_order, from_col_idx = get_kanban_column_order_and_index(board, from_colname)
+ to_col_order, to_col_idx = get_kanban_column_order_and_index(board, to_colname)
+
+ if from_colname == to_colname:
+ from_col_order = to_col_order
+
+ to_col_order.insert(new_index, from_col_order.pop((old_index)))
+
+ # save updated order
+ board.columns[from_col_idx].order = frappe.as_json(from_col_order)
+ board.columns[to_col_idx].order = frappe.as_json(to_col_order)
+ board.save()
+
+ # update changed value in doc
+ frappe.set_value(doctype, docname, fieldname, to_colname)
+
+ return board
+
+def get_kanban_column_order_and_index(board, colname):
+ for i, col in enumerate(board.columns):
+ if col.column_name == colname:
+ col_order = frappe.parse_json(col.order)
+ col_idx = i
+
+ return col_order, col_idx
+
+@frappe.whitelist()
+def add_card(board_name, docname, colname):
+ board = frappe.get_doc('Kanban Board', board_name)
+
+ col_order, col_idx = get_kanban_column_order_and_index(board, colname)
+ col_order.insert(0, docname)
+
+ board.columns[col_idx].order = frappe.as_json(col_order)
+
+ board.save()
+ return board
@frappe.whitelist()
def quick_kanban_board(doctype, board_name, field_name, project=None):
@@ -133,6 +184,13 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
doc = frappe.new_doc('Kanban Board')
meta = frappe.get_meta(doctype)
+ doc.kanban_board_name = board_name
+ doc.reference_doctype = doctype
+ doc.field_name = field_name
+
+ if project:
+ doc.filters = '[["Task","project","=","{0}"]]'.format(project)
+
options = ''
for field in meta.fields:
if field.fieldname == field_name:
@@ -149,12 +207,6 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
column_name=column
))
- doc.kanban_board_name = board_name
- doc.reference_doctype = doctype
- doc.field_name = field_name
-
- if project:
- doc.filters = '[["Task","project","=","{0}"]]'.format(project)
if doctype in ['Note', 'ToDo']:
doc.private = 1
@@ -162,6 +214,12 @@ def quick_kanban_board(doctype, board_name, field_name, project=None):
doc.save()
return doc
+def get_order_for_column(board, colname):
+ filters = [[board.reference_doctype, board.field_name, '=', colname]]
+ if board.filters:
+ filters.append(frappe.parse_json(board.filters)[0])
+
+ return frappe.as_json(frappe.get_list(board.reference_doctype, filters=filters, pluck='name'))
@frappe.whitelist()
def update_column_order(board_name, order):
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index 9b124cd6f4..4ab40bffe9 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -42,7 +42,11 @@ def create_notification_settings(user):
_doc = frappe.new_doc('Notification Settings')
_doc.name = user
_doc.insert(ignore_permissions=True)
- frappe.db.commit()
+
+
+def toggle_notifications(user, enable=False):
+ if frappe.db.exists("Notification Settings", user):
+ frappe.db.set_value("Notification Settings", user, 'enabled', enable)
@frappe.whitelist()
@@ -76,4 +80,4 @@ def get_permission_query_conditions(user):
@frappe.whitelist()
def set_seen_value(value, user):
- frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
\ No newline at end of file
+ frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 6bddd09fc7..7d1a697f6b 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -86,7 +86,7 @@ def get_result(doc, filters, to_date=None):
filters = frappe.parse_json(filters)
if not filters:
- filters = []
+ filters = []
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date])
@@ -107,9 +107,13 @@ def get_percentage_difference(doc, filters, result):
return
previous_result = calculate_previous_result(doc, filters)
- difference = (result - previous_result)/100.0
-
- return difference
+ if previous_result == 0:
+ return None
+ else:
+ if result == previous_result:
+ return 0
+ else:
+ return ((result/previous_result)-1)*100.0
def calculate_previous_result(doc, filters):
@@ -197,4 +201,4 @@ def add_card_to_dashboard(args):
card.save()
dashboard.append('cards', dashboard_link)
- dashboard.save()
\ No newline at end of file
+ dashboard.save()
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index f5ace4d732..f5e5c0ca9b 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -147,6 +147,8 @@ def get_version(doctype, doc_name, frequency, user):
return timeline
def get_comments(doctype, doc_name, frequency, user):
+ from html2text import html2text
+
timeline = []
filters = get_filters("reference_name", doc_name, frequency, user)
comments = frappe.get_all("Comment",
@@ -166,7 +168,7 @@ def get_comments(doctype, doc_name, frequency, user):
"time": comment.modified,
"data": {
"time": time,
- "comment": frappe.utils.html2text(str(comment.content)),
+ "comment": html2text(str(comment.content)),
"by": by
},
"doctype": doctype,
@@ -197,6 +199,8 @@ def get_follow_users(doctype, doc_name):
)
def get_row_changed(row_changed, time, doctype, doc_name, v):
+ from html2text import html2text
+
items = []
for d in row_changed:
d[2] = d[2] if d[2] else ' '
@@ -209,8 +213,8 @@ def get_row_changed(row_changed, time, doctype, doc_name, v):
"table_field": d[0],
"row": str(d[1]),
"field": d[3][0][0],
- "from": frappe.utils.html2text(str(d[3][0][1])),
- "to": frappe.utils.html2text(str(d[3][0][2]))
+ "from": html2text(str(d[3][0][1])),
+ "to": html2text(str(d[3][0][2]))
},
"doctype": doctype,
"doc_name": doc_name,
@@ -236,6 +240,8 @@ def get_added_row(added, time, doctype, doc_name, v):
return items
def get_field_changed(changed, time, doctype, doc_name, v):
+ from html2text import html2text
+
items = []
for d in changed:
d[1] = d[1] if d[1] else ' '
@@ -246,8 +252,8 @@ def get_field_changed(changed, time, doctype, doc_name, v):
"data": {
"time": time,
"field": d[0],
- "from": frappe.utils.html2text(str(d[1])),
- "to": frappe.utils.html2text(str(d[2]))
+ "from": html2text(str(d[1])),
+ "to": html2text(str(d[2]))
},
"doctype": doctype,
"doc_name": doc_name,
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 733ee1774c..a62e2837d5 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -79,28 +79,30 @@ def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
@frappe.whitelist()
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
"""
- Cancel all linked doctype
+ Cancel all linked doctype, optionally ignore doctypes specified in a list.
Arguments:
- docs (str) - It contains all list of dictionaries of a linked documents.
+ docs (json str) - It contains list of dictionaries of a linked documents.
+ ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
"""
docs = json.loads(docs)
if isinstance(ignore_doctypes_on_cancel_all, string_types):
ignore_doctypes_on_cancel_all = json.loads(ignore_doctypes_on_cancel_all)
for i, doc in enumerate(docs, 1):
- if validate_linked_doc(doc, ignore_doctypes_on_cancel_all) is True:
- frappe.publish_progress(percent=i * 100 / ((len(docs) - len(ignore_doctypes_on_cancel_all))), title=_("Cancelling documents"))
+ if validate_linked_doc(doc, ignore_doctypes_on_cancel_all):
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
linked_doc.cancel()
+ frappe.publish_progress(percent=i/len(docs) * 100, title=_("Cancelling documents"))
def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
- Args:
- docs (dict): The document to check for submitted and non-exempt from auto-cancel
+ Arguments:
+ docinfo (dict): The document to check for submitted and non-exempt from auto-cancel
+ ignore_doctypes_on_cancel_all (list) - List of doctypes to ignore while cancelling.
Returns:
bool: True if linked document passes all validations, else False
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index d5428b1da2..e637f4969a 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -63,7 +63,7 @@ class FormMeta(Meta):
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_column_fields", '__templates',
- '__custom_js'):
+ '__custom_js', '__custom_list_js'):
d[k] = self.get(k)
# d['fields'] = d.get('fields', [])
@@ -130,9 +130,23 @@ class FormMeta(Meta):
def add_custom_script(self):
"""embed all require files"""
# custom script
- custom = frappe.db.get_value("Custom Script", {"dt": self.name, "enabled": 1}, "script") or ""
+ client_scripts = frappe.db.get_all("Client Script",
+ filters={"dt": self.name, "enabled": 1},
+ fields=["script", "view"],
+ order_by="creation asc"
+ ) or ""
- self.set("__custom_js", custom)
+ list_script = ''
+ form_script = ''
+ for script in client_scripts:
+ if script.view == 'List':
+ list_script += script.script
+
+ if script.view == 'Form':
+ form_script += script.script
+
+ self.set("__custom_js", form_script)
+ self.set("__custom_list_js", list_script)
def add_search_fields(self):
"""add search fields found in the doctypes indicated by link fields' options"""
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 4c3bab2e23..395d2b9571 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -47,7 +47,7 @@ def validate_link():
except Exception as e:
error_message = str(e).split("Unknown column '")
fieldname = None if len(error_message)<=1 else error_message[1].split("'")[0]
- frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom script").format(fieldname))
+ frappe.msgprint(_("Wrong fieldname {0} in add_fetch configuration of custom client script").format(fieldname))
frappe.errprint(frappe.get_traceback())
if fetch_value:
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index 8d00ea9bc2..2a981f061b 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -16,8 +16,18 @@ def get_leaderboards():
@frappe.whitelist()
def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
+ all_users = frappe.db.get_all('User',
+ filters = {
+ 'name': ['not in', ['Administrator', 'Guest']],
+ 'enabled': 1,
+ 'user_type': ['!=', 'Website User']
+ },
+ order_by = 'name ASC')
+ all_users_list = list(map(lambda x: x['name'], all_users))
+
filters = [
['type', '!=', 'Review'],
+ ['user', 'in', all_users_list]
]
if date_range:
date_range = frappe.parse_json(date_range)
@@ -28,15 +38,7 @@ def get_energy_point_leaderboard(date_range, company = None, field = None, limit
group_by = 'user',
order_by = 'value desc'
)
- all_users = frappe.db.get_all('User',
- filters = {
- 'name': ['not in', ['Administrator', 'Guest']],
- 'enabled': 1,
- 'user_type': ['!=', 'Website User']
- },
- order_by = 'name ASC')
- all_users_list = list(map(lambda x: x['name'], all_users))
energy_point_users_list = list(map(lambda x: x['name'], energy_point_users))
for user in all_users_list:
if user not in energy_point_users_list:
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index 825e1d959b..b3fccf84f9 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -141,7 +141,7 @@ class Leaderboard {
}
create_date_range_field() {
- let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
+ let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`);
this.date_range_field = $(``).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index c39e7f52c0..f44a57e339 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -201,7 +201,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
this.abort_setup(r.message.fail);
}
},
- error: this.abort_setup("Error in setup", true)
+ error: () => this.abort_setup("Error in setup")
});
}
diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js
index 61f8ec3c06..c1a89f316e 100644
--- a/frappe/desk/page/user_profile/user_profile_controller.js
+++ b/frappe/desk/page/user_profile/user_profile_controller.js
@@ -151,32 +151,30 @@ class UserProfile {
// eslint-disable-next-line no-unused-vars
render_percentage_chart(field, title) {
- // REDESIGN-TODO: chart seems to be broken. Enable this once fixed.
- this.wrapper.find('.percentage-chart-container').hide();
- // frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', {
- // user: this.user_id,
- // field: field
- // }).then(chart => {
- // if (chart.labels.length) {
- // this.percentage_chart = new frappe.Chart('.performance-percentage-chart', {
- // type: 'percentage',
- // data: {
- // labels: chart.labels,
- // datasets: chart.datasets
- // },
- // truncateLegends: 1,
- // barOptions: {
- // height: 11,
- // depth: 1
- // },
- // height: 200,
- // maxSlices: 8,
- // colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'],
- // });
- // } else {
- // this.wrapper.find('.percentage-chart-container').hide();
- // }
- // });
+ frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', {
+ user: this.user_id,
+ field: field
+ }).then(chart => {
+ if (chart.labels.length) {
+ this.percentage_chart = new frappe.Chart('.performance-percentage-chart', {
+ type: 'percentage',
+ data: {
+ labels: chart.labels,
+ datasets: chart.datasets
+ },
+ truncateLegends: 1,
+ barOptions: {
+ height: 11,
+ depth: 1
+ },
+ height: 200,
+ maxSlices: 8,
+ colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'],
+ });
+ } else {
+ this.wrapper.find('.percentage-chart-container').hide();
+ }
+ });
}
create_line_chart_filters() {
@@ -452,4 +450,4 @@ class UserProfileTimeline extends BaseTimeline {
}
frappe.provide('frappe.ui');
-frappe.ui.UserProfile = UserProfile;
\ No newline at end of file
+frappe.ui.UserProfile = UserProfile;
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 3008cf0e61..22d47d1120 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -164,10 +164,14 @@ def get_script(report_name):
module = report.module or frappe.db.get_value(
"DocType", report.ref_doctype, "module"
)
- module_path = get_module_path(module)
- report_folder = os.path.join(module_path, "report", scrub(report.name))
- script_path = os.path.join(report_folder, scrub(report.name) + ".js")
- print_path = os.path.join(report_folder, scrub(report.name) + ".html")
+
+ is_custom_module = frappe.get_cached_value("Module Def", module, "custom")
+
+ # custom modules are virtual modules those exists in DB but not in disk.
+ module_path = '' if is_custom_module else get_module_path(module)
+ report_folder = module_path and os.path.join(module_path, "report", scrub(report.name))
+ script_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".js")
+ print_path = report_folder and os.path.join(report_folder, scrub(report.name) + ".html")
script = None
if os.path.exists(script_path):
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index f4e6543844..6faa827dde 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -80,13 +80,15 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
is_whitelisted(frappe.get_attr(query))
frappe.response["values"] = frappe.call(query, doctype, txt,
searchfield, start, page_length, filters, as_dict=as_dict)
- except Exception as e:
+ except frappe.exceptions.PermissionError as e:
if frappe.local.conf.developer_mode:
raise e
else:
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
indicator_color='red', http_status_code=404)
return
+ except Exception as e:
+ raise e
elif not query and doctype in standard_queries:
# from standard queries
search_widget(doctype, txt, standard_queries[doctype][0],
diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py
index c19e531739..01b47ac106 100644
--- a/frappe/desk/utils.py
+++ b/frappe/desk/utils.py
@@ -7,12 +7,13 @@ def validate_route_conflict(doctype, name):
'''
Raises exception if name clashes with routes from other documents for /app routing
'''
- if frappe.flags.ignore_route_conflict_validation:
- return
all_names = []
for _doctype in ['Page', 'Workspace', 'DocType']:
- all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)])
+ try:
+ all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)])
+ except frappe.db.TableMissingError:
+ pass
if slug(name) in all_names:
frappe.msgprint(frappe._('Name already taken, please set a new name'))
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index ca4dbb83e2..4869c5a9bf 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -90,6 +90,29 @@ class EmailAccount(Document):
if self.append_to not in valid_doctypes:
frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
+ def before_save(self):
+ messages = []
+ as_list = 1
+ if not self.enable_incoming and self.default_incoming:
+ self.default_incoming = False
+ messages.append(_("{} has been disabled. It can only be enabled if {} is checked.")
+ .format(
+ frappe.bold(_('Default Incoming')),
+ frappe.bold(_('Enable Incoming'))
+ )
+ )
+ if not self.enable_outgoing and self.default_outgoing:
+ self.default_outgoing = False
+ messages.append(_("{} has been disabled. It can only be enabled if {} is checked.")
+ .format(
+ frappe.bold(_('Default Outgoing')),
+ frappe.bold(_('Enable Outgoing'))
+ )
+ )
+ if messages:
+ if len(messages) == 1: (as_list, messages) = (0, messages[0])
+ frappe.msgprint(messages, as_list= as_list, indicator='orange', title=_("Defaults Updated"))
+
def on_update(self):
"""Check there is only one default of each type."""
from frappe.core.doctype.user.user import setup_user_email_inbox
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 1dd6115b43..dcd19ed33c 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -19,9 +19,12 @@
"message",
"message_md",
"message_html",
+ "section_break_13",
"send_unsubscribe_link",
"send_attachments",
+ "column_break_9",
"published",
+ "send_webview_link",
"route",
"test_the_newsletter",
"test_email_id",
@@ -160,6 +163,21 @@
"fieldtype": "Check",
"label": "Schedule Sending",
"read_only_depends_on": "eval: doc.email_sent"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "published",
+ "fieldname": "send_webview_link",
+ "fieldtype": "Check",
+ "label": "Send Web View Link"
+ },
+ {
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break"
}
],
"has_web_view": 1,
@@ -169,7 +187,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2020-08-24 19:59:37.262500",
+ "modified": "2021-02-22 14:33:56.095380",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 2791ebb75b..ad985ee20e 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -68,13 +68,17 @@ class Newsletter(WebsiteGenerator):
except IOError:
frappe.throw(_("Unable to find attachment {0}").format(file.name))
- send(recipients=self.recipients, sender=sender,
- subject=self.subject, message=self.get_message(),
+ args = {
+ "message": self.get_message(),
+ "name": self.name
+ }
+ frappe.sendmail(recipients=self.recipients, sender=sender,
+ subject=self.subject, message=self.get_message(), template="newsletter",
reference_doctype=self.doctype, reference_name=self.name,
add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments,
unsubscribe_method="/unsubscribe",
unsubscribe_params={"name": self.name},
- send_priority=0, queue_separately=True)
+ send_priority=0, queue_separately=True, args=args)
if not frappe.flags.in_test:
frappe.db.auto_commit_on_many_writes = False
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 7aa70830e7..3fb1dfa0da 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -297,8 +297,9 @@ def inline_style_in_html(html):
for app in apps:
path = 'assets/{0}/css/email.css'.format(app)
- if os.path.exists(os.path.abspath(path)):
- css_files.append(path)
+ css_files.append(path)
+
+ css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))]
p = Premailer(html=html, external_styles=css_files, strip_important=False)
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 9b0b5e41d7..3fcabb9495 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -17,7 +17,7 @@ class TestEmailBody(unittest.TestCase):
This is embedded image you asked for
-Notes:
\n\ndata-fieldtype and data-fieldnamevaluesection-breakcolumn-break1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-left: left; }\n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }\n"
+ "options": "Notes:
\n\ndata-fieldtype and data-fieldnamevaluesection-breakcolumn-break1. Left align integers
\n\n[data-fieldtype=\"Int\"] .value { text-align: left; }\n\n1. Add border to sections except the last section
\n\n.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }\n"
},
{
"depends_on": "custom_format",
@@ -211,7 +211,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-14 11:38:49.132061",
+ "modified": "2021-03-01 15:25:46.578863",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 7e1db1eddb..dfd93c4efa 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -269,6 +269,7 @@ frappe.ui.form.PrintView = class {
based_on: data.based_on,
};
frappe.set_route('print-format-builder');
+ this.print_sel.val(data.print_format_name);
},
__('New Custom Print Format'),
__('Start')
@@ -412,6 +413,12 @@ frappe.ui.form.PrintView = class {
`
);
+ if (frappe.utils.is_rtl(this.lang_code)) {
+ this.$print_format_body.find('head').append(
+ ``
+ );
+ }
+
this.$print_format_body.find('body').html(
`