@@ -0,0 +1,37 @@ | |||||
[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, | |||||
E124, # closing bracket, irritating while writing QB code | |||||
E131, # continuation line unaligned for hanging indent | |||||
E123, # closing bracket does not match indentation of opening bracket's line | |||||
E101, # ensured by use of black | |||||
max-line-length = 200 | |||||
exclude=.github/helper/semgrep_rules |
@@ -0,0 +1,18 @@ | |||||
# Since version 2.23 (released in August 2019), git-blame has a feature | |||||
# to ignore or bypass certain commits. | |||||
# | |||||
# This file contains a list of commits that are not likely what you | |||||
# are looking for in a blame, such as mass reformatting or renaming. | |||||
# You can set this file as a default ignore file for blame by running | |||||
# the following command. | |||||
# | |||||
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs | |||||
# sort and cleanup imports | |||||
4872c156974291f0c4c88f26033fef0b900ca995 | |||||
# old black formatting commit (from erpnext) | |||||
76c895a6c659356151433715a1efe9337e348c11 | |||||
# bulk formatting | |||||
b55d6e27af6bd274dfa47e66a3012ddec68ce798 |
@@ -0,0 +1,7 @@ | |||||
# This is a comment. | |||||
# Each line is a file pattern followed by one or more owners. | |||||
# These owners will be the default owners for everything in | |||||
# the repo. Unless a later match takes precedence. | |||||
* @ruchamahabal |
@@ -0,0 +1,73 @@ | |||||
[flake8] | |||||
ignore = | |||||
B007, | |||||
B009, | |||||
B010, | |||||
B950, | |||||
E101, | |||||
E111, | |||||
E114, | |||||
E116, | |||||
E117, | |||||
E121, | |||||
E122, | |||||
E123, | |||||
E124, | |||||
E125, | |||||
E126, | |||||
E127, | |||||
E128, | |||||
E131, | |||||
E201, | |||||
E202, | |||||
E203, | |||||
E211, | |||||
E221, | |||||
E222, | |||||
E223, | |||||
E224, | |||||
E225, | |||||
E226, | |||||
E228, | |||||
E231, | |||||
E241, | |||||
E242, | |||||
E251, | |||||
E261, | |||||
E262, | |||||
E265, | |||||
E266, | |||||
E271, | |||||
E272, | |||||
E273, | |||||
E274, | |||||
E301, | |||||
E302, | |||||
E303, | |||||
E305, | |||||
E306, | |||||
E402, | |||||
E501, | |||||
E502, | |||||
E701, | |||||
E702, | |||||
E703, | |||||
E741, | |||||
F403, | |||||
W191, | |||||
W291, | |||||
W292, | |||||
W293, | |||||
W391, | |||||
W503, | |||||
W504, | |||||
E711, | |||||
E129, | |||||
F841, | |||||
E713, | |||||
E712, | |||||
B023 | |||||
max-line-length = 200 | |||||
exclude=.github/helper/semgrep_rules,test_*.py |
@@ -0,0 +1,46 @@ | |||||
import sys | |||||
import requests | |||||
from urllib.parse import urlparse | |||||
def uri_validator(x): | |||||
result = urlparse(x) | |||||
return all([result.scheme, result.netloc, result.path]) | |||||
def docs_link_exists(body): | |||||
for line in body.splitlines(): | |||||
for word in line.split(): | |||||
if word.startswith('http') and uri_validator(word): | |||||
parsed_url = urlparse(word) | |||||
if parsed_url.netloc == "github.com": | |||||
parts = parsed_url.path.split('/') | |||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] == "hrms": | |||||
return True | |||||
elif parsed_url.netloc == "frappehr.com": | |||||
return True | |||||
if __name__ == "__main__": | |||||
pr = sys.argv[1] | |||||
response = requests.get("https://api.github.com/repos/frappe/hrms/pulls/{}".format(pr)) | |||||
if response.ok: | |||||
payload = response.json() | |||||
title = (payload.get("title") or "").lower().strip() | |||||
head_sha = (payload.get("head") or {}).get("sha") | |||||
body = (payload.get("body") or "").lower() | |||||
if (title.startswith("feat") | |||||
and head_sha | |||||
and "no-docs" not in body | |||||
and "backport" not in body | |||||
): | |||||
if docs_link_exists(body): | |||||
print("Documentation Link Found. You're Awesome! 🎉") | |||||
else: | |||||
print("Documentation Link Not Found! ⚠️") | |||||
sys.exit(1) | |||||
else: | |||||
print("Skipping documentation checks... 🏃") |
@@ -0,0 +1,52 @@ | |||||
#!/bin/bash | |||||
set -e | |||||
cd ~ || exit | |||||
sudo apt-get -y install redis-server libcups2-dev -qq | |||||
pip install frappe-bench | |||||
git clone https://github.com/frappe/frappe --branch "$BRANCH_TO_CLONE" --depth 1 | |||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench | |||||
mkdir ~/frappe-bench/sites/test_site | |||||
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ | |||||
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 USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" | |||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe" | |||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'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" | |||||
install_whktml() { | |||||
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_whktml & | |||||
cd ~/frappe-bench || exit | |||||
sed -i 's/watch:/# watch:/g' Procfile | |||||
sed -i 's/schedule:/# schedule:/g' Procfile | |||||
sed -i 's/socketio:/# socketio:/g' Procfile | |||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile | |||||
bench get-app payments | |||||
bench get-app https://github.com/frappe/erpnext --branch "$BRANCH_TO_CLONE" --resolve-deps | |||||
bench setup requirements --dev | |||||
bench start &> bench_run_logs.txt & | |||||
CI=Yes bench build --app frappe & | |||||
bench --site test_site reinstall --yes | |||||
bench get-app hrms "${GITHUB_WORKSPACE}" | |||||
bench --site test_site install-app hrms | |||||
bench setup requirements --dev |
@@ -0,0 +1,16 @@ | |||||
{ | |||||
"db_host": "127.0.0.1", | |||||
"db_port": 3306, | |||||
"db_name": "test_frappe", | |||||
"db_password": "test_frappe", | |||||
"auto_email_id": "test@example.com", | |||||
"mail_server": "smtp.example.com", | |||||
"mail_login": "test@example.com", | |||||
"mail_password": "test", | |||||
"admin_password": "admin", | |||||
"root_login": "root", | |||||
"root_password": "travis", | |||||
"host_name": "http://test_site:8000", | |||||
"install_apps": ["payments", "erpnext"], | |||||
"throttle_user_limit": 100 | |||||
} |
@@ -0,0 +1,60 @@ | |||||
import re | |||||
import sys | |||||
errors_encounter = 0 | |||||
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)") | |||||
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") | |||||
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") | |||||
f_string_pattern = re.compile(r"_\(f[\"']") | |||||
starts_with_f_pattern = re.compile(r"_\(f") | |||||
# skip first argument | |||||
files = sys.argv[1:] | |||||
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))] | |||||
for _file in files_to_scan: | |||||
with open(_file, 'r') as f: | |||||
print(f'Checking: {_file}') | |||||
file_lines = f.readlines() | |||||
for line_number, line in enumerate(file_lines, 1): | |||||
if 'frappe-lint: disable-translate' in line: | |||||
continue | |||||
start_matches = start_pattern.search(line) | |||||
if start_matches: | |||||
starts_with_f = starts_with_f_pattern.search(line) | |||||
if starts_with_f: | |||||
has_f_string = f_string_pattern.search(line) | |||||
if has_f_string: | |||||
errors_encounter += 1 | |||||
print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}') | |||||
continue | |||||
else: | |||||
continue | |||||
match = pattern.search(line) | |||||
error_found = False | |||||
if not match and line.endswith((',\n', '[\n')): | |||||
# concat remaining text to validate multiline pattern | |||||
line = "".join(file_lines[line_number - 1:]) | |||||
line = line[start_matches.start() + 1:] | |||||
match = pattern.match(line) | |||||
if not match: | |||||
error_found = True | |||||
print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}') | |||||
if not error_found and not words_pattern.search(line): | |||||
error_found = True | |||||
print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}') | |||||
if error_found: | |||||
errors_encounter += 1 | |||||
if errors_encounter > 0: | |||||
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.') | |||||
sys.exit(1) | |||||
else: | |||||
print('\nGood To Go!') |
@@ -0,0 +1,4 @@ | |||||
# Any python files modifed but no test files modified | |||||
add-test-cases: | |||||
- any: ['hrms/**/*.py'] | |||||
all: ['!hrms/**/test*.py'] |
@@ -0,0 +1,116 @@ | |||||
name: CI | |||||
on: | |||||
push: | |||||
branches: [develop, version-14-hotfix, version-14] | |||||
paths-ignore: | |||||
- "**.css" | |||||
- "**.js" | |||||
- "**.md" | |||||
- "**.html" | |||||
- "**.csv" | |||||
pull_request: | |||||
paths-ignore: | |||||
- "**.css" | |||||
- "**.js" | |||||
- "**.md" | |||||
- "**.html" | |||||
- "**.csv" | |||||
schedule: | |||||
# Run everday at midnight UTC / 5:30 IST | |||||
- cron: "0 0 * * *" | |||||
env: | |||||
HR_BRANCH: ${{ github.base_ref || github.ref_name }} | |||||
concurrency: | |||||
group: develop-${{ github.event.number }} | |||||
cancel-in-progress: true | |||||
jobs: | |||||
tests: | |||||
runs-on: ubuntu-latest | |||||
timeout-minutes: 60 | |||||
strategy: | |||||
fail-fast: false | |||||
name: Server | |||||
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 | |||||
steps: | |||||
- name: Clone | |||||
uses: actions/checkout@v2 | |||||
- name: Setup Python | |||||
uses: actions/setup-python@v2 | |||||
with: | |||||
python-version: '3.10' | |||||
- name: Setup Node | |||||
uses: actions/setup-node@v2 | |||||
with: | |||||
node-version: 14 | |||||
check-latest: true | |||||
- name: Add to Hosts | |||||
run: echo "127.0.0.1 test_site" | 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: Install | |||||
run: | | |||||
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh | |||||
env: | |||||
BRANCH_TO_CLONE: ${{ env.HR_BRANCH }} | |||||
- name: Run Tests | |||||
run: cd ~/frappe-bench/ && bench --site test_site run-tests --app hrms --coverage | |||||
env: | |||||
TYPE: server | |||||
- name: Upload coverage data | |||||
uses: codecov/codecov-action@v2 | |||||
with: | |||||
fail_ci_if_error: true | |||||
files: /home/runner/frappe-bench/sites/coverage.xml | |||||
verbose: true |
@@ -0,0 +1,25 @@ | |||||
name: 'Documentation Required' | |||||
on: | |||||
pull_request: | |||||
types: [ opened, synchronize, reopened, edited ] | |||||
jobs: | |||||
build: | |||||
runs-on: ubuntu-latest | |||||
timeout-minutes: 10 | |||||
steps: | |||||
- name: 'Setup Environment' | |||||
uses: actions/setup-python@v2 | |||||
with: | |||||
python-version: 3.8 | |||||
- name: 'Clone repo' | |||||
uses: actions/checkout@v2 | |||||
- name: Validate Docs | |||||
env: | |||||
PR_NUMBER: ${{ github.event.number }} | |||||
run: | | |||||
pip install requests --quiet | |||||
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER |
@@ -0,0 +1,12 @@ | |||||
name: "Pull Request Labeler" | |||||
on: | |||||
pull_request_target: | |||||
types: [opened, reopened] | |||||
jobs: | |||||
triage: | |||||
runs-on: ubuntu-latest | |||||
steps: | |||||
- uses: actions/labeler@v4 | |||||
with: | |||||
repo-token: "${{ secrets.GITHUB_TOKEN }}" |
@@ -0,0 +1,29 @@ | |||||
name: Linters | |||||
on: | |||||
pull_request: { } | |||||
jobs: | |||||
linters: | |||||
name: linters | |||||
runs-on: ubuntu-latest | |||||
steps: | |||||
- uses: actions/checkout@v2 | |||||
- name: Set up Python 3.10 | |||||
uses: actions/setup-python@v2 | |||||
with: | |||||
python-version: '3.10' | |||||
- name: Install and Run Pre-commit | |||||
uses: pre-commit/action@v2.0.3 | |||||
- name: Download Semgrep rules | |||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules | |||||
- name: Download semgrep | |||||
run: pip install semgrep==0.97.0 | |||||
- name: Run Semgrep rules | |||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness |
@@ -0,0 +1,32 @@ | |||||
name: Generate Semantic Release | |||||
on: | |||||
workflow_dispatch: | |||||
push: | |||||
branches: | |||||
- version-14 | |||||
jobs: | |||||
release: | |||||
name: Release | |||||
runs-on: ubuntu-latest | |||||
steps: | |||||
- name: Checkout Entire Repository | |||||
uses: actions/checkout@v2 | |||||
with: | |||||
fetch-depth: 0 | |||||
persist-credentials: false | |||||
- name: Setup Node.js | |||||
uses: actions/setup-node@v2 | |||||
with: | |||||
node-version: 18 | |||||
- name: Setup dependencies | |||||
run: | | |||||
npm install @semantic-release/git @semantic-release/exec --no-save | |||||
- name: Create Release | |||||
env: | |||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |||||
GIT_AUTHOR_NAME: "Frappe PR Bot" | |||||
GIT_AUTHOR_EMAIL: "developers@frappe.io" | |||||
GIT_COMMITTER_NAME: "Frappe PR Bot" | |||||
GIT_COMMITTER_EMAIL: "developers@frappe.io" | |||||
run: npx semantic-release |
@@ -0,0 +1,9 @@ | |||||
.DS_Store | |||||
*.pyc | |||||
*.egg-info | |||||
*.swp | |||||
tags | |||||
hrms/docs/current | |||||
node_modules/ | |||||
dist/ | |||||
__pycache__/ |
@@ -0,0 +1,56 @@ | |||||
pull_request_rules: | |||||
- name: Auto-close PRs on stable branch | |||||
conditions: | |||||
- and: | |||||
- author!=ruchamahabal | |||||
- author!=saurabh6790 | |||||
- author!=frappe-pr-bot | |||||
- author!=mergify[bot] | |||||
- base=version-14 | |||||
actions: | |||||
close: | |||||
comment: | |||||
message: | | |||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch or the develop branch. | |||||
- name: Automatic merge on CI success and review | |||||
conditions: | |||||
- label!=dont-merge | |||||
- label!=squash | |||||
- "#approved-reviews-by>=1" | |||||
actions: | |||||
merge: | |||||
method: merge | |||||
- name: Automatic squash on CI success and review | |||||
conditions: | |||||
- label!=dont-merge | |||||
- label=squash | |||||
- "#approved-reviews-by>=1" | |||||
actions: | |||||
merge: | |||||
method: squash | |||||
commit_message_template: | | |||||
{{ title }} (#{{ number }}) | |||||
{{ body }} | |||||
- name: backport to develop | |||||
conditions: | |||||
- label="backport develop" | |||||
actions: | |||||
backport: | |||||
branches: | |||||
- develop | |||||
assignees: | |||||
- "{{ author }}" | |||||
- name: backport to version-14-hotfix | |||||
conditions: | |||||
- label="backport version-14-hotfix" | |||||
actions: | |||||
backport: | |||||
branches: | |||||
- version-14-hotfix | |||||
assignees: | |||||
- "{{ author }}" |
@@ -0,0 +1,44 @@ | |||||
exclude: 'node_modules|.git' | |||||
default_stages: [commit] | |||||
fail_fast: false | |||||
repos: | |||||
- repo: https://github.com/pre-commit/pre-commit-hooks | |||||
rev: v4.0.1 | |||||
hooks: | |||||
- id: trailing-whitespace | |||||
files: "hrms.*" | |||||
exclude: ".*json$|.*txt$|.*csv|.*md" | |||||
- id: check-yaml | |||||
- id: no-commit-to-branch | |||||
args: ['--branch', 'develop'] | |||||
- id: check-merge-conflict | |||||
- id: check-ast | |||||
- repo: https://github.com/PyCQA/flake8 | |||||
rev: 5.0.4 | |||||
hooks: | |||||
- id: flake8 | |||||
additional_dependencies: [ | |||||
'flake8-bugbear', | |||||
] | |||||
args: ['--config', '.github/helper/.flake8_strict'] | |||||
exclude: ".*setup.py$" | |||||
- repo: https://github.com/adityahase/black | |||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 | |||||
hooks: | |||||
- id: black | |||||
additional_dependencies: ['click==8.0.4'] | |||||
- repo: https://github.com/PyCQA/isort | |||||
rev: 5.12.0 | |||||
hooks: | |||||
- id: isort | |||||
exclude: ".*setup.py$" | |||||
ci: | |||||
autoupdate_schedule: weekly | |||||
skip: [] | |||||
submodules: false |
@@ -0,0 +1,24 @@ | |||||
{ | |||||
"branches": ["version-14"], | |||||
"plugins": [ | |||||
"@semantic-release/commit-analyzer", { | |||||
"preset": "angular", | |||||
"releaseRules": [ | |||||
{"breaking": true, "release": false} | |||||
] | |||||
}, | |||||
"@semantic-release/release-notes-generator", | |||||
[ | |||||
"@semantic-release/exec", { | |||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" hrms/__init__.py' | |||||
} | |||||
], | |||||
[ | |||||
"@semantic-release/git", { | |||||
"assets": ["hrms/__init__.py"], | |||||
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" | |||||
} | |||||
], | |||||
"@semantic-release/github" | |||||
] | |||||
} |
@@ -0,0 +1 @@ | |||||
hrms/patches/post_install/ |
@@ -0,0 +1,18 @@ | |||||
include MANIFEST.in | |||||
include requirements.txt | |||||
include *.json | |||||
include *.md | |||||
include *.py | |||||
include *.txt | |||||
recursive-include hrms *.css | |||||
recursive-include hrms *.csv | |||||
recursive-include hrms *.html | |||||
recursive-include hrms *.ico | |||||
recursive-include hrms *.js | |||||
recursive-include hrms *.json | |||||
recursive-include hrms *.md | |||||
recursive-include hrms *.png | |||||
recursive-include hrms *.py | |||||
recursive-include hrms *.svg | |||||
recursive-include hrms *.txt | |||||
recursive-exclude hrms *.pyc |
@@ -0,0 +1,66 @@ | |||||
<div align="center" markdown="1"> | |||||
<img src=".github/frappe-hr-logo.png" alt="Frappe HR logo" width="170" style="max-width: 100%;"/> | |||||
Open Source, modern, and easy-to-use HR and Payroll Software for all organizations. | |||||
[](https://github.com/frappe/hrms/actions/workflows/ci.yml) | |||||
[](https://codecov.io/gh/frappe/hrms) | |||||
[https://frappehr.com](https://frappehr.com) | |||||
</div> | |||||
## Introduction | |||||
Frappe HR has everything you need to drive excellence within the company. It's a complete HRMS solution with over 13 different modules right from Employee Management, Onboarding, Leaves, to Payroll, Taxation, and more! | |||||
 | |||||
## Key Features | |||||
- Employee Management | |||||
- Employee Lifecycle | |||||
- Leave and Attendance | |||||
- Shift Management | |||||
- Expense Claims and Advances | |||||
- Hiring | |||||
- Performance Management | |||||
- Fleet Management | |||||
- Training | |||||
- Payroll | |||||
- Taxation | |||||
- Compensation | |||||
- Analytics | |||||
## Installation | |||||
1. [Install bench](https://github.com/frappe/bench). | |||||
2. [Install ERPNext](https://github.com/frappe/bench#installation). | |||||
3. Once ERPNext is installed, add the hrms app to your bench by running | |||||
```sh | |||||
$ bench get-app hrms | |||||
``` | |||||
4. After that, you can install the hrms app on the required site by running | |||||
```sh | |||||
$ bench --site sitename install-app hrms | |||||
``` | |||||
## Learning and Community | |||||
1. [Documentation](https://frappehr.com/docs) - Extensive documentation for Frappe HR. | |||||
2. [User Forum](https://discuss.erpnext.com/) - Engage with the community of ERPNext users and service providers. | |||||
3. [Telegram Group](https://t.me/frappehr) - Get instant help from the community of users. | |||||
## Contribute | |||||
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines) - [Create an issue](https://github.com/frappe/hrms/issues/new) | |||||
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) | |||||
## License | |||||
GNU GPL V3. (See [license.txt](license.txt) for more information). | |||||
The HR code is licensed as GNU General Public License (v3) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors. |
@@ -0,0 +1,24 @@ | |||||
codecov: | |||||
require_ci_to_pass: yes | |||||
coverage: | |||||
status: | |||||
project: | |||||
default: | |||||
target: auto | |||||
threshold: 0.5% | |||||
patch: | |||||
default: | |||||
target: 85% | |||||
threshold: 0% | |||||
base: auto | |||||
branches: | |||||
- develop | |||||
if_ci_failed: ignore | |||||
only_pulls: true | |||||
comment: | |||||
layout: "diff, files" | |||||
require_changes: true | |||||
@@ -0,0 +1 @@ | |||||
__version__ = "14.3.0" |
@@ -0,0 +1,5 @@ | |||||
from frappe import _ | |||||
def get_data(): | |||||
return [{"module_name": "HRMS", "type": "module", "label": _("HRMS")}] |
@@ -0,0 +1,11 @@ | |||||
""" | |||||
Configuration for docs | |||||
""" | |||||
# source_link = "https://github.com/[org_name]/hrms" | |||||
# headline = "App that does everything" | |||||
# sub_heading = "Yes, you got that right the first time, everything" | |||||
def get_context(context): | |||||
context.brand_html = "HRMS" |
@@ -0,0 +1,198 @@ | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.desk.form import assign_to | |||||
from frappe.model.document import Document | |||||
from frappe.utils import add_days, flt, unique | |||||
from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee | |||||
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday | |||||
class EmployeeBoardingController(Document): | |||||
""" | |||||
Create the project and the task for the boarding process | |||||
Assign to the concerned person and roles as per the onboarding/separation template | |||||
""" | |||||
def validate(self): | |||||
# remove the task if linked before submitting the form | |||||
if self.amended_from: | |||||
for activity in self.activities: | |||||
activity.task = "" | |||||
def on_submit(self): | |||||
# create the project for the given employee onboarding | |||||
project_name = _(self.doctype) + " : " | |||||
if self.doctype == "Employee Onboarding": | |||||
project_name += self.job_applicant | |||||
else: | |||||
project_name += self.employee | |||||
project = frappe.get_doc( | |||||
{ | |||||
"doctype": "Project", | |||||
"project_name": project_name, | |||||
"expected_start_date": self.date_of_joining | |||||
if self.doctype == "Employee Onboarding" | |||||
else self.resignation_letter_date, | |||||
"department": self.department, | |||||
"company": self.company, | |||||
} | |||||
).insert(ignore_permissions=True, ignore_mandatory=True) | |||||
self.db_set("project", project.name) | |||||
self.db_set("boarding_status", "Pending") | |||||
self.reload() | |||||
self.create_task_and_notify_user() | |||||
def create_task_and_notify_user(self): | |||||
# create the task for the given project and assign to the concerned person | |||||
holiday_list = self.get_holiday_list() | |||||
for activity in self.activities: | |||||
if activity.task: | |||||
continue | |||||
dates = self.get_task_dates(activity, holiday_list) | |||||
task = frappe.get_doc( | |||||
{ | |||||
"doctype": "Task", | |||||
"project": self.project, | |||||
"subject": activity.activity_name + " : " + self.employee_name, | |||||
"description": activity.description, | |||||
"department": self.department, | |||||
"company": self.company, | |||||
"task_weight": activity.task_weight, | |||||
"exp_start_date": dates[0], | |||||
"exp_end_date": dates[1], | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
activity.db_set("task", task.name) | |||||
users = [activity.user] if activity.user else [] | |||||
if activity.role: | |||||
user_list = frappe.db.sql_list( | |||||
""" | |||||
SELECT | |||||
DISTINCT(has_role.parent) | |||||
FROM | |||||
`tabHas Role` has_role | |||||
LEFT JOIN `tabUser` user | |||||
ON has_role.parent = user.name | |||||
WHERE | |||||
has_role.parenttype = 'User' | |||||
AND user.enabled = 1 | |||||
AND has_role.role = %s | |||||
""", | |||||
activity.role, | |||||
) | |||||
users = unique(users + user_list) | |||||
if "Administrator" in users: | |||||
users.remove("Administrator") | |||||
# assign the task the users | |||||
if users: | |||||
self.assign_task_to_users(task, users) | |||||
def get_holiday_list(self): | |||||
if self.doctype == "Employee Separation": | |||||
return get_holiday_list_for_employee(self.employee) | |||||
else: | |||||
if self.employee: | |||||
return get_holiday_list_for_employee(self.employee) | |||||
else: | |||||
if not self.holiday_list: | |||||
frappe.throw(_("Please set the Holiday List."), frappe.MandatoryError) | |||||
else: | |||||
return self.holiday_list | |||||
def get_task_dates(self, activity, holiday_list): | |||||
start_date = end_date = None | |||||
if activity.begin_on is not None: | |||||
start_date = add_days(self.boarding_begins_on, activity.begin_on) | |||||
start_date = self.update_if_holiday(start_date, holiday_list) | |||||
if activity.duration is not None: | |||||
end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration) | |||||
end_date = self.update_if_holiday(end_date, holiday_list) | |||||
return [start_date, end_date] | |||||
def update_if_holiday(self, date, holiday_list): | |||||
while is_holiday(holiday_list, date): | |||||
date = add_days(date, 1) | |||||
return date | |||||
def assign_task_to_users(self, task, users): | |||||
for user in users: | |||||
args = { | |||||
"assign_to": [user], | |||||
"doctype": task.doctype, | |||||
"name": task.name, | |||||
"description": task.description or task.subject, | |||||
"notify": self.notify_users_by_email, | |||||
} | |||||
assign_to.add(args) | |||||
def on_cancel(self): | |||||
# delete task project | |||||
project = self.project | |||||
for task in frappe.get_all("Task", filters={"project": project}): | |||||
frappe.delete_doc("Task", task.name, force=1) | |||||
frappe.delete_doc("Project", project, force=1) | |||||
self.db_set("project", "") | |||||
for activity in self.activities: | |||||
activity.db_set("task", "") | |||||
frappe.msgprint( | |||||
_("Linked Project {} and Tasks deleted.").format(project), alert=True, indicator="blue" | |||||
) | |||||
@frappe.whitelist() | |||||
def get_onboarding_details(parent, parenttype): | |||||
return frappe.get_all( | |||||
"Employee Boarding Activity", | |||||
fields=[ | |||||
"activity_name", | |||||
"role", | |||||
"user", | |||||
"required_for_employee_creation", | |||||
"description", | |||||
"task_weight", | |||||
"begin_on", | |||||
"duration", | |||||
], | |||||
filters={"parent": parent, "parenttype": parenttype}, | |||||
order_by="idx", | |||||
) | |||||
def update_employee_boarding_status(project, event=None): | |||||
employee_onboarding = frappe.db.exists("Employee Onboarding", {"project": project.name}) | |||||
employee_separation = frappe.db.exists("Employee Separation", {"project": project.name}) | |||||
if not (employee_onboarding or employee_separation): | |||||
return | |||||
status = "Pending" | |||||
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0: | |||||
status = "In Process" | |||||
elif flt(project.percent_complete) == 100.0: | |||||
status = "Completed" | |||||
if employee_onboarding: | |||||
frappe.db.set_value("Employee Onboarding", employee_onboarding, "boarding_status", status) | |||||
elif employee_separation: | |||||
frappe.db.set_value("Employee Separation", employee_separation, "boarding_status", status) | |||||
def update_task(task, event=None): | |||||
if task.project and not task.flags.from_project: | |||||
update_employee_boarding_status(frappe.get_cached_doc("Project", task.project)) |
@@ -0,0 +1,275 @@ | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.utils import add_days, add_months, comma_sep, getdate, today | |||||
from erpnext.setup.doctype.employee.employee import get_all_employee_emails, get_employee_email | |||||
from hrms.hr.utils import get_holidays_for_employee | |||||
# ----------------- | |||||
# HOLIDAY REMINDERS | |||||
# ----------------- | |||||
def send_reminders_in_advance_weekly(): | |||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders")) | |||||
frequency = frappe.db.get_single_value("HR Settings", "frequency") | |||||
if not (to_send_in_advance and frequency == "Weekly"): | |||||
return | |||||
send_advance_holiday_reminders("Weekly") | |||||
def send_reminders_in_advance_monthly(): | |||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders")) | |||||
frequency = frappe.db.get_single_value("HR Settings", "frequency") | |||||
if not (to_send_in_advance and frequency == "Monthly"): | |||||
return | |||||
send_advance_holiday_reminders("Monthly") | |||||
def send_advance_holiday_reminders(frequency): | |||||
"""Send Holiday Reminders in Advance to Employees | |||||
`frequency` (str): 'Weekly' or 'Monthly' | |||||
""" | |||||
if frequency == "Weekly": | |||||
start_date = getdate() | |||||
end_date = add_days(getdate(), 7) | |||||
elif frequency == "Monthly": | |||||
# Sent on 1st of every month | |||||
start_date = getdate() | |||||
end_date = add_months(getdate(), 1) | |||||
else: | |||||
return | |||||
employees = frappe.db.get_all("Employee", filters={"status": "Active"}, pluck="name") | |||||
for employee in employees: | |||||
holidays = get_holidays_for_employee( | |||||
employee, start_date, end_date, only_non_weekly=True, raise_exception=False | |||||
) | |||||
send_holidays_reminder_in_advance(employee, holidays) | |||||
def send_holidays_reminder_in_advance(employee, holidays): | |||||
if not holidays: | |||||
return | |||||
employee_doc = frappe.get_doc("Employee", employee) | |||||
employee_email = get_employee_email(employee_doc) | |||||
frequency = frappe.db.get_single_value("HR Settings", "frequency") | |||||
email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.") | |||||
frappe.sendmail( | |||||
recipients=[employee_email], | |||||
subject=_("Upcoming Holidays Reminder"), | |||||
template="holiday_reminder", | |||||
args=dict( | |||||
reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format( | |||||
employee_doc.get("first_name") | |||||
), | |||||
message=_("Below is the list of upcoming holidays for you:"), | |||||
advance_holiday_reminder=True, | |||||
holidays=holidays, | |||||
frequency=frequency[:-2], | |||||
), | |||||
header=email_header, | |||||
) | |||||
# ------------------ | |||||
# BIRTHDAY REMINDERS | |||||
# ------------------ | |||||
def send_birthday_reminders(): | |||||
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set.""" | |||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders")) | |||||
if not to_send: | |||||
return | |||||
employees_born_today = get_employees_who_are_born_today() | |||||
for company, birthday_persons in employees_born_today.items(): | |||||
employee_emails = get_all_employee_emails(company) | |||||
birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons] | |||||
recipients = list(set(employee_emails) - set(birthday_person_emails)) | |||||
reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons) | |||||
send_birthday_reminder(recipients, reminder_text, birthday_persons, message) | |||||
if len(birthday_persons) > 1: | |||||
# special email for people sharing birthdays | |||||
for person in birthday_persons: | |||||
person_email = person["user_id"] or person["personal_email"] or person["company_email"] | |||||
others = [d for d in birthday_persons if d != person] | |||||
reminder_text, message = get_birthday_reminder_text_and_message(others) | |||||
send_birthday_reminder(person_email, reminder_text, others, message) | |||||
def get_birthday_reminder_text_and_message(birthday_persons): | |||||
if len(birthday_persons) == 1: | |||||
birthday_person_text = birthday_persons[0]["name"] | |||||
else: | |||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim | |||||
person_names = [d["name"] for d in birthday_persons] | |||||
birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False) | |||||
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) | |||||
message = _("A friendly reminder of an important date for our team.") | |||||
message += "<br>" | |||||
message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text) | |||||
return reminder_text, message | |||||
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): | |||||
frappe.sendmail( | |||||
recipients=recipients, | |||||
subject=_("Birthday Reminder"), | |||||
template="birthday_reminder", | |||||
args=dict( | |||||
reminder_text=reminder_text, | |||||
birthday_persons=birthday_persons, | |||||
message=message, | |||||
), | |||||
header=_("Birthday Reminder 🎂"), | |||||
) | |||||
def get_employees_who_are_born_today(): | |||||
"""Get all employee born today & group them based on their company""" | |||||
return get_employees_having_an_event_today("birthday") | |||||
def get_employees_having_an_event_today(event_type): | |||||
"""Get all employee who have `event_type` today | |||||
& group them based on their company. `event_type` | |||||
can be `birthday` or `work_anniversary`""" | |||||
from collections import defaultdict | |||||
# Set column based on event type | |||||
if event_type == "birthday": | |||||
condition_column = "date_of_birth" | |||||
elif event_type == "work_anniversary": | |||||
condition_column = "date_of_joining" | |||||
else: | |||||
return | |||||
employees_born_today = frappe.db.multisql( | |||||
{ | |||||
"mariadb": f""" | |||||
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining` | |||||
FROM `tabEmployee` | |||||
WHERE | |||||
DAY({condition_column}) = DAY(%(today)s) | |||||
AND | |||||
MONTH({condition_column}) = MONTH(%(today)s) | |||||
AND | |||||
YEAR({condition_column}) < YEAR(%(today)s) | |||||
AND | |||||
`status` = 'Active' | |||||
""", | |||||
"postgres": f""" | |||||
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" | |||||
FROM "tabEmployee" | |||||
WHERE | |||||
DATE_PART('day', {condition_column}) = date_part('day', %(today)s) | |||||
AND | |||||
DATE_PART('month', {condition_column}) = date_part('month', %(today)s) | |||||
AND | |||||
DATE_PART('year', {condition_column}) < date_part('year', %(today)s) | |||||
AND | |||||
"status" = 'Active' | |||||
""", | |||||
}, | |||||
dict(today=today(), condition_column=condition_column), | |||||
as_dict=1, | |||||
) | |||||
grouped_employees = defaultdict(lambda: []) | |||||
for employee_doc in employees_born_today: | |||||
grouped_employees[employee_doc.get("company")].append(employee_doc) | |||||
return grouped_employees | |||||
# -------------------------- | |||||
# WORK ANNIVERSARY REMINDERS | |||||
# -------------------------- | |||||
def send_work_anniversary_reminders(): | |||||
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked""" | |||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders")) | |||||
if not to_send: | |||||
return | |||||
employees_joined_today = get_employees_having_an_event_today("work_anniversary") | |||||
for company, anniversary_persons in employees_joined_today.items(): | |||||
employee_emails = get_all_employee_emails(company) | |||||
anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons] | |||||
recipients = list(set(employee_emails) - set(anniversary_person_emails)) | |||||
reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons) | |||||
send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message) | |||||
if len(anniversary_persons) > 1: | |||||
# email for people sharing work anniversaries | |||||
for person in anniversary_persons: | |||||
person_email = person["user_id"] or person["personal_email"] or person["company_email"] | |||||
others = [d for d in anniversary_persons if d != person] | |||||
reminder_text, message = get_work_anniversary_reminder_text_and_message(others) | |||||
send_work_anniversary_reminder(person_email, reminder_text, others, message) | |||||
def get_work_anniversary_reminder_text_and_message(anniversary_persons): | |||||
if len(anniversary_persons) == 1: | |||||
anniversary_person = anniversary_persons[0]["name"] | |||||
persons_name = anniversary_person | |||||
# Number of years completed at the company | |||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year | |||||
anniversary_person += f" completed {get_pluralized_years(completed_years)}" | |||||
else: | |||||
person_names_with_years = [] | |||||
names = [] | |||||
for person in anniversary_persons: | |||||
person_text = person["name"] | |||||
names.append(person_text) | |||||
# Number of years completed at the company | |||||
completed_years = getdate().year - person["date_of_joining"].year | |||||
person_text += f" completed {get_pluralized_years(completed_years)}" | |||||
person_names_with_years.append(person_text) | |||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim | |||||
anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False) | |||||
persons_name = comma_sep(names, frappe._("{0} & {1}"), False) | |||||
reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person) | |||||
message = _("A friendly reminder of an important date for our team.") | |||||
message += "<br>" | |||||
message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name) | |||||
return reminder_text, message | |||||
def get_pluralized_years(years): | |||||
if years == 1: | |||||
return "1 year" | |||||
return f"{years} years" | |||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): | |||||
frappe.sendmail( | |||||
recipients=recipients, | |||||
subject=_("Work Anniversary Reminder"), | |||||
template="anniversary_reminder", | |||||
args=dict( | |||||
reminder_text=reminder_text, | |||||
anniversary_persons=anniversary_persons, | |||||
message=message, | |||||
), | |||||
header=_("Work Anniversary Reminder"), | |||||
) |
@@ -0,0 +1,269 @@ | |||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
import unittest | |||||
from datetime import timedelta | |||||
import frappe | |||||
from frappe.utils import add_months, getdate | |||||
from erpnext.setup.doctype.employee.test_employee import make_employee | |||||
from hrms.controllers.employee_reminders import send_holidays_reminder_in_advance | |||||
from hrms.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change | |||||
from hrms.hr.utils import get_holidays_for_employee | |||||
class TestEmployeeReminders(unittest.TestCase): | |||||
@classmethod | |||||
def setUpClass(cls): | |||||
from erpnext.setup.doctype.holiday_list.test_holiday_list import make_holiday_list | |||||
# Create a test holiday list | |||||
test_holiday_dates = cls.get_test_holiday_dates() | |||||
test_holiday_list = make_holiday_list( | |||||
"TestHolidayRemindersList", | |||||
holiday_dates=[ | |||||
{"holiday_date": test_holiday_dates[0], "description": "test holiday1"}, | |||||
{"holiday_date": test_holiday_dates[1], "description": "test holiday2"}, | |||||
{"holiday_date": test_holiday_dates[2], "description": "test holiday3", "weekly_off": 1}, | |||||
{"holiday_date": test_holiday_dates[3], "description": "test holiday4"}, | |||||
{"holiday_date": test_holiday_dates[4], "description": "test holiday5"}, | |||||
{"holiday_date": test_holiday_dates[5], "description": "test holiday6"}, | |||||
], | |||||
from_date=getdate() - timedelta(days=10), | |||||
to_date=getdate() + timedelta(weeks=5), | |||||
) | |||||
# Create a test employee | |||||
test_employee = frappe.get_doc( | |||||
"Employee", make_employee("test@gopher.io", company="_Test Company") | |||||
) | |||||
# Attach the holiday list to employee | |||||
test_employee.holiday_list = test_holiday_list.name | |||||
test_employee.save() | |||||
# Attach to class | |||||
cls.test_employee = test_employee | |||||
cls.test_holiday_dates = test_holiday_dates | |||||
# Employee without holidays in this month/week | |||||
test_employee_2 = make_employee("test@empwithoutholiday.io", company="_Test Company") | |||||
test_employee_2 = frappe.get_doc("Employee", test_employee_2) | |||||
test_holiday_list = make_holiday_list( | |||||
"TestHolidayRemindersList2", | |||||
holiday_dates=[ | |||||
{"holiday_date": add_months(getdate(), 1), "description": "test holiday1"}, | |||||
], | |||||
from_date=add_months(getdate(), -2), | |||||
to_date=add_months(getdate(), 2), | |||||
) | |||||
test_employee_2.holiday_list = test_holiday_list.name | |||||
test_employee_2.save() | |||||
cls.test_employee_2 = test_employee_2 | |||||
cls.holiday_list_2 = test_holiday_list | |||||
@classmethod | |||||
def get_test_holiday_dates(cls): | |||||
today_date = getdate() | |||||
return [ | |||||
today_date, | |||||
today_date - timedelta(days=4), | |||||
today_date - timedelta(days=3), | |||||
today_date + timedelta(days=1), | |||||
today_date + timedelta(days=3), | |||||
today_date + timedelta(weeks=3), | |||||
] | |||||
def setUp(self): | |||||
# Clear Email Queue | |||||
frappe.db.sql("delete from `tabEmail Queue`") | |||||
frappe.db.sql("delete from `tabEmail Queue Recipient`") | |||||
def test_is_holiday(self): | |||||
from erpnext.setup.doctype.employee.employee import is_holiday | |||||
self.assertTrue(is_holiday(self.test_employee.name)) | |||||
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1])) | |||||
self.assertFalse(is_holiday(self.test_employee.name, date=getdate() - timedelta(days=1))) | |||||
# Test weekly_off holidays | |||||
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2])) | |||||
self.assertFalse( | |||||
is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True) | |||||
) | |||||
# Test with descriptions | |||||
has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True) | |||||
self.assertTrue(has_holiday) | |||||
self.assertTrue("test holiday1" in descriptions) | |||||
def test_birthday_reminders(self): | |||||
employee = frappe.get_doc( | |||||
"Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0] | |||||
) | |||||
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] | |||||
employee.company_email = "test@example.com" | |||||
employee.company = "_Test Company" | |||||
employee.save() | |||||
from hrms.controllers.employee_reminders import ( | |||||
get_employees_who_are_born_today, | |||||
send_birthday_reminders, | |||||
) | |||||
employees_born_today = get_employees_who_are_born_today() | |||||
self.assertTrue(employees_born_today.get("_Test Company")) | |||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings") | |||||
hr_settings.send_birthday_reminders = 1 | |||||
hr_settings.save() | |||||
send_birthday_reminders() | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) | |||||
def test_work_anniversary_reminders(self): | |||||
from hrms.controllers.employee_reminders import ( | |||||
get_employees_having_an_event_today, | |||||
send_work_anniversary_reminders, | |||||
) | |||||
emp = make_employee( | |||||
"test_emp_work_anniversary@gmail.com", | |||||
company="_Test Company", | |||||
date_of_joining=frappe.utils.add_years(getdate(), -2), | |||||
) | |||||
employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") | |||||
employees = employees_having_work_anniversary.get("_Test Company") or [] | |||||
user_ids = [] | |||||
for entry in employees: | |||||
user_ids.append(entry.user_id) | |||||
self.assertTrue("test_emp_work_anniversary@gmail.com" in user_ids) | |||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings") | |||||
hr_settings.send_work_anniversary_reminders = 1 | |||||
hr_settings.save() | |||||
send_work_anniversary_reminders() | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) | |||||
def test_work_anniversary_reminder_not_sent_for_0_years(self): | |||||
make_employee( | |||||
"test_work_anniversary_2@gmail.com", | |||||
date_of_joining=getdate(), | |||||
company="_Test Company", | |||||
) | |||||
from hrms.controllers.employee_reminders import get_employees_having_an_event_today | |||||
employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") | |||||
employees = employees_having_work_anniversary.get("_Test Company") or [] | |||||
user_ids = [] | |||||
for entry in employees: | |||||
user_ids.append(entry.user_id) | |||||
self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids) | |||||
def test_send_holidays_reminder_in_advance(self): | |||||
setup_hr_settings("Weekly") | |||||
holidays = get_holidays_for_employee( | |||||
self.test_employee.get("name"), | |||||
getdate(), | |||||
getdate() + timedelta(days=3), | |||||
only_non_weekly=True, | |||||
raise_exception=False, | |||||
) | |||||
send_holidays_reminder_in_advance(self.test_employee.get("name"), holidays) | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertEqual(len(email_queue), 1) | |||||
self.assertTrue("Holidays this Week." in email_queue[0].message) | |||||
def test_advance_holiday_reminders_monthly(self): | |||||
from hrms.controllers.employee_reminders import send_reminders_in_advance_monthly | |||||
setup_hr_settings("Monthly") | |||||
# disable emp 2, set same holiday list | |||||
frappe.db.set_value( | |||||
"Employee", | |||||
self.test_employee_2.name, | |||||
{"status": "Left", "holiday_list": self.test_employee.holiday_list}, | |||||
) | |||||
send_reminders_in_advance_monthly() | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertTrue(len(email_queue) > 0) | |||||
# even though emp 2 has holiday, non-active employees should not be recipients | |||||
recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") | |||||
self.assertTrue(self.test_employee_2.user_id not in recipients) | |||||
# teardown: enable emp 2 | |||||
frappe.db.set_value( | |||||
"Employee", | |||||
self.test_employee_2.name, | |||||
{"status": "Active", "holiday_list": self.holiday_list_2.name}, | |||||
) | |||||
def test_advance_holiday_reminders_weekly(self): | |||||
from hrms.controllers.employee_reminders import send_reminders_in_advance_weekly | |||||
setup_hr_settings("Weekly") | |||||
# disable emp 2, set same holiday list | |||||
frappe.db.set_value( | |||||
"Employee", | |||||
self.test_employee_2.name, | |||||
{"status": "Left", "holiday_list": self.test_employee.holiday_list}, | |||||
) | |||||
send_reminders_in_advance_weekly() | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertTrue(len(email_queue) > 0) | |||||
# even though emp 2 has holiday, non-active employees should not be recipients | |||||
recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") | |||||
self.assertTrue(self.test_employee_2.user_id not in recipients) | |||||
# teardown: enable emp 2 | |||||
frappe.db.set_value( | |||||
"Employee", | |||||
self.test_employee_2.name, | |||||
{"status": "Active", "holiday_list": self.holiday_list_2.name}, | |||||
) | |||||
def test_reminder_not_sent_if_no_holdays(self): | |||||
setup_hr_settings("Monthly") | |||||
# reminder not sent if there are no holidays | |||||
holidays = get_holidays_for_employee( | |||||
self.test_employee_2.get("name"), | |||||
getdate(), | |||||
getdate() + timedelta(days=3), | |||||
only_non_weekly=True, | |||||
raise_exception=False, | |||||
) | |||||
send_holidays_reminder_in_advance(self.test_employee_2.get("name"), holidays) | |||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) | |||||
self.assertEqual(len(email_queue), 0) | |||||
def setup_hr_settings(frequency=None): | |||||
# Get HR settings and enable advance holiday reminders | |||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings") | |||||
hr_settings.send_holiday_reminders = 1 | |||||
set_proceed_with_frequency_change() | |||||
hr_settings.frequency = frequency or "Weekly" | |||||
hr_settings.save() |
@@ -0,0 +1,308 @@ | |||||
app_name = "hrms" | |||||
app_title = "Frappe HR" | |||||
app_publisher = "Frappe Technologies Pvt. Ltd." | |||||
app_description = "Modern HR and Payroll Software" | |||||
app_email = "contact@frappe.io" | |||||
app_license = "GNU General Public License (v3)" | |||||
required_apps = ["erpnext"] | |||||
# Includes in <head> | |||||
# ------------------ | |||||
# include js, css files in header of desk.html | |||||
# app_include_css = "/assets/hrms/css/hrms.css" | |||||
app_include_js = [ | |||||
"hrms.bundle.js", | |||||
"performance.bundle.js", | |||||
] | |||||
app_include_css = "hrms.bundle.css" | |||||
# website | |||||
# include js, css files in header of web template | |||||
# web_include_css = "/assets/hrms/css/hrms.css" | |||||
# web_include_js = "/assets/hrms/js/hrms.js" | |||||
# include custom scss in every website theme (without file extension ".scss") | |||||
# website_theme_scss = "hrms/public/scss/website" | |||||
# include js, css files in header of web form | |||||
# webform_include_js = {"doctype": "public/js/doctype.js"} | |||||
# webform_include_css = {"doctype": "public/css/doctype.css"} | |||||
# include js in page | |||||
# page_js = {"page" : "public/js/file.js"} | |||||
# include js in doctype views | |||||
doctype_js = { | |||||
"Employee": "public/js/erpnext/employee.js", | |||||
"Company": "public/js/erpnext/company.js", | |||||
"Department": "public/js/erpnext/department.js", | |||||
"Timesheet": "public/js/erpnext/timesheet.js", | |||||
"Payment Entry": "public/js/erpnext/payment_entry.js", | |||||
"Journal Entry": "public/js/erpnext/journal_entry.js", | |||||
"Delivery Trip": "public/js/erpnext/deliver_trip.js", | |||||
"Bank Transaction": "public/js/erpnext/bank_transaction.js", | |||||
} | |||||
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} | |||||
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} | |||||
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} | |||||
# Home Pages | |||||
# ---------- | |||||
# application home page (will override Website Settings) | |||||
# home_page = "login" | |||||
# website user home page (by Role) | |||||
# role_home_page = { | |||||
# "Role": "home_page" | |||||
# } | |||||
# Generators | |||||
# ---------- | |||||
# automatically create page for each record of this doctype | |||||
website_generators = ["Job Opening"] | |||||
website_route_rules = [ | |||||
{"from_route": "/jobs", "to_route": "Job Opening"}, | |||||
] | |||||
# Jinja | |||||
# ---------- | |||||
# add methods and filters to jinja environment | |||||
jinja = { | |||||
"methods": [ | |||||
"hrms.utils.get_country", | |||||
], | |||||
} | |||||
# Installation | |||||
# ------------ | |||||
# before_install = "hrms.install.before_install" | |||||
after_install = "hrms.install.after_install" | |||||
after_migrate = "hrms.setup.update_select_perm_after_install" | |||||
# Uninstallation | |||||
# ------------ | |||||
before_uninstall = "hrms.uninstall.before_uninstall" | |||||
# after_uninstall = "hrms.uninstall.after_uninstall" | |||||
# Desk Notifications | |||||
# ------------------ | |||||
# See frappe.core.notifications.get_notification_config | |||||
# notification_config = "hrms.notifications.get_notification_config" | |||||
# Permissions | |||||
# ----------- | |||||
# Permissions evaluated in scripted ways | |||||
# permission_query_conditions = { | |||||
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", | |||||
# } | |||||
# | |||||
# has_permission = { | |||||
# "Event": "frappe.desk.doctype.event.event.has_permission", | |||||
# } | |||||
has_upload_permission = { | |||||
"Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission" | |||||
} | |||||
# DocType Class | |||||
# --------------- | |||||
# Override standard doctype classes | |||||
override_doctype_class = { | |||||
"Employee": "hrms.overrides.employee_master.EmployeeMaster", | |||||
"Timesheet": "hrms.overrides.employee_timesheet.EmployeeTimesheet", | |||||
"Payment Entry": "hrms.overrides.employee_payment_entry.EmployeePaymentEntry", | |||||
"Project": "hrms.overrides.employee_project.EmployeeProject", | |||||
} | |||||
# Document Events | |||||
# --------------- | |||||
# Hook on document methods and events | |||||
doc_events = { | |||||
"User": { | |||||
"validate": "erpnext.setup.doctype.employee.employee.validate_employee_role", | |||||
"on_update": "erpnext.setup.doctype.employee.employee.update_user_permissions", | |||||
}, | |||||
"Company": { | |||||
"validate": "hrms.overrides.company.validate_default_accounts", | |||||
"on_update": [ | |||||
"hrms.overrides.company.make_company_fixtures", | |||||
"hrms.overrides.company.set_default_hr_accounts", | |||||
], | |||||
}, | |||||
"Timesheet": {"validate": "hrms.hr.utils.validate_active_employee"}, | |||||
"Payment Entry": { | |||||
"on_submit": "hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
"on_cancel": "hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
"on_update_after_submit": "hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
}, | |||||
"Journal Entry": { | |||||
"validate": "hrms.hr.doctype.expense_claim.expense_claim.validate_expense_claim_in_jv", | |||||
"on_submit": [ | |||||
"hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
"hrms.hr.doctype.full_and_final_statement.full_and_final_statement.update_full_and_final_statement_status", | |||||
], | |||||
"on_update_after_submit": "hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
"on_cancel": [ | |||||
"hrms.hr.doctype.expense_claim.expense_claim.update_payment_for_expense_claim", | |||||
"hrms.payroll.doctype.salary_slip.salary_slip.unlink_ref_doc_from_salary_slip", | |||||
"hrms.hr.doctype.full_and_final_statement.full_and_final_statement.update_full_and_final_statement_status", | |||||
], | |||||
}, | |||||
"Loan": {"validate": "hrms.hr.utils.validate_loan_repay_from_salary"}, | |||||
"Employee": { | |||||
"validate": "hrms.overrides.employee_master.validate_onboarding_process", | |||||
"on_update": "hrms.overrides.employee_master.update_approver_role", | |||||
"on_trash": "hrms.overrides.employee_master.update_employee_transfer", | |||||
}, | |||||
"Project": { | |||||
"validate": "hrms.controllers.employee_boarding_controller.update_employee_boarding_status" | |||||
}, | |||||
"Task": {"on_update": "hrms.controllers.employee_boarding_controller.update_task"}, | |||||
} | |||||
# Scheduled Tasks | |||||
# --------------- | |||||
scheduler_events = { | |||||
"all": [ | |||||
"hrms.hr.doctype.interview.interview.send_interview_reminder", | |||||
], | |||||
"hourly": [ | |||||
"hrms.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails", | |||||
], | |||||
"hourly_long": [ | |||||
"hrms.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", | |||||
], | |||||
"daily": [ | |||||
"hrms.controllers.employee_reminders.send_birthday_reminders", | |||||
"hrms.controllers.employee_reminders.send_work_anniversary_reminders", | |||||
"hrms.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary", | |||||
"hrms.hr.doctype.interview.interview.send_daily_feedback_reminder", | |||||
], | |||||
"daily_long": [ | |||||
"hrms.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", | |||||
"hrms.hr.utils.generate_leave_encashment", | |||||
"hrms.hr.utils.allocate_earned_leaves", | |||||
], | |||||
"weekly": ["hrms.controllers.employee_reminders.send_reminders_in_advance_weekly"], | |||||
"monthly": ["hrms.controllers.employee_reminders.send_reminders_in_advance_monthly"], | |||||
} | |||||
advance_payment_doctypes = ["Gratuity", "Employee Advance"] | |||||
invoice_doctypes = ["Expense Claim"] | |||||
period_closing_doctypes = ["Payroll Entry"] | |||||
accounting_dimension_doctypes = [ | |||||
"Expense Claim", | |||||
"Expense Claim Detail", | |||||
"Expense Taxes and Charges", | |||||
"Payroll Entry", | |||||
] | |||||
bank_reconciliation_doctypes = ["Expense Claim"] | |||||
# Testing | |||||
# ------- | |||||
before_tests = "hrms.utils.before_tests" | |||||
# Overriding Methods | |||||
# ----------------------------- | |||||
# get matching queries for Bank Reconciliation | |||||
get_matching_queries = "hrms.hr.utils.get_matching_queries" | |||||
regional_overrides = { | |||||
"India": { | |||||
"hrms.hr.utils.calculate_annual_eligible_hra_exemption": "hrms.regional.india.utils.calculate_annual_eligible_hra_exemption", | |||||
"hrms.hr.utils.calculate_hra_exemption_for_period": "hrms.regional.india.utils.calculate_hra_exemption_for_period", | |||||
}, | |||||
} | |||||
# ERPNext doctypes for Global Search | |||||
global_search_doctypes = { | |||||
"Default": [ | |||||
{"doctype": "Salary Slip", "index": 19}, | |||||
{"doctype": "Leave Application", "index": 20}, | |||||
{"doctype": "Expense Claim", "index": 21}, | |||||
{"doctype": "Employee Grade", "index": 37}, | |||||
{"doctype": "Job Opening", "index": 39}, | |||||
{"doctype": "Job Applicant", "index": 40}, | |||||
{"doctype": "Job Offer", "index": 41}, | |||||
{"doctype": "Salary Structure Assignment", "index": 42}, | |||||
{"doctype": "Appraisal", "index": 43}, | |||||
], | |||||
} | |||||
# override_whitelisted_methods = { | |||||
# "frappe.desk.doctype.event.event.get_events": "hrms.event.get_events" | |||||
# } | |||||
# | |||||
# each overriding function accepts a `data` argument; | |||||
# generated from the base implementation of the doctype dashboard, | |||||
# along with any modifications made in other Frappe apps | |||||
override_doctype_dashboards = { | |||||
"Employee": "hrms.overrides.dashboard_overrides.get_dashboard_for_employee", | |||||
"Holiday List": "hrms.overrides.dashboard_overrides.get_dashboard_for_holiday_list", | |||||
"Task": "hrms.overrides.dashboard_overrides.get_dashboard_for_project", | |||||
"Project": "hrms.overrides.dashboard_overrides.get_dashboard_for_project", | |||||
"Timesheet": "hrms.overrides.dashboard_overrides.get_dashboard_for_timesheet", | |||||
} | |||||
# exempt linked doctypes from being automatically cancelled | |||||
# | |||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"] | |||||
# User Data Protection | |||||
# -------------------- | |||||
# user_data_fields = [ | |||||
# { | |||||
# "doctype": "{doctype_1}", | |||||
# "filter_by": "{filter_by}", | |||||
# "redact_fields": ["{field_1}", "{field_2}"], | |||||
# "partial": 1, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_2}", | |||||
# "filter_by": "{filter_by}", | |||||
# "partial": 1, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_3}", | |||||
# "strict": False, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_4}" | |||||
# } | |||||
# ] | |||||
# Authentication and authorization | |||||
# -------------------------------- | |||||
# auth_hooks = [ | |||||
# "hrms.auth.validate" | |||||
# ] | |||||
# Translation | |||||
# -------------------------------- | |||||
# Make link fields search translated document names for these DocTypes | |||||
# Recommended only for DocTypes which have limited documents with untranslated names | |||||
# For example: Role, Gender, etc. | |||||
# translated_search_doctypes = [] |
@@ -0,0 +1,6 @@ | |||||
Key features: | |||||
- Leave and Attendance | |||||
- Payroll | |||||
- Appraisal | |||||
- Expense Claim |
@@ -0,0 +1,28 @@ | |||||
{ | |||||
"chart_name": "Attendance Count", | |||||
"chart_type": "Report", | |||||
"creation": "2020-07-22 11:56:32.730068", | |||||
"custom_options": "{\n\t\t\"type\": \"line\",\n\t\t\"axisOptions\": {\n\t\t\t\"shortenYAxisNumbers\": 1\n\t\t},\n\t\t\"tooltipOptions\": {}\n\t}", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"dynamic_filters_json": "{\"month\":\"frappe.datetime.str_to_obj(frappe.datetime.get_today()).getMonth() + 1\",\"year\":\"frappe.datetime.str_to_obj(frappe.datetime.get_today()).getFullYear();\",\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\"}", | |||||
"filters_json": "{\"summarized_view\":0}", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-21 14:25:01.608941", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Attendance Count", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"report_name": "Monthly Attendance Sheet", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 1, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Claims by Type", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-31 23:04:43.377345", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Expense Claim Detail", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "expense_type", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-09-16 11:36:29.484579", | |||||
"modified": "2022-09-16 11:39:08.205987", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Claims by Type", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "Expense Claim", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Department Wise Employee Count", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.760730", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "department", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 14:08:17.017113", | |||||
"modified": "2022-08-22 14:09:19.603767", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Department Wise Employee Count", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Department wise Expense Claims", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-31 23:06:51.144716", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Expense Claim", | |||||
"dynamic_filters_json": "[[\"Expense Claim\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Expense Claim\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "department", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-09-16 12:36:29.444007", | |||||
"modified": "2022-09-16 11:41:32.160907", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Department wise Expense Claims", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Bar", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"aggregate_function_based_on": "planned_vacancies", | |||||
"chart_name": "Department Wise Openings", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.849775", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Opening", | |||||
"dynamic_filters_json": "[[\"Job Opening\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Job Opening\",\"status\",\"=\",\"Open\",false]]", | |||||
"group_by_based_on": "department", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-21 23:19:31.637348", | |||||
"modified": "2022-08-20 23:22:24.707871", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Department Wise Openings", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Monthly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Bar", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,34 @@ | |||||
{ | |||||
"aggregate_function_based_on": "total_hours", | |||||
"based_on": "", | |||||
"chart_name": "Department wise Timesheet Hours", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-21 17:32:09.625319", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Timesheet", | |||||
"dynamic_filters_json": "[[\"Timesheet\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Timesheet\",\"start_date\",\"Timespan\",\"this month\",false],[\"Timesheet\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "department", | |||||
"group_by_type": "Sum", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-21 17:56:03.184928", | |||||
"modified": "2022-08-21 17:57:47.234034", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Department wise Timesheet Hours", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Bar", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Designation Wise Employee Count", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.790337", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "designation", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:33:54.631648", | |||||
"modified": "2022-08-22 12:45:29.009857", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Designation Wise Employee Count", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"aggregate_function_based_on": "planned_vacancies", | |||||
"chart_name": "Designation Wise Openings", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.820217", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Opening", | |||||
"dynamic_filters_json": "[[\"Job Opening\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Job Opening\",\"status\",\"=\",\"Open\",false]]", | |||||
"group_by_based_on": "designation", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-20 23:20:41.683553", | |||||
"modified": "2022-08-20 23:22:15.254254", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Designation Wise Openings", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Monthly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Bar", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Employee Advance Status", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-31 23:06:16.063039", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee Advance", | |||||
"dynamic_filters_json": "[[\"Employee Advance\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee Advance\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "status", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-09-16 12:36:29.430589", | |||||
"modified": "2022-09-16 11:43:57.244256", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employee Advance Status", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Employees by Age", | |||||
"chart_type": "Custom", | |||||
"creation": "2022-08-22 19:07:51.906347", | |||||
"custom_options": "{\n\t\"colors\": [\"#7cd6fd\"],\n\t\"barOptions\": {\"spaceRatio\": 0.5}\n}", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "", | |||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\"}", | |||||
"filters_json": "{}", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 19:00:02.464180", | |||||
"modified": "2022-08-22 19:11:20.076166", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employees by Age", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "Employees by Age", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Bar", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Employees by Branch", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-22 12:33:43.241006", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "branch", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:25:47.733581", | |||||
"modified": "2022-08-22 12:33:49.094799", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employees by Branch", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Employees by Grade", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-22 12:33:23.767559", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "grade", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:25:47.733581", | |||||
"modified": "2022-08-22 12:33:29.562836", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employees by Grade", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Employees by Type", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-22 13:49:59.343893", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "employment_type", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 13:45:38.913766", | |||||
"modified": "2022-08-22 13:50:05.645535", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employees by Type", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "posting_date", | |||||
"chart_name": "Expense Claims", | |||||
"chart_type": "Sum", | |||||
"color": "#449CF0", | |||||
"creation": "2022-08-21 14:07:24.120739", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Expense Claim", | |||||
"dynamic_filters_json": "[[\"Expense Claim\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Expense Claim\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-09-16 11:36:29.292891", | |||||
"modified": "2022-09-16 11:37:59.669291", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Expense Claims", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Monthly", | |||||
"timeseries": 1, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "total_sanctioned_amount", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,29 @@ | |||||
{ | |||||
"chart_name": "Gender Diversity Ratio", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.667291", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee\",\"status\",\"=\",\"Active\",false]]", | |||||
"group_by_based_on": "gender", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2020-07-22 14:27:40.143783", | |||||
"modified": "2020-07-22 14:32:50.962459", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Gender Diversity Ratio", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Grievance Type", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-21 13:02:06.880100", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee Grievance", | |||||
"dynamic_filters_json": "[[\"Employee\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee Grievance\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "grievance_type", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-21 13:08:57.019388", | |||||
"modified": "2022-08-21 13:40:40.415600", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Grievance Type", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Hiring vs Attrition Count", | |||||
"chart_type": "Custom", | |||||
"creation": "2022-08-21 22:58:12.740936", | |||||
"custom_options": "{\n\t\"type\": \"axis-mixed\",\n\t\"axisOptions\": {\n\t\t\"xIsSeries\": 1\n\t},\n\t\"lineOptions\": {\n\t \"regionFill\": 1\n\t},\n\t\"colors\": [\"#7cd6fd\", \"#5e64ff\"]\n}", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "", | |||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\", \"from_date\":\"frappe.defaults.get_user_default(\\\"year_start_date\\\")\", \"to_date\":\"frappe.defaults.get_user_default(\\\"year_end_date\\\")\"}", | |||||
"filters_json": "{\"time_interval\":\"Monthly\"}", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 10:57:55.011020", | |||||
"modified": "2022-08-22 11:03:30.080835", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Hiring vs Attrition Count", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "Hiring vs Attrition Count", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Interview Status", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-20 23:10:33.131622", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Interview", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "status", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:13:19.640093", | |||||
"modified": "2022-08-22 12:16:33.674218", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Interview Status", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Job Applicant Pipeline", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-20 21:18:45.283444", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Applicant", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "job_title", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-20 23:45:11.740188", | |||||
"modified": "2022-08-20 23:48:35.499218", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Applicant Pipeline", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Percentage", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Job Applicant Source", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-20 22:59:15.210760", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Applicant", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "source", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-20 23:45:11.697841", | |||||
"modified": "2022-08-20 23:47:52.946872", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Applicant Source", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Percentage", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,32 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Job Applicants by Country", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-22 12:17:53.776473", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Applicant", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "country", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-22 12:18:01.288634", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Applicants by Country", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,32 @@ | |||||
{ | |||||
"based_on": "creation", | |||||
"chart_name": "Job Application Frequency", | |||||
"chart_type": "Count", | |||||
"creation": "2022-08-20 22:00:12.227849", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Applicant", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-20 23:11:18.520971", | |||||
"modified": "2022-08-20 23:16:02.076184", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Application Frequency", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Monthly", | |||||
"timeseries": 1, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,30 @@ | |||||
{ | |||||
"chart_name": "Job Application Status", | |||||
"chart_type": "Group By", | |||||
"creation": "2020-07-22 11:56:32.699696", | |||||
"custom_options": "", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Applicant", | |||||
"dynamic_filters_json": "", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "status", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:07:53.129240", | |||||
"modified": "2022-08-22 12:10:29.144396", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Application Status", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"roles": [], | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Job Offer Status", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-20 21:33:17.378147", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Job Offer", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "status", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-22 12:12:19.736710", | |||||
"modified": "2022-08-22 12:14:23.044346", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Job Offer Status", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,32 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Shift Assignment Breakup", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-21 18:11:42.510195", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Shift Assignment", | |||||
"dynamic_filters_json": "[[\"Shift Assignment\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Shift Assignment\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "shift_type", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-21 18:12:47.352410", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Shift Assignment Breakup", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,34 @@ | |||||
{ | |||||
"aggregate_function_based_on": "hours", | |||||
"based_on": "", | |||||
"chart_name": "Timesheet Activity Breakup", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-21 14:31:10.401241", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Timesheet Detail", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[]", | |||||
"group_by_based_on": "activity_type", | |||||
"group_by_type": "Sum", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"last_synced_on": "2022-08-21 17:55:44.318686", | |||||
"modified": "2022-08-21 17:59:38.576219", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Timesheet Activity Breakup", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "Timesheet", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Percentage", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,32 @@ | |||||
{ | |||||
"based_on": "", | |||||
"chart_name": "Training Type", | |||||
"chart_type": "Group By", | |||||
"creation": "2022-08-21 13:29:27.202404", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Training Event", | |||||
"dynamic_filters_json": "[]", | |||||
"filters_json": "[[\"Training Event\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_based_on": "type", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-21 13:38:05.390856", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Training Type", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 0, | |||||
"timespan": "Last Year", | |||||
"type": "Pie", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"based_on": "promotion_date", | |||||
"chart_name": "Y-O-Y Promotions", | |||||
"chart_type": "Count", | |||||
"creation": "2022-08-21 13:34:12.830736", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee Promotion", | |||||
"dynamic_filters_json": "[[\"Employee Promotion\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee Promotion\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-21 13:38:46.190352", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Y-O-Y Promotions", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 1, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,31 @@ | |||||
{ | |||||
"based_on": "transfer_date", | |||||
"chart_name": "Y-O-Y Transfers", | |||||
"chart_type": "Count", | |||||
"creation": "2022-08-21 13:28:05.162754", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart", | |||||
"document_type": "Employee Transfer", | |||||
"dynamic_filters_json": "[[\"Employee Transfer\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", | |||||
"filters_json": "[[\"Employee Transfer\",\"docstatus\",\"=\",\"1\",false]]", | |||||
"group_by_type": "Count", | |||||
"idx": 0, | |||||
"is_public": 1, | |||||
"is_standard": 1, | |||||
"modified": "2022-08-21 13:38:54.890663", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Y-O-Y Transfers", | |||||
"number_of_groups": 0, | |||||
"owner": "Administrator", | |||||
"parent_document_type": "", | |||||
"roles": [], | |||||
"source": "", | |||||
"time_interval": "Yearly", | |||||
"timeseries": 1, | |||||
"timespan": "Last Year", | |||||
"type": "Line", | |||||
"use_report_chart": 0, | |||||
"value_based_on": "", | |||||
"y_axis": [] | |||||
} |
@@ -0,0 +1,14 @@ | |||||
frappe.provide("frappe.dashboards.chart_sources"); | |||||
frappe.dashboards.chart_sources["Employees by Age"] = { | |||||
method: "hrms.hr.dashboard_chart_source.employees_by_age.employees_by_age.get_data", | |||||
filters: [ | |||||
{ | |||||
fieldname: "company", | |||||
label: __("Company"), | |||||
fieldtype: "Link", | |||||
options: "Company", | |||||
default: frappe.defaults.get_user_default("Company") | |||||
}, | |||||
] | |||||
}; |
@@ -0,0 +1,13 @@ | |||||
{ | |||||
"creation": "2022-08-22 12:49:07.303139", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart Source", | |||||
"idx": 0, | |||||
"modified": "2022-08-22 12:49:07.303139", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Employees by Age", | |||||
"owner": "Administrator", | |||||
"source_name": "Employees by Age", | |||||
"timeseries": 0 | |||||
} |
@@ -0,0 +1,87 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
from dateutil.relativedelta import relativedelta | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.utils import getdate | |||||
from frappe.utils.dashboard import cache_source | |||||
@frappe.whitelist() | |||||
@cache_source | |||||
def get_data( | |||||
chart_name=None, | |||||
chart=None, | |||||
no_cache=None, | |||||
filters=None, | |||||
from_date=None, | |||||
to_date=None, | |||||
timespan=None, | |||||
time_interval=None, | |||||
heatmap_year=None, | |||||
) -> dict[str, list]: | |||||
if filters: | |||||
filters = frappe.parse_json(filters) | |||||
employees = frappe.db.get_list( | |||||
"Employee", | |||||
filters={"company": filters.get("company"), "status": "Active"}, | |||||
pluck="date_of_birth", | |||||
) | |||||
age_list = get_age_list(employees) | |||||
ranges = get_ranges() | |||||
age_range, values = get_employees_by_age(age_list, ranges) | |||||
return { | |||||
"labels": age_range, | |||||
"datasets": [ | |||||
{"name": _("Employees"), "values": values}, | |||||
], | |||||
} | |||||
def get_ranges() -> list[tuple[int, int]]: | |||||
ranges = [] | |||||
for i in range(15, 80, 5): | |||||
ranges.append((i, i + 4)) | |||||
ranges.append(80) | |||||
return ranges | |||||
def get_age_list(employees) -> list[int]: | |||||
age_list = [] | |||||
for dob in employees: | |||||
if not dob: | |||||
continue | |||||
age = relativedelta(getdate(), getdate(dob)).years | |||||
age_list.append(age) | |||||
return age_list | |||||
def get_employees_by_age(age_list, ranges) -> tuple[list[str], list[int]]: | |||||
age_range = [] | |||||
values = [] | |||||
for bracket in ranges: | |||||
if isinstance(bracket, int): | |||||
age_range.append(f"{bracket}+") | |||||
else: | |||||
age_range.append(f"{bracket[0]}-{bracket[1]}") | |||||
count = 0 | |||||
for age in age_list: | |||||
if (isinstance(bracket, int) and age >= bracket) or ( | |||||
isinstance(bracket, tuple) and bracket[0] <= age <= bracket[1] | |||||
): | |||||
count += 1 | |||||
values.append(count) | |||||
return age_range, values |
@@ -0,0 +1,35 @@ | |||||
frappe.provide("frappe.dashboards.chart_sources"); | |||||
frappe.dashboards.chart_sources["Hiring vs Attrition Count"] = { | |||||
method: "hrms.hr.dashboard_chart_source.hiring_vs_attrition_count.hiring_vs_attrition_count.get_data", | |||||
filters: [ | |||||
{ | |||||
fieldname: "company", | |||||
label: __("Company"), | |||||
fieldtype: "Link", | |||||
options: "Company", | |||||
default: frappe.defaults.get_user_default("Company") | |||||
}, | |||||
{ | |||||
fieldname: "from_date", | |||||
label: __("From Date"), | |||||
fieldtype: "Date", | |||||
default: frappe.defaults.get_user_default("year_start_date"), | |||||
reqd: 1, | |||||
}, | |||||
{ | |||||
fieldname: "to_date", | |||||
label: __("To Date"), | |||||
fieldtype: "Date", | |||||
default: frappe.defaults.get_user_default("year_end_date"), | |||||
}, | |||||
{ | |||||
fieldname: "time_interval", | |||||
label: __("Time Interval"), | |||||
fieldtype: "Select", | |||||
options: ["Monthly", "Quarterly", "Yearly"], | |||||
default: "Monthly", | |||||
reqd: 1 | |||||
}, | |||||
] | |||||
}; |
@@ -0,0 +1,13 @@ | |||||
{ | |||||
"creation": "2022-08-21 21:38:22.271985", | |||||
"docstatus": 0, | |||||
"doctype": "Dashboard Chart Source", | |||||
"idx": 0, | |||||
"modified": "2022-08-21 21:38:22.271985", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Hiring vs Attrition Count", | |||||
"owner": "Administrator", | |||||
"source_name": "Hiring vs Attrition Count", | |||||
"timeseries": 1 | |||||
} |
@@ -0,0 +1,69 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result | |||||
from frappe.utils import getdate | |||||
from frappe.utils.dashboard import cache_source | |||||
from frappe.utils.dateutils import get_period | |||||
@frappe.whitelist() | |||||
@cache_source | |||||
def get_data( | |||||
chart_name=None, | |||||
chart=None, | |||||
no_cache=None, | |||||
filters=None, | |||||
from_date=None, | |||||
to_date=None, | |||||
timespan=None, | |||||
time_interval=None, | |||||
heatmap_year=None, | |||||
) -> dict[str, list]: | |||||
if filters: | |||||
filters = frappe.parse_json(filters) | |||||
from_date = filters.get("from_date") | |||||
to_date = filters.get("to_date") | |||||
if not to_date: | |||||
to_date = getdate() | |||||
hiring = get_records(from_date, to_date, "date_of_joining", filters.get("company")) | |||||
attrition = get_records(from_date, to_date, "relieving_date", filters.get("company")) | |||||
hiring_data = get_result(hiring, filters.get("time_interval"), from_date, to_date, "Count") | |||||
attrition_data = get_result(attrition, filters.get("time_interval"), from_date, to_date, "Count") | |||||
return { | |||||
"labels": [get_period(r[0], filters.get("time_interval")) for r in hiring_data], | |||||
"datasets": [ | |||||
{"name": _("Hiring Count"), "values": [r[1] for r in hiring_data]}, | |||||
{"name": _("Attrition Count"), "values": [r[1] for r in attrition_data]}, | |||||
], | |||||
} | |||||
def get_records( | |||||
from_date: str, to_date: str, datefield: str, company: str | |||||
) -> tuple[tuple[str, float, int]]: | |||||
filters = [ | |||||
["Employee", "company", "=", company], | |||||
["Employee", datefield, ">=", from_date, False], | |||||
["Employee", datefield, "<=", to_date, False], | |||||
] | |||||
data = frappe.db.get_list( | |||||
"Employee", | |||||
fields=[f"{datefield} as _unit", "SUM(1)", "COUNT(*)"], | |||||
filters=filters, | |||||
group_by="_unit", | |||||
order_by="_unit asc", | |||||
as_list=True, | |||||
ignore_ifnull=True, | |||||
) | |||||
return data |
@@ -0,0 +1,30 @@ | |||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('Appointment Letter', { | |||||
appointment_letter_template: function(frm){ | |||||
if (frm.doc.appointment_letter_template){ | |||||
frappe.call({ | |||||
method: 'hrms.hr.doctype.appointment_letter.appointment_letter.get_appointment_letter_details', | |||||
args : { | |||||
template : frm.doc.appointment_letter_template | |||||
}, | |||||
callback: function(r){ | |||||
if(r.message){ | |||||
let message_body = r.message; | |||||
frm.set_value("introduction", message_body[0].introduction); | |||||
frm.set_value("closing_notes", message_body[0].closing_notes); | |||||
frm.doc.terms = [] | |||||
for (var i in message_body[1].description){ | |||||
frm.add_child("terms"); | |||||
frm.fields_dict.terms.get_value()[i].title = message_body[1].description[i].title; | |||||
frm.fields_dict.terms.get_value()[i].description = message_body[1].description[i].description; | |||||
} | |||||
frm.refresh(); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
}); |
@@ -0,0 +1,128 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "HR-APP-LETTER-.#####", | |||||
"creation": "2019-12-26 12:35:49.574828", | |||||
"default_print_format": "Standard Appointment Letter", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"job_applicant", | |||||
"applicant_name", | |||||
"column_break_3", | |||||
"company", | |||||
"appointment_date", | |||||
"appointment_letter_template", | |||||
"body_section", | |||||
"introduction", | |||||
"terms", | |||||
"closing_notes" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fetch_from": "job_applicant.applicant_name", | |||||
"fieldname": "applicant_name", | |||||
"fieldtype": "Data", | |||||
"in_global_search": 1, | |||||
"in_list_view": 1, | |||||
"label": "Applicant Name", | |||||
"read_only": 1, | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "appointment_date", | |||||
"fieldtype": "Date", | |||||
"label": "Appointment Date", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "appointment_letter_template", | |||||
"fieldtype": "Link", | |||||
"label": "Appointment Letter Template", | |||||
"options": "Appointment Letter Template", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fetch_from": "appointment_letter_template.introduction", | |||||
"fieldname": "introduction", | |||||
"fieldtype": "Long Text", | |||||
"label": "Introduction", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "body_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Body" | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_3", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"fieldname": "job_applicant", | |||||
"fieldtype": "Link", | |||||
"label": "Job Applicant", | |||||
"options": "Job Applicant", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "company", | |||||
"fieldtype": "Link", | |||||
"label": "Company", | |||||
"options": "Company", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "closing_notes", | |||||
"fieldtype": "Text", | |||||
"label": "Closing Notes" | |||||
}, | |||||
{ | |||||
"fieldname": "terms", | |||||
"fieldtype": "Table", | |||||
"label": "Terms", | |||||
"options": "Appointment Letter content", | |||||
"reqd": 1 | |||||
} | |||||
], | |||||
"links": [], | |||||
"modified": "2022-01-18 19:27:35.649424", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appointment Letter", | |||||
"name_case": "Title Case", | |||||
"naming_rule": "Expression (old style)", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"search_fields": "applicant_name, company", | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [], | |||||
"title_field": "applicant_name", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,29 @@ | |||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors | |||||
# For license information, please see license.txt | |||||
import frappe | |||||
from frappe.model.document import Document | |||||
class AppointmentLetter(Document): | |||||
pass | |||||
@frappe.whitelist() | |||||
def get_appointment_letter_details(template): | |||||
body = [] | |||||
intro = frappe.get_list( | |||||
"Appointment Letter Template", | |||||
fields=["introduction", "closing_notes"], | |||||
filters={"name": template}, | |||||
)[0] | |||||
content = frappe.get_all( | |||||
"Appointment Letter content", | |||||
fields=["title", "description"], | |||||
filters={"parent": template}, | |||||
order_by="idx", | |||||
) | |||||
body.append(intro) | |||||
body.append({"description": content}) | |||||
return body |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# See license.txt | |||||
# import frappe | |||||
import unittest | |||||
class TestAppointmentLetter(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,39 @@ | |||||
{ | |||||
"actions": [], | |||||
"creation": "2019-12-26 12:22:16.575767", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"title", | |||||
"description" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Title", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "description", | |||||
"fieldtype": "Long Text", | |||||
"in_list_view": 1, | |||||
"label": "Description", | |||||
"reqd": 1 | |||||
} | |||||
], | |||||
"istable": 1, | |||||
"links": [], | |||||
"modified": "2019-12-26 12:24:09.824084", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appointment Letter content", | |||||
"owner": "Administrator", | |||||
"permissions": [], | |||||
"quick_entry": 1, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,10 @@ | |||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors | |||||
# For license information, please see license.txt | |||||
# import frappe | |||||
from frappe.model.document import Document | |||||
class AppointmentLettercontent(Document): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on('Appointment Letter Template', { | |||||
// refresh: function(frm) { | |||||
// } | |||||
}); |
@@ -0,0 +1,81 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "field:template_name", | |||||
"creation": "2019-12-26 12:20:14.219578", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"template_name", | |||||
"introduction", | |||||
"terms", | |||||
"closing_notes" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "introduction", | |||||
"fieldtype": "Long Text", | |||||
"in_list_view": 1, | |||||
"label": "Introduction", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "closing_notes", | |||||
"fieldtype": "Text", | |||||
"label": "Closing Notes" | |||||
}, | |||||
{ | |||||
"fieldname": "terms", | |||||
"fieldtype": "Table", | |||||
"label": "Terms", | |||||
"options": "Appointment Letter content", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "template_name", | |||||
"fieldtype": "Data", | |||||
"label": "Template Name", | |||||
"reqd": 1, | |||||
"unique": 1 | |||||
} | |||||
], | |||||
"links": [], | |||||
"modified": "2022-01-18 19:25:14.614616", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appointment Letter Template", | |||||
"naming_rule": "By fieldname", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"search_fields": "template_name", | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [], | |||||
"title_field": "template_name", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,10 @@ | |||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors | |||||
# For license information, please see license.txt | |||||
# import frappe | |||||
from frappe.model.document import Document | |||||
class AppointmentLetterTemplate(Document): | |||||
pass |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# See license.txt | |||||
# import frappe | |||||
import unittest | |||||
class TestAppointmentLetterTemplate(unittest.TestCase): | |||||
pass |
@@ -0,0 +1 @@ | |||||
Performance of an Employee in a Time Period against given goals. |
@@ -0,0 +1,148 @@ | |||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
// License: GNU General Public License v3. See license.txt | |||||
frappe.ui.form.on("Appraisal", { | |||||
refresh(frm) { | |||||
if (!frm.doc.__islocal) { | |||||
frm.trigger("add_custom_buttons"); | |||||
frm.trigger("show_feedback_history"); | |||||
frm.trigger("setup_chart"); | |||||
} | |||||
// don't allow removing image (fetched from employee) | |||||
frm.sidebar.image_wrapper.find(".sidebar-image-actions").addClass("hide"); | |||||
}, | |||||
appraisal_template(frm) { | |||||
if (frm.doc.appraisal_template) { | |||||
frm.call("set_kras_and_rating_criteria", () => { | |||||
frm.refresh_field("appraisal_kra"); | |||||
frm.refresh_field("feedback_ratings"); | |||||
}); | |||||
} | |||||
}, | |||||
appraisal_cycle(frm) { | |||||
if (frm.doc.appraisal_cycle) { | |||||
frappe.run_serially([ | |||||
() => { | |||||
if (frm.doc.__islocal && frm.doc.appraisal_cycle) { | |||||
frappe.db.get_value("Appraisal Cycle", frm.doc.appraisal_cycle, "kra_evaluation_method", (r) => { | |||||
if (r.kra_evaluation_method) { | |||||
frm.set_value("rate_goals_manually", cint(r.kra_evaluation_method === "Manual Rating")); | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
() => { | |||||
frm.call({ | |||||
method: "set_appraisal_template", | |||||
doc: frm.doc, | |||||
}); | |||||
} | |||||
]); | |||||
} | |||||
}, | |||||
add_custom_buttons(frm) { | |||||
frm.add_custom_button(__("View Goals"), function() { | |||||
frappe.route_options = { | |||||
company: frm.doc.company, | |||||
employee: frm.doc.employee, | |||||
appraisal_cycle: frm.doc.appraisal_cycle, | |||||
}; | |||||
frappe.set_route("Tree", "Goal"); | |||||
}); | |||||
}, | |||||
show_feedback_history(frm) { | |||||
frappe.require("performance.bundle.js", () => { | |||||
const feedback_history = new hrms.PerformanceFeedback({ | |||||
frm: frm, | |||||
wrapper: $(frm.fields_dict.feedback_html.wrapper), | |||||
}); | |||||
feedback_history.refresh(); | |||||
}); | |||||
}, | |||||
setup_chart(frm) { | |||||
const labels = []; | |||||
const maximum_scores = []; | |||||
const scores = []; | |||||
frm.doc.appraisal_kra.forEach((d) => { | |||||
labels.push(d.kra); | |||||
maximum_scores.push(d.per_weightage || 0); | |||||
scores.push(d.goal_score || 0); | |||||
}); | |||||
if (labels.length && maximum_scores.length && scores.length) { | |||||
frm.dashboard.render_graph({ | |||||
data: { | |||||
labels: labels, | |||||
datasets: [ | |||||
{ | |||||
name: "Maximum Score", | |||||
chartType: "bar", | |||||
values: maximum_scores, | |||||
}, | |||||
{ | |||||
name: "Score Obtained", | |||||
chartType: "bar", | |||||
values: scores, | |||||
} | |||||
] | |||||
}, | |||||
title: __("Scores"), | |||||
height: 250, | |||||
type: "bar", | |||||
barOptions: { | |||||
spaceRatio: 0.7 | |||||
}, | |||||
colors: ["blue", "green"] | |||||
}); | |||||
} | |||||
}, | |||||
calculate_total(frm) { | |||||
let total = 0; | |||||
frm.doc.goals.forEach((d) => { | |||||
total += flt(d.score_earned); | |||||
}); | |||||
frm.set_value("total_score", total); | |||||
} | |||||
}); | |||||
frappe.ui.form.on("Appraisal Goal", { | |||||
score(frm, cdt, cdn) { | |||||
let d = frappe.get_doc(cdt, cdn); | |||||
if (flt(d.score) > 5) { | |||||
frappe.msgprint(__("Score must be less than or equal to 5")); | |||||
d.score = 0; | |||||
refresh_field("score", d.name, "goals"); | |||||
} else { | |||||
frm.trigger("set_score_earned", cdt, cdn); | |||||
} | |||||
}, | |||||
per_weightage(frm, cdt, cdn) { | |||||
frm.trigger("set_score_earned", cdt, cdn); | |||||
}, | |||||
goals_remove(frm, cdt, cdn) { | |||||
frm.trigger("set_score_earned", cdt, cdn); | |||||
}, | |||||
set_score_earned(frm, cdt, cdn) { | |||||
let d = frappe.get_doc(cdt, cdn); | |||||
score_earned = flt(d.score) * flt(d.per_weightage) / 100; | |||||
frappe.model.set_value(cdt, cdn, "score_earned", score_earned); | |||||
frm.trigger("calculate_total"); | |||||
} | |||||
}); |
@@ -0,0 +1,382 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "naming_series:", | |||||
"creation": "2022-08-26 05:55:37.571091", | |||||
"doctype": "DocType", | |||||
"document_type": "Setup", | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"employee_details_tab", | |||||
"naming_series", | |||||
"employee", | |||||
"employee_name", | |||||
"department", | |||||
"designation", | |||||
"employee_image", | |||||
"column_break0", | |||||
"company", | |||||
"status", | |||||
"appraisal_cycle", | |||||
"start_date", | |||||
"end_date", | |||||
"section_break_aeb0", | |||||
"final_score", | |||||
"kra_tab", | |||||
"appraisal_template", | |||||
"rate_goals_manually", | |||||
"section_break_kras", | |||||
"appraisal_kra", | |||||
"goal_score_percentage", | |||||
"section_break_goals", | |||||
"goals", | |||||
"remarks", | |||||
"total_section", | |||||
"total_score", | |||||
"feedback_tab", | |||||
"feedback_html", | |||||
"section_break_20", | |||||
"avg_feedback_score", | |||||
"self_appraisal_tab", | |||||
"section_break_23", | |||||
"self_ratings", | |||||
"self_score", | |||||
"reflections_section", | |||||
"reflections", | |||||
"amended_from" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "naming_series", | |||||
"fieldtype": "Select", | |||||
"label": "Series", | |||||
"no_copy": 1, | |||||
"options": "HR-APR-.YYYY.-", | |||||
"print_hide": 1, | |||||
"reqd": 1, | |||||
"set_only_once": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "employee", | |||||
"fieldtype": "Link", | |||||
"in_global_search": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Employee", | |||||
"oldfieldname": "employee", | |||||
"oldfieldtype": "Link", | |||||
"options": "Employee", | |||||
"reqd": 1, | |||||
"search_index": 1 | |||||
}, | |||||
{ | |||||
"fetch_from": "employee.employee_name", | |||||
"fieldname": "employee_name", | |||||
"fieldtype": "Data", | |||||
"in_global_search": 1, | |||||
"label": "Employee Name", | |||||
"oldfieldname": "employee_name", | |||||
"oldfieldtype": "Data", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "column_break0", | |||||
"fieldtype": "Column Break", | |||||
"oldfieldtype": "Column Break", | |||||
"width": "50%" | |||||
}, | |||||
{ | |||||
"default": "Draft", | |||||
"fieldname": "status", | |||||
"fieldtype": "Select", | |||||
"in_standard_filter": 1, | |||||
"label": "Status", | |||||
"no_copy": 1, | |||||
"oldfieldname": "status", | |||||
"oldfieldtype": "Select", | |||||
"options": "\nDraft\nSubmitted\nCompleted\nCancelled", | |||||
"read_only": 1, | |||||
"reqd": 1, | |||||
"search_index": 1 | |||||
}, | |||||
{ | |||||
"fetch_from": "employee.department", | |||||
"fieldname": "department", | |||||
"fieldtype": "Link", | |||||
"label": "Department", | |||||
"options": "Department", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "company", | |||||
"fieldtype": "Link", | |||||
"label": "Company", | |||||
"oldfieldname": "company", | |||||
"oldfieldtype": "Link", | |||||
"options": "Company", | |||||
"remember_last_selected_value": 1, | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fetch_from": "employee.designation", | |||||
"fieldname": "designation", | |||||
"fieldtype": "Link", | |||||
"label": "Designation", | |||||
"options": "Designation", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "appraisal_cycle", | |||||
"fieldtype": "Link", | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Appraisal Cycle", | |||||
"options": "Appraisal Cycle", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"depends_on": "eval: !doc.rate_goals_manually", | |||||
"fieldname": "appraisal_kra", | |||||
"fieldtype": "Table", | |||||
"label": "KRA vs Goals", | |||||
"oldfieldname": "appraisal_details", | |||||
"oldfieldtype": "Table", | |||||
"options": "Appraisal KRA" | |||||
}, | |||||
{ | |||||
"fieldname": "total_section", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"description": "Out of 5", | |||||
"fieldname": "total_score", | |||||
"fieldtype": "Float", | |||||
"in_list_view": 1, | |||||
"label": "Total Goal Score", | |||||
"no_copy": 1, | |||||
"oldfieldname": "total_score", | |||||
"oldfieldtype": "Currency", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "feedback_html", | |||||
"fieldtype": "HTML", | |||||
"label": "Feedback HTML" | |||||
}, | |||||
{ | |||||
"fieldname": "self_appraisal_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Self Appraisal" | |||||
}, | |||||
{ | |||||
"fieldname": "reflections_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Reflections" | |||||
}, | |||||
{ | |||||
"fieldname": "self_score", | |||||
"fieldtype": "Float", | |||||
"label": "Total Self Score", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "avg_feedback_score", | |||||
"fieldtype": "Float", | |||||
"hidden": 1, | |||||
"label": "Average Feedback Score", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "section_break_20", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"fetch_from": "employee.image", | |||||
"fieldname": "employee_image", | |||||
"fieldtype": "Attach Image", | |||||
"hidden": 1, | |||||
"label": "Employee Image" | |||||
}, | |||||
{ | |||||
"fieldname": "feedback_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Feedback" | |||||
}, | |||||
{ | |||||
"fieldname": "appraisal_template", | |||||
"fieldtype": "Link", | |||||
"in_standard_filter": 1, | |||||
"label": "Appraisal Template", | |||||
"mandatory_depends_on": "eval:!doc.__islocal", | |||||
"oldfieldname": "kra_template", | |||||
"oldfieldtype": "Link", | |||||
"options": "Appraisal Template" | |||||
}, | |||||
{ | |||||
"fieldname": "amended_from", | |||||
"fieldtype": "Link", | |||||
"label": "Amended From", | |||||
"no_copy": 1, | |||||
"options": "Appraisal", | |||||
"print_hide": 1, | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "employee_details_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Overview", | |||||
"oldfieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"fieldname": "section_break_23", | |||||
"fieldtype": "Section Break", | |||||
"label": "Ratings" | |||||
}, | |||||
{ | |||||
"fieldname": "reflections", | |||||
"fieldtype": "Text Editor" | |||||
}, | |||||
{ | |||||
"depends_on": "rate_goals_manually", | |||||
"fieldname": "goals", | |||||
"fieldtype": "Table", | |||||
"label": "Goals", | |||||
"options": "Appraisal Goal" | |||||
}, | |||||
{ | |||||
"depends_on": "rate_goals_manually", | |||||
"description": "Any other remarks, noteworthy effort that should go in the records", | |||||
"fieldname": "remarks", | |||||
"fieldtype": "Text", | |||||
"label": "Remarks" | |||||
}, | |||||
{ | |||||
"fieldname": "section_break_kras", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"fieldname": "section_break_goals", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "rate_goals_manually", | |||||
"fieldtype": "Check", | |||||
"label": "Rate Goals Manually", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "start_date", | |||||
"fieldtype": "Date", | |||||
"label": "Start Date", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "end_date", | |||||
"fieldtype": "Date", | |||||
"label": "End Date", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "kra_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "KRAs", | |||||
"oldfieldtype": "Section Break", | |||||
"show_dashboard": 1 | |||||
}, | |||||
{ | |||||
"depends_on": "eval: !doc.rate_goals_manually", | |||||
"fieldname": "goal_score_percentage", | |||||
"fieldtype": "Float", | |||||
"label": "Goal Score (%)", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "self_ratings", | |||||
"fieldtype": "Table", | |||||
"options": "Employee Feedback Rating" | |||||
}, | |||||
{ | |||||
"fieldname": "section_break_aeb0", | |||||
"fieldtype": "Section Break" | |||||
}, | |||||
{ | |||||
"depends_on": "appraisal_cycle", | |||||
"description": "Average of Goal Score, Feedback Score, and Self Appraisal Score (out of 5)", | |||||
"fieldname": "final_score", | |||||
"fieldtype": "Float", | |||||
"in_list_view": 1, | |||||
"label": "Final Score", | |||||
"read_only": 1 | |||||
} | |||||
], | |||||
"icon": "fa fa-thumbs-up", | |||||
"image_field": "employee_image", | |||||
"index_web_pages_for_search": 1, | |||||
"is_submittable": 1, | |||||
"links": [], | |||||
"modified": "2023-03-30 21:02:16.425253", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appraisal", | |||||
"naming_rule": "By \"Naming Series\" field", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "Employee", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"amend": 1, | |||||
"cancel": 1, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"submit": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"amend": 1, | |||||
"cancel": 1, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR User", | |||||
"share": 1, | |||||
"submit": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR Manager", | |||||
"share": 1, | |||||
"submit": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"search_fields": "status, employee, employee_name, appraisal_cycle", | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [], | |||||
"timeline_field": "employee", | |||||
"title_field": "employee_name", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,314 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# License: GNU General Public License v3. See license.txt | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.model.document import Document | |||||
from frappe.query_builder.functions import Avg | |||||
from frappe.utils import flt, get_link_to_form, now | |||||
from hrms.hr.doctype.appraisal_cycle.appraisal_cycle import validate_active_appraisal_cycle | |||||
from hrms.hr.utils import validate_active_employee | |||||
class Appraisal(Document): | |||||
def validate(self): | |||||
if not self.status: | |||||
self.status = "Draft" | |||||
self.set_kra_evaluation_method() | |||||
validate_active_employee(self.employee) | |||||
validate_active_appraisal_cycle(self.appraisal_cycle) | |||||
self.validate_duplicate() | |||||
self.set_goal_score() | |||||
self.calculate_self_appraisal_score() | |||||
self.calculate_avg_feedback_score() | |||||
self.calculate_final_score() | |||||
def validate_duplicate(self): | |||||
Appraisal = frappe.qb.DocType("Appraisal") | |||||
duplicate = ( | |||||
frappe.qb.from_(Appraisal) | |||||
.select(Appraisal.name) | |||||
.where( | |||||
(Appraisal.employee == self.employee) | |||||
& (Appraisal.docstatus != 2) | |||||
& (Appraisal.name != self.name) | |||||
& ( | |||||
(Appraisal.appraisal_cycle == self.appraisal_cycle) | |||||
| ( | |||||
(Appraisal.start_date.between(self.start_date, self.end_date)) | |||||
| (Appraisal.end_date.between(self.start_date, self.end_date)) | |||||
| ((self.start_date >= Appraisal.start_date) & (self.start_date <= Appraisal.end_date)) | |||||
| ((self.end_date >= Appraisal.start_date) & (self.end_date <= Appraisal.end_date)) | |||||
) | |||||
) | |||||
) | |||||
).run() | |||||
duplicate = duplicate[0][0] if duplicate else 0 | |||||
if duplicate: | |||||
frappe.throw( | |||||
_( | |||||
"Appraisal {0} already exists for Employee {1} for this Appraisal Cycle or overlapping period" | |||||
).format( | |||||
get_link_to_form("Appraisal", duplicate), frappe.bold(self.employee_name) | |||||
), | |||||
exc=frappe.DuplicateEntryError, | |||||
title=_("Duplicate Entry"), | |||||
) | |||||
def set_kra_evaluation_method(self): | |||||
if ( | |||||
self.is_new() | |||||
and self.appraisal_cycle | |||||
and ( | |||||
frappe.db.get_value("Appraisal Cycle", self.appraisal_cycle, "kra_evaluation_method") | |||||
== "Manual Rating" | |||||
) | |||||
): | |||||
self.rate_goals_manually = 1 | |||||
@frappe.whitelist() | |||||
def set_appraisal_template(self): | |||||
"""Sets appraisal template from Appraisee table in Cycle""" | |||||
if not self.appraisal_cycle: | |||||
return | |||||
appraisal_template = frappe.db.get_value( | |||||
"Appraisee", | |||||
{ | |||||
"employee": self.employee, | |||||
"parent": self.appraisal_cycle, | |||||
}, | |||||
"appraisal_template", | |||||
) | |||||
if appraisal_template: | |||||
self.appraisal_template = appraisal_template | |||||
self.set_kras_and_rating_criteria() | |||||
@frappe.whitelist() | |||||
def set_kras_and_rating_criteria(self): | |||||
if not self.appraisal_template: | |||||
return | |||||
self.set("appraisal_kra", []) | |||||
self.set("self_ratings", []) | |||||
self.set("goals", []) | |||||
template = frappe.get_doc("Appraisal Template", self.appraisal_template) | |||||
for entry in template.goals: | |||||
table_name = "goals" if self.rate_goals_manually else "appraisal_kra" | |||||
self.append( | |||||
table_name, | |||||
{ | |||||
"kra": entry.key_result_area, | |||||
"per_weightage": entry.per_weightage, | |||||
}, | |||||
) | |||||
for entry in template.rating_criteria: | |||||
self.append( | |||||
"self_ratings", | |||||
{ | |||||
"criteria": entry.criteria, | |||||
"per_weightage": entry.per_weightage, | |||||
}, | |||||
) | |||||
return self | |||||
def calculate_total_score(self): | |||||
total_weightage, total, goal_score_percentage = 0, 0, 0 | |||||
if self.rate_goals_manually: | |||||
table = _("Goals") | |||||
for entry in self.goals: | |||||
if flt(entry.score) > 5: | |||||
frappe.throw(_("Row {0}: Goal Score cannot be greater than 5").format(entry.idx)) | |||||
entry.score_earned = flt(entry.score) * flt(entry.per_weightage) / 100 | |||||
total += flt(entry.score_earned) | |||||
total_weightage += flt(entry.per_weightage) | |||||
else: | |||||
table = _("KRAs") | |||||
for entry in self.appraisal_kra: | |||||
goal_score_percentage += flt(entry.goal_score) | |||||
total_weightage += flt(entry.per_weightage) | |||||
self.goal_score_percentage = flt(goal_score_percentage, self.precision("goal_score_percentage")) | |||||
# convert goal score percentage to total score out of 5 | |||||
total = flt(goal_score_percentage) / 20 | |||||
if total_weightage and flt(total_weightage, 2) != 100.0: | |||||
frappe.throw( | |||||
_("Total weightage for all {0} must add up to 100. Currently, it is {1}%").format( | |||||
table, total_weightage | |||||
), | |||||
title=_("Incorrect Weightage Allocation"), | |||||
) | |||||
self.total_score = flt(total, self.precision("total_score")) | |||||
def calculate_self_appraisal_score(self): | |||||
total = 0 | |||||
for entry in self.self_ratings: | |||||
score = flt(entry.rating) * 5 * flt(entry.per_weightage / 100) | |||||
total += flt(score) | |||||
self.self_score = flt(total, self.precision("self_score")) | |||||
def calculate_avg_feedback_score(self, update=False): | |||||
avg_feedback_score = frappe.qb.avg( | |||||
"Employee Performance Feedback", | |||||
"total_score", | |||||
{"employee": self.employee, "appraisal": self.name, "docstatus": 1}, | |||||
) | |||||
self.avg_feedback_score = flt(avg_feedback_score, self.precision("avg_feedback_score")) | |||||
if update: | |||||
self.calculate_final_score() | |||||
self.db_update() | |||||
def calculate_final_score(self): | |||||
final_score = (flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score)) / 3 | |||||
self.final_score = flt(final_score, self.precision("final_score")) | |||||
@frappe.whitelist() | |||||
def add_feedback(self, feedback, feedback_ratings): | |||||
feedback = frappe.get_doc( | |||||
{ | |||||
"doctype": "Employee Performance Feedback", | |||||
"appraisal": self.name, | |||||
"employee": self.employee, | |||||
"added_on": now(), | |||||
"feedback": feedback, | |||||
"reviewer": frappe.db.get_value("Employee", {"user_id": frappe.session.user}), | |||||
} | |||||
) | |||||
for entry in feedback_ratings: | |||||
feedback.append( | |||||
"feedback_ratings", | |||||
{ | |||||
"criteria": entry.get("criteria"), | |||||
"rating": entry.get("rating"), | |||||
"per_weightage": entry.get("per_weightage"), | |||||
}, | |||||
) | |||||
feedback.submit() | |||||
return feedback | |||||
def set_goal_score(self, update=False): | |||||
for kra in self.appraisal_kra: | |||||
# update progress for all goals as KRA linked could be removed or changed | |||||
Goal = frappe.qb.DocType("Goal") | |||||
avg_goal_completion = ( | |||||
frappe.qb.from_(Goal) | |||||
.select(Avg(Goal.progress).as_("avg_goal_completion")) | |||||
.where( | |||||
(Goal.kra == kra.kra) | |||||
& (Goal.employee == self.employee) | |||||
# archived goals should not contribute to progress | |||||
& (Goal.status != "Archived") | |||||
& ((Goal.parent_goal == "") | (Goal.parent_goal.isnull())) | |||||
& (Goal.appraisal_cycle == self.appraisal_cycle) | |||||
) | |||||
).run()[0][0] | |||||
kra.goal_completion = flt(avg_goal_completion, kra.precision("goal_completion")) | |||||
kra.goal_score = flt(kra.goal_completion * kra.per_weightage / 100, kra.precision("goal_score")) | |||||
if update: | |||||
kra.db_update() | |||||
self.calculate_total_score() | |||||
if update: | |||||
self.calculate_final_score() | |||||
self.db_update() | |||||
return self | |||||
@frappe.whitelist() | |||||
def get_feedback_history(employee, appraisal): | |||||
data = frappe._dict() | |||||
data.feedback_history = frappe.get_list( | |||||
"Employee Performance Feedback", | |||||
filters={"employee": employee, "appraisal": appraisal, "docstatus": 1}, | |||||
fields=[ | |||||
"feedback", | |||||
"reviewer", | |||||
"user", | |||||
"owner", | |||||
"reviewer_name", | |||||
"reviewer_designation", | |||||
"added_on", | |||||
"employee", | |||||
"total_score", | |||||
"name", | |||||
], | |||||
order_by="added_on desc", | |||||
) | |||||
# get percentage of reviews per rating | |||||
reviews_per_rating = [] | |||||
feedback_count = frappe.db.count( | |||||
"Employee Performance Feedback", | |||||
filters={ | |||||
"appraisal": appraisal, | |||||
"employee": employee, | |||||
"docstatus": 1, | |||||
}, | |||||
) | |||||
for i in range(1, 6): | |||||
count = frappe.db.count( | |||||
"Employee Performance Feedback", | |||||
filters={ | |||||
"appraisal": appraisal, | |||||
"employee": employee, | |||||
"total_score": ("between", [i, i + 0.99]), | |||||
"docstatus": 1, | |||||
}, | |||||
) | |||||
percent = flt((count / feedback_count) * 100, 0) if feedback_count else 0 | |||||
reviews_per_rating.append(percent) | |||||
data.reviews_per_rating = reviews_per_rating | |||||
data.avg_feedback_score = frappe.db.get_value("Appraisal", appraisal, "avg_feedback_score") | |||||
return data | |||||
@frappe.whitelist() | |||||
@frappe.validate_and_sanitize_search_inputs | |||||
def get_kras_for_employee(doctype, txt, searchfield, start, page_len, filters): | |||||
appraisal = frappe.db.get_value( | |||||
"Appraisal", | |||||
{ | |||||
"appraisal_cycle": filters.get("appraisal_cycle"), | |||||
"employee": filters.get("employee"), | |||||
}, | |||||
"name", | |||||
) | |||||
return frappe.get_all( | |||||
"Appraisal KRA", | |||||
filters={"parent": appraisal, "kra": ("like", "{0}%".format(txt))}, | |||||
fields=["kra"], | |||||
as_list=1, | |||||
) |
@@ -0,0 +1,324 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors | |||||
# See license.txt | |||||
import unittest | |||||
import frappe | |||||
from frappe.tests.utils import FrappeTestCase | |||||
from erpnext.setup.doctype.designation.test_designation import create_designation | |||||
from erpnext.setup.doctype.employee.test_employee import make_employee | |||||
from hrms.hr.doctype.appraisal_cycle.appraisal_cycle import get_appraisal_cycle_summary | |||||
from hrms.hr.doctype.appraisal_cycle.test_appraisal_cycle import create_appraisal_cycle | |||||
from hrms.hr.doctype.appraisal_template.test_appraisal_template import create_appraisal_template | |||||
from hrms.hr.doctype.employee_performance_feedback.test_employee_performance_feedback import ( | |||||
create_performance_feedback, | |||||
) | |||||
from hrms.hr.doctype.goal.test_goal import create_goal | |||||
from hrms.tests.test_utils import create_company | |||||
class TestAppraisal(FrappeTestCase): | |||||
def setUp(self): | |||||
frappe.db.delete("Goal") | |||||
frappe.db.delete("Appraisal") | |||||
frappe.db.delete("Employee Performance Feedback") | |||||
self.company = create_company("_Test Appraisal").name | |||||
self.template = create_appraisal_template() | |||||
engineer = create_designation(designation_name="Engineer") | |||||
engineer.appraisal_template = self.template.name | |||||
engineer.save() | |||||
self.employee1 = make_employee( | |||||
"employee1@example.com", company=self.company, designation="Engineer" | |||||
) | |||||
def test_validate_duplicate(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
appraisal = frappe.get_doc( | |||||
{ | |||||
"doctype": "Appraisal", | |||||
"employee": self.employee1, | |||||
"appraisal_cycle": cycle.name, | |||||
} | |||||
) | |||||
appraisal.set_appraisal_template() | |||||
self.assertRaises(frappe.DuplicateEntryError, appraisal.insert) | |||||
def test_manual_kra_rating(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating") | |||||
cycle.create_appraisals() | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# 30% weightage | |||||
appraisal.goals[0].score = 5 | |||||
# 70% weightage | |||||
appraisal.goals[1].score = 3 | |||||
appraisal.save() | |||||
self.assertEqual(appraisal.goals[0].score_earned, 1.5) | |||||
self.assertEqual(appraisal.goals[1].score_earned, 2.1) | |||||
self.assertEqual(appraisal.total_score, 3.6) | |||||
self.assertEqual(appraisal.final_score, 1.2) | |||||
def test_final_score(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating") | |||||
cycle.create_appraisals() | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# GOAL SCORE | |||||
appraisal.goals[0].score = 5 # 30% weightage | |||||
appraisal.goals[1].score = 3 # 70% weightage | |||||
# SELF APPRAISAL SCORE | |||||
ratings = appraisal.self_ratings | |||||
ratings[0].rating = 0.8 # 70% weightage | |||||
ratings[1].rating = 0.7 # 30% weightage | |||||
appraisal.save() | |||||
# FEEDBACK SCORE | |||||
reviewer = make_employee("reviewer1@example.com", designation="Engineer") | |||||
feedback = create_performance_feedback( | |||||
self.employee1, | |||||
reviewer, | |||||
appraisal.name, | |||||
) | |||||
ratings = feedback.feedback_ratings | |||||
ratings[0].rating = 0.8 # 70% weightage | |||||
ratings[1].rating = 0.7 # 30% weightage | |||||
feedback.submit() | |||||
appraisal.reload() | |||||
self.assertEqual(appraisal.final_score, 3.767) | |||||
def test_goal_score(self): | |||||
""" | |||||
parent1 (12.5%) (Quality) | |||||
|_ child1 (12.5%) | |||||
|_ child1_1 (25%) | |||||
|_ child1_2 | |||||
parent2 (50%) (Development) | |||||
|_ child2_1 (100%) | |||||
|_ child2_2 | |||||
""" | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
parent1 = create_goal(self.employee1, "Quality", 1, appraisal_cycle=cycle.name) | |||||
child1 = create_goal(self.employee1, is_group=1, parent_goal=parent1.name) | |||||
child1_1 = create_goal(self.employee1, parent_goal=child1.name, progress=25) | |||||
child1_2 = create_goal(self.employee1, parent_goal=child1.name) | |||||
parent2 = create_goal(self.employee1, "Development", 1, appraisal_cycle=cycle.name) | |||||
child2_1 = create_goal(self.employee1, parent_goal=parent2.name, progress=100) | |||||
child2_2 = create_goal(self.employee1, parent_goal=parent2.name) | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# Quality KRA, 30% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 12.5) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 3.75) | |||||
# Development KRA, 70% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_score, 35) | |||||
self.assertEqual(appraisal.goal_score_percentage, 38.75) | |||||
self.assertEqual(appraisal.total_score, 1.938) | |||||
self.assertEqual(appraisal.final_score, 0.646) | |||||
def test_goal_score_after_parent_goal_change(self): | |||||
""" | |||||
BEFORE | |||||
parent1 (50%) (Quality) | |||||
|_ child1 (50%) | |||||
parent2 (25%) (Development) | |||||
|_ child2_1 (50%) | |||||
|_ child2_2 | |||||
AFTER | |||||
parent1 (50%) (Quality) | |||||
|_ child1 (50%) | |||||
|_ child2_1 (50%) | |||||
parent2 (0%) (Development) | |||||
|_ child2_2 | |||||
""" | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
parent1 = create_goal(self.employee1, "Quality", 1, appraisal_cycle=cycle.name) | |||||
child1 = create_goal(self.employee1, parent_goal=parent1.name, progress=50) | |||||
parent2 = create_goal(self.employee1, "Development", 1, appraisal_cycle=cycle.name) | |||||
child2_1 = create_goal(self.employee1, parent_goal=parent2.name, progress=50) | |||||
child2_2 = create_goal(self.employee1, parent_goal=parent2.name) | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# Quality KRA, 30% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 15) | |||||
# Development KRA, 70% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_completion, 25) | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_score, 17.5) | |||||
# Parent changed. Old parent's KRA score should be updated | |||||
child2_1.parent_goal = parent1.name | |||||
child2_1.save() | |||||
appraisal.reload() | |||||
# Quality KRA, 30% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 15) | |||||
# Development KRA, 70% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_completion, 0) | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_score, 0) | |||||
def test_goal_score_after_kra_change(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
goal = create_goal(self.employee1, "Quality", appraisal_cycle=cycle.name, progress=50) | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# Quality KRA, 30% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 15) | |||||
goal.kra = "Development" | |||||
goal.save() | |||||
# goal completion should now contribute to Development KRA score, instead of Quality (row 1) | |||||
appraisal.reload() | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 0) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 0) | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[1].goal_score, 35) | |||||
def test_goal_score_after_goal_deletion(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
goal = create_goal(self.employee1, "Quality", appraisal_cycle=cycle.name, progress=50) | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
# Quality KRA, 30% weightage | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 50) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 15) | |||||
goal.delete() | |||||
appraisal.reload() | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_completion, 0) | |||||
self.assertEqual(appraisal.appraisal_kra[0].goal_score, 0) | |||||
def test_calculate_self_appraisal_score(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
ratings = appraisal.self_ratings | |||||
# 70% weightage | |||||
ratings[0].rating = 0.8 | |||||
# 30% weightage | |||||
ratings[1].rating = 0.7 | |||||
appraisal.save() | |||||
self.assertEqual(appraisal.self_score, 3.85) | |||||
def test_cycle_completion(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
# unsubmitted appraisals | |||||
self.assertRaises(frappe.ValidationError, cycle.complete_cycle) | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
appraisal.submit() | |||||
cycle.complete_cycle() | |||||
appraisal = frappe.get_doc( | |||||
{ | |||||
"doctype": "Appraisal", | |||||
"employee": self.employee1, | |||||
"appraisal_cycle": cycle.name, | |||||
"appraisal_template": self.template.name, | |||||
} | |||||
) | |||||
# transaction against a Completed cycle | |||||
self.assertRaises(frappe.ValidationError, appraisal.insert) | |||||
def test_cycle_summary(self): | |||||
employee2 = make_employee("employee2@example.com", company=self.company, designation="Engineer") | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
appraisal = frappe.db.exists( | |||||
"Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1} | |||||
) | |||||
appraisal = frappe.get_doc("Appraisal", appraisal) | |||||
goal = create_goal(self.employee1, "Quality", appraisal_cycle=cycle.name) | |||||
feedback = create_performance_feedback( | |||||
self.employee1, | |||||
employee2, | |||||
appraisal.name, | |||||
) | |||||
ratings = feedback.feedback_ratings | |||||
ratings[0].rating = 0.8 # 70% weightage | |||||
ratings[1].rating = 0.7 # 30% weightage | |||||
feedback.submit() | |||||
summary = get_appraisal_cycle_summary(cycle.name) | |||||
expected_data = { | |||||
"appraisees": 2, | |||||
"self_appraisal_pending": 2, | |||||
"goals_missing": 1, | |||||
"feedback_missing": 1, | |||||
} | |||||
self.assertEqual(summary, expected_data) |
@@ -0,0 +1,120 @@ | |||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors | |||||
// For license information, please see license.txt | |||||
frappe.ui.form.on("Appraisal Cycle", { | |||||
refresh(frm) { | |||||
frm.set_query("department", () => { | |||||
return { | |||||
filters: { | |||||
company: frm.doc.company | |||||
} | |||||
} | |||||
}); | |||||
frm.trigger("show_custom_buttons"); | |||||
frm.trigger("show_appraisal_summary"); | |||||
}, | |||||
show_custom_buttons(frm) { | |||||
if (frm.doc.__islocal) return; | |||||
frm.add_custom_button(__("View Goals"), () => { | |||||
frappe.route_options = { | |||||
company: frm.doc.company, | |||||
appraisal_cycle: frm.doc.name, | |||||
}; | |||||
frappe.set_route("Tree", "Goal"); | |||||
}); | |||||
let className = ""; | |||||
let appraisals_created = frm.doc.__onload?.appraisals_created; | |||||
if (frm.doc.status !== "Completed") { | |||||
className = appraisals_created ? "btn-default": "btn-primary"; | |||||
frm.add_custom_button(__("Create Appraisals"), () => { | |||||
frm.trigger("create_appraisals"); | |||||
}).addClass(className); | |||||
} | |||||
className = appraisals_created ? "btn-primary": "btn-default"; | |||||
if (frm.doc.status === "Not Started") { | |||||
frm.add_custom_button(__("Start"), () => { | |||||
frm.set_value("status", "In Progress"); | |||||
frm.save(); | |||||
}).addClass(className); | |||||
} else if (frm.doc.status === "In Progress") { | |||||
frm.add_custom_button(__("Mark as Completed"), () => { | |||||
frm.trigger("complete_cycle"); | |||||
}).addClass(className); | |||||
} else if (frm.doc.status === "Completed") { | |||||
frm.add_custom_button(__("Mark as In Progress"), () => { | |||||
frm.set_value("status", "In Progress"); | |||||
frm.save(); | |||||
}); | |||||
} | |||||
}, | |||||
get_employees(frm) { | |||||
frappe.call({ | |||||
method: "set_employees", | |||||
doc: frm.doc, | |||||
freeze: true, | |||||
freeze_message: __("Fetching Employees"), | |||||
callback: function() { | |||||
refresh_field("appraisees"); | |||||
frm.dirty(); | |||||
} | |||||
}); | |||||
}, | |||||
create_appraisals(frm) { | |||||
frm.call({ | |||||
method: "create_appraisals", | |||||
doc: frm.doc, | |||||
freeze: true, | |||||
}).then((r) => { | |||||
if (!r.exc) { | |||||
frm.reload_doc(); | |||||
} | |||||
}); | |||||
}, | |||||
complete_cycle(frm) { | |||||
let msg = __("This action will prevent making changes to the linked appraisal feedback/goals."); | |||||
msg += "<br>"; | |||||
msg += __("Are you sure you want to proceed?"); | |||||
frappe.confirm( | |||||
msg, | |||||
() => { | |||||
frm.call({ | |||||
method: "complete_cycle", | |||||
doc: frm.doc, | |||||
freeze: true, | |||||
}).then((r) => { | |||||
if (!r.exc) { | |||||
frm.reload_doc(); | |||||
} | |||||
}); | |||||
} | |||||
); | |||||
}, | |||||
show_appraisal_summary(frm) { | |||||
if (frm.doc.__islocal) return; | |||||
frappe.call( | |||||
"hrms.hr.doctype.appraisal_cycle.appraisal_cycle.get_appraisal_cycle_summary", | |||||
{cycle_name: frm.doc.name} | |||||
).then(r => { | |||||
if (r.message) { | |||||
frm.dashboard.add_indicator(__("Appraisees: {0}", [r.message.appraisees]), "blue"); | |||||
frm.dashboard.add_indicator(__("Self Appraisal Pending: {0}", [r.message.self_appraisal_pending]), "orange"); | |||||
frm.dashboard.add_indicator(__("Employees without Feedback: {0}", [r.message.feedback_missing]), "orange"); | |||||
frm.dashboard.add_indicator(__("Employees without Goals: {0}", [r.message.goals_missing]), "orange"); | |||||
} | |||||
}); | |||||
} | |||||
}); |
@@ -0,0 +1,245 @@ | |||||
{ | |||||
"actions": [], | |||||
"allow_rename": 1, | |||||
"autoname": "field:cycle_name", | |||||
"creation": "2022-08-24 15:05:29.694466", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"overview_tab", | |||||
"cycle_name", | |||||
"company", | |||||
"status", | |||||
"column_break_3", | |||||
"start_date", | |||||
"end_date", | |||||
"section_break_4", | |||||
"description", | |||||
"settings_section", | |||||
"kra_evaluation_method", | |||||
"applicable_for_tab", | |||||
"filters_section", | |||||
"branch", | |||||
"department", | |||||
"column_break_11", | |||||
"designation", | |||||
"employees_section", | |||||
"get_employees", | |||||
"appraisees" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "start_date", | |||||
"fieldtype": "Date", | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Start Date", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"allow_in_quick_entry": 1, | |||||
"fieldname": "end_date", | |||||
"fieldtype": "Date", | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "End Date", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"collapsible": 1, | |||||
"fieldname": "section_break_4", | |||||
"fieldtype": "Section Break", | |||||
"label": "Description" | |||||
}, | |||||
{ | |||||
"allow_in_quick_entry": 1, | |||||
"fieldname": "description", | |||||
"fieldtype": "Text Editor" | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_3", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"fieldname": "applicable_for_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Applicable For" | |||||
}, | |||||
{ | |||||
"collapsible": 1, | |||||
"collapsible_depends_on": "eval: doc.__islocal", | |||||
"description": "Set optional filters to fetch employees in the appraisee list", | |||||
"fieldname": "filters_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Filters" | |||||
}, | |||||
{ | |||||
"fieldname": "branch", | |||||
"fieldtype": "Link", | |||||
"label": "Branch", | |||||
"options": "Branch" | |||||
}, | |||||
{ | |||||
"fieldname": "department", | |||||
"fieldtype": "Link", | |||||
"label": "Department", | |||||
"options": "Department" | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_11", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"fieldname": "designation", | |||||
"fieldtype": "Link", | |||||
"label": "Designation", | |||||
"options": "Designation" | |||||
}, | |||||
{ | |||||
"fieldname": "get_employees", | |||||
"fieldtype": "Button", | |||||
"label": "Get Employees" | |||||
}, | |||||
{ | |||||
"fieldname": "company", | |||||
"fieldtype": "Link", | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Company", | |||||
"options": "Company", | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "employees_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Employees" | |||||
}, | |||||
{ | |||||
"fieldname": "appraisees", | |||||
"fieldtype": "Table", | |||||
"options": "Appraisee" | |||||
}, | |||||
{ | |||||
"fieldname": "cycle_name", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Cycle Name", | |||||
"reqd": 1, | |||||
"unique": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "overview_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Overview" | |||||
}, | |||||
{ | |||||
"fieldname": "settings_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Settings" | |||||
}, | |||||
{ | |||||
"default": "Automated Based on Goal Progress", | |||||
"fieldname": "kra_evaluation_method", | |||||
"fieldtype": "Select", | |||||
"label": "KRA Evaluation Method", | |||||
"options": "Automated Based on Goal Progress\nManual Rating" | |||||
}, | |||||
{ | |||||
"default": "Not Started", | |||||
"fieldname": "status", | |||||
"fieldtype": "Select", | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 1, | |||||
"label": "Status", | |||||
"options": "Not Started\nIn Progress\nCompleted", | |||||
"read_only": 1 | |||||
} | |||||
], | |||||
"index_web_pages_for_search": 1, | |||||
"links": [ | |||||
{ | |||||
"link_doctype": "Appraisal", | |||||
"link_fieldname": "appraisal_cycle" | |||||
}, | |||||
{ | |||||
"link_doctype": "Employee Performance Feedback", | |||||
"link_fieldname": "appraisal_cycle" | |||||
}, | |||||
{ | |||||
"link_doctype": "Goal", | |||||
"link_fieldname": "appraisal_cycle" | |||||
} | |||||
], | |||||
"modified": "2023-03-29 12:28:36.247120", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appraisal Cycle", | |||||
"naming_rule": "By fieldname", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"create": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "HR User", | |||||
"share": 1, | |||||
"write": 1 | |||||
}, | |||||
{ | |||||
"email": 1, | |||||
"export": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 1, | |||||
"role": "Employee", | |||||
"select": 1, | |||||
"share": 1 | |||||
} | |||||
], | |||||
"search_fields": "start_date, end_date", | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [ | |||||
{ | |||||
"color": "Gray", | |||||
"title": "Not Started" | |||||
}, | |||||
{ | |||||
"color": "Orange", | |||||
"title": "In Progress" | |||||
}, | |||||
{ | |||||
"color": "Green", | |||||
"title": "Completed" | |||||
} | |||||
], | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,272 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors | |||||
# For license information, please see license.txt | |||||
import frappe | |||||
from frappe import _ | |||||
from frappe.model.document import Document | |||||
from frappe.query_builder.functions import Count | |||||
from frappe.query_builder.terms import SubQuery | |||||
class AppraisalCycle(Document): | |||||
def onload(self): | |||||
self.set_onload("appraisals_created", self.check_if_appraisals_exist()) | |||||
def validate(self): | |||||
self.validate_from_to_dates("start_date", "end_date") | |||||
self.validate_evaluation_method_change() | |||||
def validate_evaluation_method_change(self): | |||||
if self.is_new(): | |||||
return | |||||
if self.has_value_changed("kra_evaluation_method") and self.check_if_appraisals_exist(): | |||||
frappe.throw( | |||||
_( | |||||
"Evaluation Method cannot be changed as there are existing appraisals created for this cycle" | |||||
), | |||||
title=_("Not Allowed"), | |||||
) | |||||
def check_if_appraisals_exist(self): | |||||
return frappe.db.exists( | |||||
"Appraisal", | |||||
{"appraisal_cycle": self.name, "docstatus": ["!=", 2]}, | |||||
) | |||||
@frappe.whitelist() | |||||
def set_employees(self): | |||||
"""Pull employees in appraisee list based on selected filters""" | |||||
employees = self.get_employees_for_appraisal() | |||||
appraisal_templates = self.get_appraisal_template_map() | |||||
if employees: | |||||
self.set("appraisees", []) | |||||
template_missing = False | |||||
for data in employees: | |||||
if not appraisal_templates.get(data.designation): | |||||
template_missing = True | |||||
self.append( | |||||
"appraisees", | |||||
{ | |||||
"employee": data.name, | |||||
"employee_name": data.employee_name, | |||||
"branch": data.branch, | |||||
"designation": data.designation, | |||||
"department": data.department, | |||||
"appraisal_template": appraisal_templates.get(data.designation), | |||||
}, | |||||
) | |||||
if template_missing: | |||||
self.show_missing_template_message() | |||||
else: | |||||
self.set("appraisees", []) | |||||
frappe.msgprint(_("No employees found for the selected criteria")) | |||||
return self | |||||
def get_employees_for_appraisal(self): | |||||
filters = { | |||||
"status": "Active", | |||||
"company": self.company, | |||||
} | |||||
if self.department: | |||||
filters["department"] = self.department | |||||
if self.branch: | |||||
filters["branch"] = self.branch | |||||
if self.designation: | |||||
filters["designation"] = self.designation | |||||
employees = frappe.db.get_all( | |||||
"Employee", | |||||
filters=filters, | |||||
fields=[ | |||||
"name", | |||||
"employee_name", | |||||
"branch", | |||||
"designation", | |||||
"department", | |||||
], | |||||
) | |||||
return employees | |||||
def get_appraisal_template_map(self): | |||||
designations = frappe.get_all("Designation", fields=["name", "appraisal_template"]) | |||||
appraisal_templates = frappe._dict() | |||||
for entry in designations: | |||||
appraisal_templates[entry.name] = entry.appraisal_template | |||||
return appraisal_templates | |||||
@frappe.whitelist() | |||||
def create_appraisals(self): | |||||
self.check_permission("write") | |||||
if not self.appraisees: | |||||
frappe.throw( | |||||
_("Please select employees to create appraisals for"), title=_("No Employees Selected") | |||||
) | |||||
if not all(appraisee.appraisal_template for appraisee in self.appraisees): | |||||
self.show_missing_template_message(raise_exception=True) | |||||
if len(self.appraisees) > 30: | |||||
frappe.enqueue( | |||||
create_appraisals_for_cycle, | |||||
queue="long", | |||||
timeout=600, | |||||
appraisal_cycle=self, | |||||
) | |||||
frappe.msgprint( | |||||
_("Appraisal creation is queued. It may take a few minutes."), | |||||
alert=True, | |||||
indicator="blue", | |||||
) | |||||
else: | |||||
create_appraisals_for_cycle(self, publish_progress=True) | |||||
# since this method is called via frm.call this doc needs to be updated manually | |||||
self.reload() | |||||
def show_missing_template_message(self, raise_exception=False): | |||||
msg = _("Appraisal Template not found for some designations.") | |||||
msg += "<br><br>" | |||||
msg += _( | |||||
"Please set the Appraisal Template for all the {0} or select the template in the Employees table below." | |||||
).format(f"""<a href='{frappe.utils.get_url_to_list("Designation")}'>Designations</a>""") | |||||
frappe.msgprint( | |||||
msg, title=_("Appraisal Template Missing"), indicator="yellow", raise_exception=raise_exception | |||||
) | |||||
@frappe.whitelist() | |||||
def complete_cycle(self): | |||||
self.check_permission("write") | |||||
draft_appraisals = frappe.db.count("Appraisal", {"appraisal_cycle": self.name, "docstatus": 0}) | |||||
if draft_appraisals: | |||||
link = frappe.utils.get_url_to_list("Appraisal") + f"?status=Draft&appraisal_cycle={self.name}" | |||||
link = f"""<a href="{link}">documents</a>""" | |||||
msg = _("{0} Appraisal(s) are not submitted yet").format(frappe.bold(draft_appraisals)) | |||||
msg += "<br><br>" | |||||
msg += _("Please submit the {0} before marking the cycle as Completed").format(link) | |||||
frappe.throw(msg, title=_("Unsubmitted Appraisals")) | |||||
self.status = "Completed" | |||||
self.save() | |||||
def create_appraisals_for_cycle(appraisal_cycle: AppraisalCycle, publish_progress: bool = False): | |||||
""" | |||||
Creates appraisals for employees in the appraisee list of appraisal cycle, | |||||
if not already created | |||||
""" | |||||
count = 0 | |||||
for employee in appraisal_cycle.appraisees: | |||||
try: | |||||
appraisal = frappe.get_doc( | |||||
{ | |||||
"doctype": "Appraisal", | |||||
"appraisal_template": employee.appraisal_template, | |||||
"employee": employee.employee, | |||||
"appraisal_cycle": appraisal_cycle.name, | |||||
} | |||||
) | |||||
appraisal.rate_goals_manually = ( | |||||
1 if appraisal_cycle.kra_evaluation_method == "Manual Rating" else 0 | |||||
) | |||||
appraisal.set_kras_and_rating_criteria() | |||||
appraisal.insert() | |||||
if publish_progress: | |||||
count += 1 | |||||
frappe.publish_progress( | |||||
count * 100 / len(appraisal_cycle.appraisees), title=_("Creating Appraisals") + "..." | |||||
) | |||||
except frappe.DuplicateEntryError: | |||||
# already exists | |||||
pass | |||||
def validate_active_appraisal_cycle(appraisal_cycle: str) -> None: | |||||
if frappe.db.get_value("Appraisal Cycle", appraisal_cycle, "status") == "Completed": | |||||
msg = _("Cannot create or change transactions against a {0} Appraisal Cycle.").format( | |||||
frappe.bold("Completed") | |||||
) | |||||
msg += "<br><br>" | |||||
msg += _("Mark the cycle as {0} if required.").format(frappe.bold("In Progress")) | |||||
frappe.throw(msg, title=_("Not Allowed")) | |||||
@frappe.whitelist() | |||||
def get_appraisal_cycle_summary(cycle_name: str) -> dict: | |||||
summary = frappe._dict() | |||||
summary["appraisees"] = frappe.db.count( | |||||
"Appraisal", {"appraisal_cycle": cycle_name, "docstatus": ("!=", 2)} | |||||
) | |||||
summary["self_appraisal_pending"] = frappe.db.count( | |||||
"Appraisal", {"appraisal_cycle": cycle_name, "docstatus": 0, "self_score": 0} | |||||
) | |||||
summary["goals_missing"] = get_employees_without_goals(cycle_name) | |||||
summary["feedback_missing"] = get_employees_without_feedback(cycle_name) | |||||
return summary | |||||
def get_employees_without_goals(cycle_name: str) -> int: | |||||
Goal = frappe.qb.DocType("Goal") | |||||
Appraisal = frappe.qb.DocType("Appraisal") | |||||
count = Count("*").as_("count") | |||||
filtered_records = SubQuery( | |||||
frappe.qb.from_(Goal) | |||||
.select(Goal.employee) | |||||
.distinct() | |||||
.where((Goal.appraisal_cycle == cycle_name) & (Goal.status != "Archived")) | |||||
) | |||||
goals_missing = ( | |||||
frappe.qb.from_(Appraisal) | |||||
.select(count) | |||||
.where( | |||||
(Appraisal.appraisal_cycle == cycle_name) | |||||
& (Appraisal.docstatus != 2) | |||||
& (Appraisal.employee.notin(filtered_records)) | |||||
) | |||||
).run(as_dict=True) | |||||
return goals_missing[0].count | |||||
def get_employees_without_feedback(cycle_name: str) -> int: | |||||
Feedback = frappe.qb.DocType("Employee Performance Feedback") | |||||
Appraisal = frappe.qb.DocType("Appraisal") | |||||
count = Count("*").as_("count") | |||||
filtered_records = SubQuery( | |||||
frappe.qb.from_(Feedback) | |||||
.select(Feedback.employee) | |||||
.distinct() | |||||
.where((Feedback.appraisal_cycle == cycle_name) & (Feedback.docstatus == 1)) | |||||
) | |||||
feedback_missing = ( | |||||
frappe.qb.from_(Appraisal) | |||||
.select(count) | |||||
.where( | |||||
(Appraisal.appraisal_cycle == cycle_name) | |||||
& (Appraisal.docstatus != 2) | |||||
& (Appraisal.employee.notin(filtered_records)) | |||||
) | |||||
).run(as_dict=True) | |||||
return feedback_missing[0].count |
@@ -0,0 +1,86 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# See license.txt | |||||
import frappe | |||||
from frappe.tests.utils import FrappeTestCase | |||||
from erpnext.setup.doctype.designation.test_designation import create_designation | |||||
from erpnext.setup.doctype.employee.test_employee import make_employee | |||||
from hrms.hr.doctype.appraisal_template.test_appraisal_template import create_appraisal_template | |||||
from hrms.tests.test_utils import create_company | |||||
class TestAppraisalCycle(FrappeTestCase): | |||||
def setUp(self): | |||||
company = create_company("_Test Appraisal").name | |||||
self.template = create_appraisal_template() | |||||
engineer = create_designation(designation_name="Engineer") | |||||
engineer.appraisal_template = self.template.name | |||||
engineer.save() | |||||
create_designation(designation_name="Consultant") | |||||
self.employee1 = make_employee("employee1@example.com", company=company, designation="Engineer") | |||||
self.employee2 = make_employee( | |||||
"employee2@example.com", company=company, designation="Consultant" | |||||
) | |||||
def test_set_employees(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
self.assertEqual(len(cycle.appraisees), 1) | |||||
self.assertEqual(cycle.appraisees[0].employee, self.employee1) | |||||
def test_create_appraisals(self): | |||||
cycle = create_appraisal_cycle(designation="Engineer") | |||||
cycle.create_appraisals() | |||||
appraisals = frappe.db.get_all("Appraisal", filters={"appraisal_cycle": cycle.name}) | |||||
self.assertEqual(len(appraisals), 1) | |||||
appraisal = frappe.get_doc("Appraisal", appraisals[0].name) | |||||
for i in range(2): | |||||
# check if KRAs are set | |||||
self.assertEqual(appraisal.appraisal_kra[i].kra, self.template.goals[i].key_result_area) | |||||
self.assertEqual(appraisal.appraisal_kra[i].per_weightage, self.template.goals[i].per_weightage) | |||||
# check if rating criteria is set | |||||
self.assertEqual(appraisal.self_ratings[i].criteria, self.template.rating_criteria[i].criteria) | |||||
self.assertEqual( | |||||
appraisal.self_ratings[i].per_weightage, self.template.rating_criteria[i].per_weightage | |||||
) | |||||
def create_appraisal_cycle(**args): | |||||
args = frappe._dict(args) | |||||
name = args.name or "Q1" | |||||
if frappe.db.exists("Appraisal Cycle", name): | |||||
frappe.delete_doc("Appraisal Cycle", name, force=True) | |||||
appraisal_cycle = frappe.get_doc( | |||||
{ | |||||
"doctype": "Appraisal Cycle", | |||||
"cycle_name": name, | |||||
"company": args.company or "_Test Appraisal", | |||||
"start_date": args.start_date or "2022-01-01", | |||||
"end_date": args.end_date or "2022-03-31", | |||||
} | |||||
) | |||||
if args.kra_evaluation_method: | |||||
appraisal_cycle.kra_evaluation_method = args.kra_evaluation_method | |||||
filters = {} | |||||
for filter_by in ["department", "designation", "branch"]: | |||||
if args.get(filter_by): | |||||
filters[filter_by] = args.get(filter_by) | |||||
appraisal_cycle.update(filters) | |||||
appraisal_cycle.set_employees() | |||||
appraisal_cycle.insert() | |||||
return appraisal_cycle |
@@ -0,0 +1 @@ | |||||
Goal for the parent Appraisal. |
@@ -0,0 +1,220 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "hash", | |||||
"beta": 0, | |||||
"creation": "2013-02-22 01:27:44", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"description": "Key Responsibility Area", | |||||
"fieldname": "kra", | |||||
"fieldtype": "Small Text", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Goal", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "kra", | |||||
"oldfieldtype": "Small Text", | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "240px", | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0, | |||||
"width": "240px" | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "section_break_2", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "per_weightage", | |||||
"fieldtype": "Float", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Weightage (%)", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"oldfieldname": "per_weightage", | |||||
"oldfieldtype": "Currency", | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "70px", | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0, | |||||
"width": "70px" | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "column_break_4", | |||||
"fieldtype": "Column Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "score", | |||||
"fieldtype": "Float", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Score (0-5)", | |||||
"length": 0, | |||||
"no_copy": 1, | |||||
"oldfieldname": "score", | |||||
"oldfieldtype": "Select", | |||||
"options": "", | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "70px", | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0, | |||||
"width": "70px" | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "section_break_6", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "score_earned", | |||||
"fieldtype": "Float", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 1, | |||||
"label": "Score Earned", | |||||
"length": 0, | |||||
"no_copy": 1, | |||||
"oldfieldname": "score_earned", | |||||
"oldfieldtype": "Currency", | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"print_width": "70px", | |||||
"read_only": 1, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0, | |||||
"width": "70px" | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 1, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 1, | |||||
"max_attachments": 0, | |||||
"modified": "2020-09-18 17:26:09.703215", | |||||
"modified_by": "Administrator", | |||||
"module": "HR", | |||||
"name": "Appraisal Goal", | |||||
"owner": "Administrator", | |||||
"permissions": [], | |||||
"quick_entry": 0, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"track_seen": 0 | |||||
} |