@@ -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 | |||
} |