Merge branch 'develop' into fix-link-selector
This commit is contained in:
commit
552dc47e90
41 changed files with 675 additions and 151 deletions
|
|
@ -9,6 +9,6 @@ trim_trailing_whitespace = true
|
|||
charset = utf-8
|
||||
|
||||
# python, js indentation settings
|
||||
[{*.py,*.js}]
|
||||
[{*.py,*.js,*.vue}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
|
|
|||
5
.github/helper/install_dependencies.sh
vendored
5
.github/helper/install_dependencies.sh
vendored
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
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
|
||||
|
|
|
|||
84
.github/helper/roulette.py
vendored
84
.github/helper/roulette.py
vendored
|
|
@ -1,56 +1,72 @@
|
|||
# if the script ends with exit code 0, then no tests are run further, else all tests are run
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def get_files_list(pr_number, repo="frappe/frappe"):
|
||||
req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
|
||||
res = urllib.request.urlopen(req)
|
||||
dump = json.loads(res.read().decode('utf8'))
|
||||
return [change["filename"] for change in dump]
|
||||
|
||||
def get_output(command, shell=True):
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
print(command)
|
||||
command = shlex.split(command)
|
||||
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
|
||||
|
||||
def is_py(file):
|
||||
return file.endswith("py")
|
||||
return file.endswith("py")
|
||||
|
||||
def is_js(file):
|
||||
return file.endswith("js")
|
||||
def is_ci(file):
|
||||
return ".github" in file
|
||||
|
||||
def is_frontend_code(file):
|
||||
return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue"))
|
||||
|
||||
def is_docs(file):
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
regex = re.compile(r'\.(md|png|jpg|jpeg|csv)$|^.github|LICENSE')
|
||||
return bool(regex.search(file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_type = os.environ.get("TYPE")
|
||||
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))
|
||||
files_list = sys.argv[1:]
|
||||
build_type = os.environ.get("TYPE")
|
||||
pr_number = os.environ.get("PR_NUMBER")
|
||||
repo = os.environ.get("REPO_NAME")
|
||||
|
||||
try:
|
||||
files_changed = get_output("git diff --name-only {}".format(commit_range), shell=False)
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
# this is a push build, run all builds
|
||||
if not pr_number:
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
sys.exit(0)
|
||||
|
||||
if "fatal" not in files_changed:
|
||||
files_list = files_changed.split()
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_js_changed = len(list(filter(is_js, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
|
||||
|
||||
if only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
if not files_list:
|
||||
print("No files' changes detected. Build is shutting")
|
||||
sys.exit(0)
|
||||
|
||||
if only_js_changed and build_type == "server":
|
||||
print("Only JavaScript code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
|
||||
|
||||
if only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
if ci_files_changed:
|
||||
print("CI related files were updated, running all build processes.")
|
||||
|
||||
sys.exit(2)
|
||||
elif only_docs_changed:
|
||||
print("Only docs were updated, stopping build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_frontend_code_changed and build_type == "server":
|
||||
print("Only Frontend code was updated; Stopping Python build process.")
|
||||
sys.exit(0)
|
||||
|
||||
elif only_py_changed and build_type == "ui":
|
||||
print("Only Python code was updated, stopping Cypress build process.")
|
||||
sys.exit(0)
|
||||
|
||||
os.system('echo "::set-output name=build::strawberry"')
|
||||
|
|
|
|||
22
.github/workflows/patch-mariadb-tests.yml
vendored
22
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -2,6 +2,11 @@ name: Patch
|
|||
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
|
||||
concurrency:
|
||||
group: patch-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
|
@ -26,10 +31,21 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -39,6 +55,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -51,10 +68,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -63,6 +82,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -70,12 +90,14 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Patch Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: |
|
||||
cd ~/frappe-bench/
|
||||
wget https://frappeframework.com/files/v10-frappe.sql.gz
|
||||
|
|
|
|||
38
.github/workflows/server-mariadb-tests.yml
vendored
38
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -6,6 +6,11 @@ on:
|
|||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
|
@ -35,17 +40,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
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
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -55,6 +72,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -67,10 +85,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -79,6 +99,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -86,18 +107,22 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: upload-coverage-data
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
|
|
@ -111,16 +136,29 @@ jobs:
|
|||
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
||||
COVERALLS_PARALLEL: true
|
||||
|
||||
- run: echo ${{ steps.check-build.outputs.build }} > guess-the-fruit.txt
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: fruit
|
||||
path: guess-the-fruit.txt
|
||||
|
||||
coveralls:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
container: python:3-slim
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: fruit
|
||||
- run: echo "WILDCARD=$(cat fruit/guess-the-fruit.txt)" >> $GITHUB_ENV
|
||||
|
||||
- name: Clone
|
||||
if: ${{ env.WILDCARD == 'strawberry' }}
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Coveralls Finished
|
||||
if: ${{ env.WILDCARD == 'strawberry' }}
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
|
|
|
|||
22
.github/workflows/server-postgres-tests.yml
vendored
22
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -4,6 +4,10 @@ on:
|
|||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
|
@ -37,17 +41,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: '14'
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
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
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -57,6 +73,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -69,10 +86,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -81,6 +100,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -88,12 +108,14 @@ jobs:
|
|||
TYPE: server
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
|
||||
env:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
|
|
|
|||
24
.github/workflows/ui-tests.yml
vendored
24
.github/workflows/ui-tests.yml
vendored
|
|
@ -6,6 +6,10 @@ on:
|
|||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
concurrency:
|
||||
group: ui-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
|
@ -35,17 +39,29 @@ jobs:
|
|||
with:
|
||||
python-version: 3.7
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
python "${GITHUB_WORKSPACE}/.github/helper/roulette.py"
|
||||
env:
|
||||
TYPE: "ui"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
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
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
|
@ -55,6 +71,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
|
|
@ -67,10 +84,12 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -79,6 +98,7 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Cache cypress binary
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache
|
||||
|
|
@ -88,6 +108,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
|
||||
env:
|
||||
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
|
||||
|
|
@ -95,15 +116,18 @@ jobs:
|
|||
TYPE: ui
|
||||
|
||||
- name: Install
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Site Setup
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@
|
|||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
patches/ @surajshetty3416 @gavindsouza
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
database @gavindsouza
|
||||
model @gavindsouza
|
||||
requirements.txt @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
workspace @shariquerik
|
||||
|
|
|
|||
63
cypress/integration/dashboard_links.js
Normal file
63
cypress/integration/dashboard_links.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
context('Dashboard links', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if initially the dashboard contains only the "Contact" link and there is no counter
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
|
||||
//Adding a new contact
|
||||
cy.get('.btn[data-doctype="Contact"]').click();
|
||||
cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
|
||||
//To check if the counter for contact doc is "1" after adding the contact
|
||||
cy.get('[data-doctype="Contact"] > .count').should('contain', '1');
|
||||
cy.get('[data-doctype="Contact"]').contains('Contact').click();
|
||||
|
||||
//Deleting the newly created contact
|
||||
cy.visit('/app/contact');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click({delay: 700});
|
||||
|
||||
|
||||
//To check if the counter from the "Contact" doc link is removed
|
||||
cy.wait(700);
|
||||
cy.visit('/app/user');
|
||||
cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click();
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
});
|
||||
|
||||
it('Report link in dashboard', () => {
|
||||
cy.visit('/app/user');
|
||||
cy.visit('/app/user/Administrator');
|
||||
cy.get('[data-doctype="Contact"]').should('contain', 'Contact');
|
||||
cy.findByText('Connections');
|
||||
cy.window()
|
||||
.its('cur_frm')
|
||||
.then(cur_frm => {
|
||||
cur_frm.dashboard.data.reports = [
|
||||
{
|
||||
'label': 'Reports',
|
||||
'items': ['Permitted Documents For User']
|
||||
}
|
||||
];
|
||||
cur_frm.dashboard.render_report_links();
|
||||
cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
|
||||
cy.findByText('Permitted Documents For User');
|
||||
cy.findByPlaceholderText('User').should("have.value", "Administrator");
|
||||
});
|
||||
});
|
||||
});
|
||||
19
cypress/integration/datetime_field_form_validation.js
Normal file
19
cypress/integration/datetime_field_form_validation.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
context('Datetime Field Validation', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/communication');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call("frappe.tests.ui_test_helpers.create_communication_records");
|
||||
});
|
||||
});
|
||||
|
||||
// validating datetime field value when value is set from backend and get validated on form load.
|
||||
it('datetime field form validation', () => {
|
||||
cy.visit('/app/communication');
|
||||
cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name')
|
||||
.then((name) => {
|
||||
cy.visit(`/app/communication/${name}`);
|
||||
cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red');
|
||||
});
|
||||
});
|
||||
});
|
||||
79
cypress/integration/folder_navigation.js
Normal file
79
cypress/integration/folder_navigation.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
context('Folder Navigation', () => {
|
||||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/file');
|
||||
});
|
||||
|
||||
it('Adding Folders', () => {
|
||||
//Adding filter to go into the home folder
|
||||
cy.get('.filter-selector > .btn').findByText('1 filter').click();
|
||||
cy.findByRole('button', {name: 'Clear Filters'}).click();
|
||||
cy.get('.filter-action-buttons > .text-muted').findByText('+ Add a Filter').click();
|
||||
cy.get('.fieldname-select-area > .awesomplete > .form-control').type('Fol{enter}');
|
||||
cy.get('.filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback').type('Home{enter}');
|
||||
cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();
|
||||
|
||||
//Adding folder (Test Folder)
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
});
|
||||
|
||||
it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
|
||||
//Navigating inside the Attachments folder
|
||||
cy.get('[title="Attachments"] > span').click();
|
||||
|
||||
//To check if the URL formed after visiting the attachments folder is correct
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments');
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
|
||||
//Adding folder inside the attachments folder
|
||||
cy.get('.menu-btn-group > .btn').click();
|
||||
cy.get('.menu-btn-group [data-label="New Folder"]').click();
|
||||
cy.get('form > [data-fieldname="value"]').type('Test Folder');
|
||||
cy.findByRole('button', {name: 'Create'}).click();
|
||||
|
||||
//Navigating inside the added folder in the Attachments folder
|
||||
cy.get('[title="Test Folder"] > span').click();
|
||||
|
||||
//To check if the URL is correct after visiting the Test Folder
|
||||
cy.location('pathname').should('eq', '/app/file/view/home/Attachments/Test%20Folder');
|
||||
cy.visit('/app/file/view/home/Attachments/Test%20Folder');
|
||||
|
||||
//Adding a file inside the Test Folder
|
||||
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
|
||||
cy.get('.file-uploader').findByText('Link').click();
|
||||
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
|
||||
cy.findByRole('button', {name: 'Upload'}).click();
|
||||
|
||||
//To check if the added file is present in the Test Folder
|
||||
cy.get('span.level-item > span').should('contain', 'Test Folder');
|
||||
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
|
||||
//Deleting the added file from the Test folder
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.wait(700);
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.wait(700);
|
||||
|
||||
//Deleting the Test Folder
|
||||
cy.visit('/app/file/view/home/Attachments');
|
||||
cy.get('.list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
|
||||
it('Deleting Test Folder from the home', () => {
|
||||
//Deleting the Test Folder added in the home directory
|
||||
cy.visit('/app/file/view/home');
|
||||
cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500});
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
});
|
||||
|
|
@ -29,15 +29,8 @@ frappe.ui.form.on("File", "refresh", function(frm) {
|
|||
if (is_optimizable) {
|
||||
frm.add_custom_button(__("Optimize"), function() {
|
||||
frappe.show_alert(__("Optimizing image..."));
|
||||
frappe.call({
|
||||
method: "frappe.core.doctype.file.file.optimize_saved_image",
|
||||
args: {
|
||||
doc_name: frm.doc.name,
|
||||
},
|
||||
callback: function() {
|
||||
frappe.show_alert(__("Image optimized"));
|
||||
frappe.set_route("List", "File");
|
||||
}
|
||||
frm.call("optimize_file").then(() => {
|
||||
frappe.show_alert(__("Image optimized"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,8 +313,16 @@ class File(Document):
|
|||
self.delete_file_data_content(only_thumbnail=True)
|
||||
|
||||
def on_rollback(self):
|
||||
self.flags.on_rollback = True
|
||||
self.on_trash()
|
||||
# if original_content flag is set, this rollback should revert the file to its original state
|
||||
if self.flags.original_content:
|
||||
file_path = self.get_full_path()
|
||||
with open(file_path, "wb+") as f:
|
||||
f.write(self.flags.original_content)
|
||||
|
||||
# following condition is only executed when an insert has been rolledback
|
||||
else:
|
||||
self.flags.on_rollback = True
|
||||
self.on_trash()
|
||||
|
||||
def unzip(self):
|
||||
'''Unzip current file and replace it by its children'''
|
||||
|
|
@ -531,6 +539,35 @@ class File(Document):
|
|||
if self.file_url:
|
||||
self.is_private = cint(self.file_url.startswith('/private'))
|
||||
|
||||
@frappe.whitelist()
|
||||
def optimize_file(self):
|
||||
if self.is_folder:
|
||||
raise TypeError('Folders cannot be optimized')
|
||||
|
||||
content_type = mimetypes.guess_type(self.file_name)[0]
|
||||
is_local_image = content_type.startswith('image/') and self.file_size > 0
|
||||
is_svg = content_type == 'image/svg+xml'
|
||||
|
||||
if not is_local_image:
|
||||
raise NotImplementedError('Only local image files can be optimized')
|
||||
|
||||
if is_svg:
|
||||
raise TypeError('Optimization of SVG images is not supported')
|
||||
|
||||
content = self.get_content()
|
||||
file_path = self.get_full_path()
|
||||
optimized_content = optimize_image(content, content_type)
|
||||
|
||||
with open(file_path, 'wb+') as f:
|
||||
f.write(optimized_content)
|
||||
|
||||
self.file_size = len(optimized_content)
|
||||
self.content_hash = get_content_hash(optimized_content)
|
||||
# if rolledback, revert back to original
|
||||
self.flags.original_content = content
|
||||
frappe.local.rollback_observers.append(self)
|
||||
self.save()
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
|
||||
|
||||
|
|
@ -838,22 +875,6 @@ def unzip_file(name):
|
|||
files = file_obj.unzip()
|
||||
return files
|
||||
|
||||
@frappe.whitelist()
|
||||
def optimize_saved_image(doc_name):
|
||||
file_doc = frappe.get_doc('File', doc_name)
|
||||
content = file_doc.get_content()
|
||||
content_type = mimetypes.guess_type(file_doc.file_name)[0]
|
||||
|
||||
optimized_content = optimize_image(content, content_type)
|
||||
|
||||
file_path = get_files_path(is_private=file_doc.is_private)
|
||||
file_path = os.path.join(file_path.encode('utf-8'), file_doc.file_name.encode('utf-8'))
|
||||
with open(file_path, 'wb+') as f:
|
||||
f.write(optimized_content)
|
||||
|
||||
file_doc.file_size = len(optimized_content)
|
||||
file_doc.content_hash = get_content_hash(optimized_content)
|
||||
file_doc.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_attached_images(doctype, names):
|
||||
|
|
|
|||
|
|
@ -440,6 +440,7 @@ class TestFile(unittest.TestCase):
|
|||
}).insert(ignore_permissions=True)
|
||||
self.assertRaisesRegex(frappe.exceptions.ValidationError, 'not a zip file', test_file.unzip)
|
||||
|
||||
|
||||
class TestAttachment(unittest.TestCase):
|
||||
test_doctype = 'Test For Attachment'
|
||||
|
||||
|
|
@ -569,3 +570,68 @@ class TestFileUtils(unittest.TestCase):
|
|||
from frappe.core.doctype.file.file import create_new_folder
|
||||
folder = create_new_folder('test_folder', 'Home')
|
||||
self.assertTrue(folder.is_folder)
|
||||
|
||||
|
||||
class TestFileOptimization(unittest.TestCase):
|
||||
def test_optimize_file(self):
|
||||
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": "sample_image_for_optimization.jpg",
|
||||
"content": file_content
|
||||
}).insert()
|
||||
original_size = test_file.file_size
|
||||
original_content_hash = test_file.content_hash
|
||||
|
||||
test_file.optimize_file()
|
||||
optimized_size = test_file.file_size
|
||||
updated_content_hash = test_file.content_hash
|
||||
|
||||
self.assertLess(optimized_size, original_size)
|
||||
self.assertNotEqual(original_content_hash, updated_content_hash)
|
||||
test_file.delete()
|
||||
|
||||
def test_optimize_svg(self):
|
||||
file_path = frappe.get_app_path("frappe", "tests/data/sample_svg.svg")
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": "sample_svg.svg",
|
||||
"content": file_content
|
||||
}).insert()
|
||||
self.assertRaises(TypeError, test_file.optimize_file)
|
||||
test_file.delete()
|
||||
|
||||
def test_optimize_textfile(self):
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": "sample_text.txt",
|
||||
"content": "Text files cannot be optimized"
|
||||
}).insert()
|
||||
self.assertRaises(NotImplementedError, test_file.optimize_file)
|
||||
test_file.delete()
|
||||
|
||||
def test_optimize_folder(self):
|
||||
test_folder = frappe.get_doc("File", "Home/Attachments")
|
||||
self.assertRaises(TypeError, test_folder.optimize_file)
|
||||
|
||||
def test_revert_optimized_file_on_rollback(self):
|
||||
file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg")
|
||||
with open(file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": "sample_image_for_optimization.jpg",
|
||||
"content": file_content
|
||||
}).insert()
|
||||
image_path = test_file.get_full_path()
|
||||
size_before_optimization = os.stat(image_path).st_size
|
||||
|
||||
test_file.optimize_file()
|
||||
frappe.db.rollback()
|
||||
size_after_rollback = os.stat(image_path).st_size
|
||||
self.assertEqual(size_before_optimization, size_after_rollback)
|
||||
test_file.delete()
|
||||
|
|
@ -12,19 +12,20 @@ class TestWorkspace(unittest.TestCase):
|
|||
frappe.db.delete("DocType", {"module": "Test Module"})
|
||||
frappe.delete_doc("Module Def", "Test Module")
|
||||
|
||||
def test_workspace_with_cards_specific_to_a_country(self):
|
||||
workspace = create_workspace()
|
||||
insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France")
|
||||
insert_card(workspace, "Card Label 2", "DocType A", "DocType B")
|
||||
# TODO: FIX ME - flaky test!!!
|
||||
# def test_workspace_with_cards_specific_to_a_country(self):
|
||||
# workspace = create_workspace()
|
||||
# insert_card(workspace, "Card Label 1", "DocType 1", "DocType 2", "France")
|
||||
# insert_card(workspace, "Card Label 2", "DocType A", "DocType B")
|
||||
|
||||
workspace.insert(ignore_if_duplicate = True)
|
||||
# workspace.insert(ignore_if_duplicate = True)
|
||||
|
||||
cards = workspace.get_link_groups()
|
||||
# cards = workspace.get_link_groups()
|
||||
|
||||
if frappe.get_system_settings('country') == "France":
|
||||
self.assertEqual(len(cards), 2)
|
||||
else:
|
||||
self.assertEqual(len(cards), 1)
|
||||
# if frappe.get_system_settings('country') == "France":
|
||||
# self.assertEqual(len(cards), 2)
|
||||
# else:
|
||||
# self.assertEqual(len(cards), 1)
|
||||
|
||||
def create_module(module_name):
|
||||
module = frappe.get_doc({
|
||||
|
|
@ -91,4 +92,4 @@ def create_doctype(doctype_name, module):
|
|||
'permissions': [
|
||||
{'role': 'System Manager'}
|
||||
]
|
||||
}).insert(ignore_if_duplicate = True)
|
||||
}).insert(ignore_if_duplicate = True)
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ def handle_duration_fieldtype_values(result, columns):
|
|||
return result
|
||||
|
||||
|
||||
def build_xlsx_data(columns, data, visible_idx, include_indentation):
|
||||
def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
|
||||
result = [[]]
|
||||
column_widths = []
|
||||
|
||||
|
|
@ -407,7 +407,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
|
|||
# build table from result
|
||||
for row_idx, row in enumerate(data.result):
|
||||
# only pick up rows that are visible in the report
|
||||
if row_idx in visible_idx:
|
||||
if ignore_visible_idx or row_idx in visible_idx:
|
||||
row_data = []
|
||||
if isinstance(row, dict):
|
||||
for col_idx, column in enumerate(data.columns):
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ def get_users_for_mentions():
|
|||
'name': ['not in', ('Administrator', 'Guest')],
|
||||
'allowed_in_mentions': True,
|
||||
'user_type': 'System User',
|
||||
'enabled': True,
|
||||
})
|
||||
|
||||
def get_user_groups():
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from frappe.utils import (format_time, get_link_to_form, get_url_to_report,
|
|||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils.csvutils import to_csv
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
from frappe.desk.query_report import build_xlsx_data
|
||||
|
||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
|
||||
|
||||
|
|
@ -99,13 +100,21 @@ class AutoEmailReport(Document):
|
|||
return self.get_html_table(columns, data)
|
||||
|
||||
elif self.format == 'XLSX':
|
||||
spreadsheet_data = self.get_spreadsheet_data(columns, data)
|
||||
xlsx_file = make_xlsx(spreadsheet_data, "Auto Email Report")
|
||||
report_data = frappe._dict()
|
||||
report_data['columns'] = columns
|
||||
report_data['result'] = data
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
|
||||
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
|
||||
return xlsx_file.getvalue()
|
||||
|
||||
elif self.format == 'CSV':
|
||||
spreadsheet_data = self.get_spreadsheet_data(columns, data)
|
||||
return to_csv(spreadsheet_data)
|
||||
report_data = frappe._dict()
|
||||
report_data['columns'] = columns
|
||||
report_data['result'] = data
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
|
||||
return to_csv(xlsx_data)
|
||||
|
||||
else:
|
||||
frappe.throw(_('Invalid Output Format'))
|
||||
|
|
@ -126,18 +135,6 @@ class AutoEmailReport(Document):
|
|||
'edit_report_settings': get_link_to_form('Auto Email Report', self.name)
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def get_spreadsheet_data(columns, data):
|
||||
out = [[_(df.label) for df in columns], ]
|
||||
for row in data:
|
||||
new_row = []
|
||||
out.append(new_row)
|
||||
for df in columns:
|
||||
if df.fieldname not in row: continue
|
||||
new_row.append(frappe.format(row[df.fieldname], df, row))
|
||||
|
||||
return out
|
||||
|
||||
def get_file_name(self):
|
||||
return "{0}.{1}".format(self.report.replace(" ", "-").replace("/", "-"), self.format.lower())
|
||||
|
||||
|
|
|
|||
|
|
@ -874,7 +874,7 @@ class BaseDocument(object):
|
|||
return self._precision[cache_key][fieldname]
|
||||
|
||||
|
||||
def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False):
|
||||
def get_formatted(self, fieldname, doc=None, currency=None, absolute_value=False, translated=False, format=None):
|
||||
from frappe.utils.formatters import format_value
|
||||
|
||||
df = self.meta.get_field(fieldname)
|
||||
|
|
@ -898,7 +898,7 @@ class BaseDocument(object):
|
|||
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)):
|
||||
val = abs(self.get(fieldname))
|
||||
|
||||
return format_value(val, df=df, doc=doc, currency=currency)
|
||||
return format_value(val, df=df, doc=doc, currency=currency, format=format)
|
||||
|
||||
def is_print_hide(self, fieldname, df=None, for_print=True):
|
||||
"""Returns true if fieldname is to be hidden for print.
|
||||
|
|
|
|||
|
|
@ -567,7 +567,7 @@
|
|||
<path d="M6.45466 8.81824L4.47873 10.7942C3.85205 11.4211 3.5 12.2713 3.5 13.1577C3.5 14.0442 3.85205 14.8943 4.47873 15.5213V15.5213C5.10568 16.148 5.95584 16.5 6.84229 16.5C7.72874 16.5 8.5789 16.148 9.20584 15.5213L11.1818 13.5453" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 24 24" fill="none" id="icon-scan" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"
|
||||
stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/>
|
||||
</symbol>
|
||||
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
|
@ -13,6 +13,13 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __('Export Data'),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'file_type',
|
||||
label: __('File Type'),
|
||||
options: ['Excel', 'CSV'],
|
||||
default: 'CSV'
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'export_records',
|
||||
|
|
@ -45,13 +52,6 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
fieldname: 'filter_area',
|
||||
depends_on: doc => doc.export_records === 'by_filter'
|
||||
},
|
||||
{
|
||||
fieldtype: 'Select',
|
||||
fieldname: 'file_type',
|
||||
label: __('File Type'),
|
||||
options: ['Excel', 'CSV'],
|
||||
default: 'CSV'
|
||||
},
|
||||
{
|
||||
fieldtype: 'Section Break'
|
||||
},
|
||||
|
|
@ -141,7 +141,7 @@ frappe.data_import.DataExporter = class DataExporter {
|
|||
let for_insert = this.exporting_for === 'Insert New Records';
|
||||
let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update');
|
||||
let $select_all_buttons = $(`
|
||||
<div>
|
||||
<div class="mb-3">
|
||||
<h6 class="form-section-heading uppercase">${section_title}</h6>
|
||||
<button class="btn btn-default btn-xs" data-action="select_all">
|
||||
${__('Select All')}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default class FileUploader {
|
|||
}
|
||||
|
||||
if (attach_doc_image) {
|
||||
restrictions.allowed_file_types = ['.jpg', '.jpeg', '.png'];
|
||||
restrictions.allowed_file_types = ['image/jpeg', 'image/png'];
|
||||
}
|
||||
|
||||
this.$fileuploader = new Vue({
|
||||
|
|
@ -70,8 +70,10 @@ export default class FileUploader {
|
|||
this.uploader.$watch('hide_dialog_footer', (hide_dialog_footer) => {
|
||||
if (hide_dialog_footer) {
|
||||
this.dialog && this.dialog.footer.addClass('hide');
|
||||
this.dialog.$wrapper.data('bs.modal')._config.backdrop = 'static';
|
||||
} else {
|
||||
this.dialog && this.dialog.footer.removeClass('hide');
|
||||
this.dialog.$wrapper.data('bs.modal')._config.backdrop = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
|
|||
this.ace_editor_target = $('<div class="ace-editor-target"></div>')
|
||||
.appendTo(this.input_area);
|
||||
|
||||
this.expanded = false;
|
||||
this.$expand_button = $(`<button class="btn btn-xs btn-default">${this.get_button_label()}</button>`).click(() => {
|
||||
this.expanded = !this.expanded;
|
||||
this.refresh_height();
|
||||
this.toggle_label();
|
||||
}).appendTo(this.$input_wrapper);
|
||||
|
||||
// styling
|
||||
this.ace_editor_target.addClass('border rounded');
|
||||
this.ace_editor_target.css('height', 300);
|
||||
|
|
@ -22,6 +17,21 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
|
|||
// initialize
|
||||
const ace = window.ace;
|
||||
this.editor = ace.edit(this.ace_editor_target.get(0));
|
||||
|
||||
if (this.df.max_lines || this.df.min_lines) {
|
||||
if (this.df.max_lines)
|
||||
this.editor.setOption("maxLines", this.df.max_lines);
|
||||
if (this.df.min_lines)
|
||||
this.editor.setOption("minLines", this.df.min_lines);
|
||||
} else {
|
||||
this.expanded = false;
|
||||
this.$expand_button = $(`<button class="btn btn-xs btn-default">${this.get_button_label()}</button>`).click(() => {
|
||||
this.expanded = !this.expanded;
|
||||
this.refresh_height();
|
||||
this.toggle_label();
|
||||
}).appendTo(this.$input_wrapper);
|
||||
}
|
||||
|
||||
this.editor.setTheme('ace/theme/tomorrow');
|
||||
this.editor.setOption("showPrintMargin", false);
|
||||
this.editor.setOption("wrap", this.df.wrap);
|
||||
|
|
|
|||
|
|
@ -36,4 +36,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
|
|||
$tp.$secondsText.prev().css('display', 'none');
|
||||
}
|
||||
}
|
||||
|
||||
get_model_value() {
|
||||
let value = super.get_model_value();
|
||||
return frappe.datetime.get_datetime_as_string(value);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -283,21 +283,13 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
$(frappe.render_template('form_links', this.data))
|
||||
.appendTo(transactions_area_body);
|
||||
|
||||
if (this.data.reports && this.data.reports.length) {
|
||||
$(frappe.render_template('report_links', this.data))
|
||||
.appendTo(transactions_area_body);
|
||||
}
|
||||
this.render_report_links();
|
||||
|
||||
// bind links
|
||||
transactions_area_body.find(".badge-link").on('click', function() {
|
||||
me.open_document_list($(this).closest('.document-link'));
|
||||
});
|
||||
|
||||
// bind reports
|
||||
transactions_area_body.find(".report-link").on('click', function() {
|
||||
me.open_report($(this).parent());
|
||||
});
|
||||
|
||||
// bind open notifications
|
||||
transactions_area_body.find('.open-notification').on('click', function() {
|
||||
me.open_document_list($(this).parent(), true);
|
||||
|
|
@ -311,6 +303,18 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
this.data_rendered = true;
|
||||
}
|
||||
|
||||
render_report_links() {
|
||||
let parent = this.transactions_area;
|
||||
if (this.data.reports && this.data.reports.length) {
|
||||
$(frappe.render_template('report_links', this.data))
|
||||
.appendTo(parent);
|
||||
// bind reports
|
||||
parent.find(".report-link").on('click', (e) => {
|
||||
this.open_report($(e.target).parent());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
open_report($link) {
|
||||
let report = $link.attr('data-report');
|
||||
|
||||
|
|
@ -318,6 +322,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
? (this.data.non_standard_fieldnames[report] || this.data.fieldname)
|
||||
: this.data.fieldname;
|
||||
|
||||
frappe.provide('frappe.route_options');
|
||||
frappe.route_options[fieldname] = this.frm.doc.name;
|
||||
frappe.set_route("query-report", report);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,4 +261,14 @@ export default class BulkOperations {
|
|||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
export(doctype, docnames) {
|
||||
frappe.require('data_import_tools.bundle.js', () => {
|
||||
const data_exporter = new frappe.data_import.DataExporter(doctype, 'Insert New Records');
|
||||
data_exporter.dialog.set_value('export_records', 'by_filter');
|
||||
data_exporter.filter_group.add_filters_to_filter_group(
|
||||
[[doctype, "name", "in", docnames, false]]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
|
||||
if (
|
||||
!this.settings.hide_name_column &&
|
||||
this.meta.title_field &&
|
||||
this.meta.title_field !== 'name'
|
||||
) {
|
||||
this.columns.push({
|
||||
|
|
@ -867,8 +868,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
filters: this.get_filters_for_args()
|
||||
}).then(total_count => {
|
||||
this.total_count = total_count || current_count;
|
||||
this.count_without_children = count_without_children !== current_count ? count_without_children : undefined;
|
||||
let str = __('{0} of {1}', [current_count, this.total_count]);
|
||||
if (count_without_children !== current_count) {
|
||||
if (this.count_without_children) {
|
||||
str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]);
|
||||
}
|
||||
return str;
|
||||
|
|
@ -1730,11 +1732,25 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
};
|
||||
};
|
||||
|
||||
const bulk_export = () => {
|
||||
return {
|
||||
label: __("Export"),
|
||||
action: () => {
|
||||
const docnames = this.get_checked_items(true);
|
||||
|
||||
bulk_operations.export(doctype, docnames);
|
||||
},
|
||||
standard: true
|
||||
};
|
||||
};
|
||||
|
||||
// bulk edit
|
||||
if (has_editable_fields(doctype)) {
|
||||
actions_menu_items.push(bulk_edit());
|
||||
}
|
||||
|
||||
actions_menu_items.push(bulk_export());
|
||||
|
||||
// bulk assignment
|
||||
actions_menu_items.push(bulk_assignment());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-cloak @drop.prevent="import_data" @dragover.prevent>
|
||||
<div class="page-form">
|
||||
<div class="filter-list">
|
||||
<div class="tag-filters-area">
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
</div>
|
||||
<div class="result-list">
|
||||
<div class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" @click="route_to_request_detail(request.uuid)">
|
||||
<div class="list-row-container" v-for="(request, index) in paginated(sorted(filtered(requests)))" :key="index" @click="route_to_request_detail(request)">
|
||||
<div class="level list-row small">
|
||||
<div class="level-left ellipsis">
|
||||
<div class="list-row-col ellipsis list-subject level ">
|
||||
|
|
@ -71,15 +71,16 @@
|
|||
</div>
|
||||
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
|
||||
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
|
||||
<p>{{ __("Recorder is Inactive") }}</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
|
||||
<p>{{ __("Recorder is Inactive.") }}</p>
|
||||
<p>{{ __("Start recording or drag & drop a previously exported data file to view it.") }}</p>
|
||||
</div>
|
||||
<div class="msg-box no-border" v-if="status.status == 'Active'" >
|
||||
<p>{{ __("No Requests found") }}</p>
|
||||
<p>{{ __("Go make some noise") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="requests.length != 0" class="list-paging-area">
|
||||
<div v-else class="list-paging-area">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="btn-group btn-group-paging">
|
||||
|
|
@ -144,7 +145,7 @@ export default {
|
|||
frappe.set_route("recorder");
|
||||
this.clear();
|
||||
});
|
||||
|
||||
this.$root.page.add_menu_item("Export data", () => this.export_data());
|
||||
},
|
||||
computed: {
|
||||
pages: function() {
|
||||
|
|
@ -239,8 +240,36 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
route_to_request_detail(id) {
|
||||
this.$router.push({name: 'request-detail', params: {id}});
|
||||
route_to_request_detail(request) {
|
||||
this.$router.push({name: 'request-detail', params: {request, id: request.uuid}});
|
||||
},
|
||||
export_data: function() {
|
||||
if (!this.requests) {
|
||||
return;
|
||||
}
|
||||
frappe.call("frappe.recorder.export_data")
|
||||
.then((r) => {
|
||||
const data = r.message;
|
||||
const filename = `${data[0]['uuid']}..${data[data.length -1]['uuid']}.json`
|
||||
|
||||
const el = document.createElement('a');
|
||||
el.setAttribute('href', 'data:application/json,' + encodeURIComponent(JSON.stringify(data)));
|
||||
el.setAttribute('download', filename);
|
||||
el.click();
|
||||
});
|
||||
},
|
||||
import_data: function(e) {
|
||||
if (this.requests.length > 0) {
|
||||
// don't replace existing capture
|
||||
return;
|
||||
}
|
||||
const request_file = e.dataTransfer.files[0];
|
||||
|
||||
const file_reader = new FileReader();
|
||||
file_reader.readAsText(request_file, 'UTF-8');
|
||||
file_reader.onload = ({target: {result}}) => {
|
||||
this.requests = JSON.parse(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -283,14 +283,21 @@ export default {
|
|||
label: __('Recorder'),
|
||||
route: '/app/recorder'
|
||||
});
|
||||
frappe.call({
|
||||
method: "frappe.recorder.get",
|
||||
args: {
|
||||
uuid: this.$route.params.id
|
||||
}
|
||||
}).then( r => {
|
||||
this.request = r.message
|
||||
});
|
||||
}
|
||||
|
||||
const request = this.$route.params.request;
|
||||
if (request.headers || request.form_dict || request.calls) {
|
||||
// complete request data passed as parameter.
|
||||
this.request = request;
|
||||
} else {
|
||||
frappe.call({
|
||||
method: "frappe.recorder.get",
|
||||
args: {
|
||||
uuid: request.uuid
|
||||
}
|
||||
}).then( r => {
|
||||
this.request = r.message
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -854,6 +854,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
}
|
||||
};
|
||||
|
||||
if (this.raw_data.add_total_row) {
|
||||
this.$page.find('.layout-main-section').css('--report-total-height', '310px');
|
||||
}
|
||||
|
||||
if (this.report_settings.get_datatable_options) {
|
||||
datatable_options = this.report_settings.get_datatable_options(datatable_options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1401,7 +1401,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
];
|
||||
|
||||
if (this.total_count > args.page_length) {
|
||||
if (this.total_count > this.count_without_children || args.page_length) {
|
||||
fields.push({
|
||||
fieldtype: 'Check',
|
||||
fieldname: 'export_all_rows',
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
margin-bottom: var(--margin-sm);
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
.row:first-child {
|
||||
.form-documents:first-of-type .row:first-child {
|
||||
.form-link-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,8 +84,9 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.layout-main-section .frappe-card {
|
||||
.layout-main-section {
|
||||
--report-filter-height: 0px;
|
||||
--report-total-height: 275px;
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
height: calc(100vh - var(--report-filter-height) - 205px);
|
||||
|
||||
.dt-scrollable {
|
||||
height: calc(100vh - var(--report-filter-height) - 275px);
|
||||
height: calc(100vh - var(--report-filter-height) - var(--report-total-height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,13 @@ def get(uuid=None, *args, **kwargs):
|
|||
return result
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@do_not_record
|
||||
@administrator_only
|
||||
def export_data(*args, **kwargs):
|
||||
return list(frappe.cache().hgetall(RECORDER_REQUEST_HASH).values())
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@do_not_record
|
||||
@administrator_only
|
||||
|
|
|
|||
12
frappe/tests/data/sample_svg.svg
Normal file
12
frappe/tests/data/sample_svg.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||
<title>Artboard</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Artboard" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="frappe" transform="translate(3.000000, 1.000000)" fill="#0089FF" fill-rule="nonzero">
|
||||
<polygon id="Path" points="9.360932 0 0 0 0 2.46232 9.360932 2.46232"></polygon>
|
||||
<polygon id="Path" points="0 6.281996 0 14 2.98788 14 2.98788 8.74846 8.740172 8.74846 8.740172 6.281996"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 751 B |
|
|
@ -227,3 +227,28 @@ class TestDocument(unittest.TestCase):
|
|||
self.assertEqual(frappe.db.get_value("Currency", d.name), d.name)
|
||||
|
||||
frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1)
|
||||
|
||||
def test_get_formatted(self):
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': 'Test Formatted',
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'fields': [
|
||||
{'label': 'Currency', 'fieldname': 'currency', 'reqd': 1, 'fieldtype': 'Currency'},
|
||||
]
|
||||
}).insert()
|
||||
|
||||
frappe.delete_doc_if_exists("Currency", "INR", 1)
|
||||
|
||||
d = frappe.get_doc({
|
||||
'doctype': 'Currency',
|
||||
'currency_name': 'INR',
|
||||
'symbol': '₹',
|
||||
}).insert()
|
||||
|
||||
d = frappe.get_doc({
|
||||
'doctype': 'Test Formatted',
|
||||
'currency': 100000
|
||||
})
|
||||
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
|
||||
|
|
@ -92,6 +92,9 @@ class TestFmtMoney(unittest.TestCase):
|
|||
self.assertEqual(fmt_money(1000.456), "1.000,456")
|
||||
frappe.db.set_default("currency_precision", "")
|
||||
|
||||
def test_custom_fmt_money_format(self):
|
||||
self.assertEqual(fmt_money(100000, format="#,###.##"), '100,000.00')
|
||||
|
||||
if __name__=="__main__":
|
||||
frappe.connect()
|
||||
unittest.main()
|
||||
|
|
@ -17,9 +17,9 @@ class TestFormatter(unittest.TestCase):
|
|||
frappe.db.set_default("currency", 'INR')
|
||||
|
||||
# if currency field is not passed then default currency should be used.
|
||||
self.assertEqual(format(100, df, doc), '₹ 100.00')
|
||||
self.assertEqual(format(100000, df, doc, format="#,###.##"), '₹ 100,000.00')
|
||||
|
||||
doc.currency = 'USD'
|
||||
self.assertEqual(format(100, df, doc), "$ 100.00")
|
||||
self.assertEqual(format(100000, df, doc, format="#,###.##"), "$ 100,000.00")
|
||||
|
||||
frappe.db.set_default("currency", None)
|
||||
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.desk.search import search_link
|
||||
from frappe.desk.search import search_widget
|
||||
from frappe.desk.search import search_link, search_widget, get_names_for_mentions
|
||||
|
||||
|
||||
class TestSearch(unittest.TestCase):
|
||||
|
|
@ -47,6 +46,23 @@ class TestSearch(unittest.TestCase):
|
|||
search_link, 'DocType', 'Customer', query=None, filters=None,
|
||||
page_length=20, searchfield=';')
|
||||
|
||||
def test_only_enabled_in_mention(self):
|
||||
email = 'test_disabled_user_in_mentions@example.com'
|
||||
frappe.delete_doc('User', email)
|
||||
if not frappe.db.exists('User', email):
|
||||
user = frappe.new_doc('User')
|
||||
user.update({
|
||||
'email' : email,
|
||||
'first_name' : email.split("@")[0],
|
||||
'enabled' : False,
|
||||
'allowed_in_mentions' : True,
|
||||
})
|
||||
# saved when roles are added
|
||||
user.add_roles('System Manager',)
|
||||
|
||||
names_for_mention = [user.get('id') for user in get_names_for_mentions('')]
|
||||
self.assertNotIn(email, names_for_mention)
|
||||
|
||||
def test_link_field_order(self):
|
||||
# Making a request to the search_link with the tree doctype
|
||||
search_link(doctype=self.tree_doctype_name, txt='all', query=None,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,18 @@ def create_todo_records():
|
|||
"description": "this is fourth todo"
|
||||
}).insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_communication_records():
|
||||
if frappe.db.get_all('Communication', {'subject': 'Test Form Communication 1'}):
|
||||
return
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "Communication",
|
||||
"recipients": "test@gmail.com",
|
||||
"subject": "Test Form Communication 1",
|
||||
"communication_date": frappe.utils.now_datetime(),
|
||||
}).insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_workflow():
|
||||
from frappe.workflow.doctype.workflow.test_workflow import create_todo_workflow
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.utils import formatdate, fmt_money, flt, cstr, cint, format_datetime
|
|||
from frappe.model.meta import get_field_currency, get_field_precision
|
||||
import re
|
||||
|
||||
def format_value(value, df=None, doc=None, currency=None, translated=False):
|
||||
def format_value(value, df=None, doc=None, currency=None, translated=False, format=None):
|
||||
'''Format value based on given fieldtype, document reference, currency reference.
|
||||
If docfield info (df) is not given, it will try and guess based on the datatype of the value'''
|
||||
if isinstance(df, str):
|
||||
|
|
@ -56,7 +56,7 @@ def format_value(value, df=None, doc=None, currency=None, translated=False):
|
|||
elif df.get("fieldtype") == "Currency":
|
||||
default_currency = frappe.db.get_default("currency")
|
||||
currency = currency or get_field_currency(df, doc) or default_currency
|
||||
return fmt_money(value, precision=get_field_precision(df, doc), currency=currency)
|
||||
return fmt_money(value, precision=get_field_precision(df, doc), currency=currency, format=format)
|
||||
|
||||
elif df.get("fieldtype") == "Float":
|
||||
precision = get_field_precision(df, doc)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue