Kaynağa Gözat

Initial

master
Anoop 2 yıl önce
işleme
c9d4607f5c
100 değiştirilmiş dosya ile 5459 ekleme ve 0 silme
  1. +37
    -0
      .flake8
  2. +18
    -0
      .git-blame-ignore-revs
  3. +7
    -0
      .github/CODEOWNERS
  4. BIN
      .github/frappe-hr-logo.png
  5. +73
    -0
      .github/helper/.flake8_strict
  6. +46
    -0
      .github/helper/documentation.py
  7. +52
    -0
      .github/helper/install.sh
  8. +16
    -0
      .github/helper/site_config.json
  9. +60
    -0
      .github/helper/translation.py
  10. +4
    -0
      .github/labeler.yml
  11. +116
    -0
      .github/workflows/ci.yml
  12. +25
    -0
      .github/workflows/docs_checker.yml
  13. +12
    -0
      .github/workflows/labeller.yml
  14. +29
    -0
      .github/workflows/linters.yml
  15. +32
    -0
      .github/workflows/on_release.yml
  16. +9
    -0
      .gitignore
  17. +56
    -0
      .mergify.yml
  18. +44
    -0
      .pre-commit-config.yaml
  19. +24
    -0
      .releaserc
  20. +1
    -0
      .semgrepignore
  21. +18
    -0
      MANIFEST.in
  22. +66
    -0
      README.md
  23. +24
    -0
      codecov.yml
  24. BIN
      hrms.png
  25. +1
    -0
      hrms/__init__.py
  26. +0
    -0
      hrms/config/__init__.py
  27. +5
    -0
      hrms/config/desktop.py
  28. +11
    -0
      hrms/config/docs.py
  29. +198
    -0
      hrms/controllers/employee_boarding_controller.py
  30. +275
    -0
      hrms/controllers/employee_reminders.py
  31. +269
    -0
      hrms/controllers/tests/test_employee_reminders.py
  32. +308
    -0
      hrms/hooks.py
  33. +6
    -0
      hrms/hr/README.md
  34. +0
    -0
      hrms/hr/__init__.py
  35. +28
    -0
      hrms/hr/dashboard_chart/attendance_count/attendance_count.json
  36. +33
    -0
      hrms/hr/dashboard_chart/claims_by_type/claims_by_type.json
  37. +30
    -0
      hrms/hr/dashboard_chart/department_wise_employee_count/department_wise_employee_count.json
  38. +33
    -0
      hrms/hr/dashboard_chart/department_wise_expense_claims/department_wise_expense_claims.json
  39. +31
    -0
      hrms/hr/dashboard_chart/department_wise_openings/department_wise_openings.json
  40. +34
    -0
      hrms/hr/dashboard_chart/department_wise_timesheet_hours/department_wise_timesheet_hours.json
  41. +30
    -0
      hrms/hr/dashboard_chart/designation_wise_employee_count/designation_wise_employee_count.json
  42. +31
    -0
      hrms/hr/dashboard_chart/designation_wise_openings/designation_wise_openings.json
  43. +33
    -0
      hrms/hr/dashboard_chart/employee_advance_status/employee_advance_status.json
  44. +33
    -0
      hrms/hr/dashboard_chart/employees_by_age/employees_by_age.json
  45. +30
    -0
      hrms/hr/dashboard_chart/employees_by_branch/employees_by_branch.json
  46. +30
    -0
      hrms/hr/dashboard_chart/employees_by_grade/employees_by_grade.json
  47. +30
    -0
      hrms/hr/dashboard_chart/employees_by_type/employees_by_type.json
  48. +33
    -0
      hrms/hr/dashboard_chart/expense_claims/expense_claims.json
  49. +29
    -0
      hrms/hr/dashboard_chart/gender_diversity_ratio/gender_diversity_ratio.json
  50. +33
    -0
      hrms/hr/dashboard_chart/grievance_type/grievance_type.json
  51. +33
    -0
      hrms/hr/dashboard_chart/hiring_vs_attrition_count/hiring_vs_attrition_count.json
  52. +33
    -0
      hrms/hr/dashboard_chart/interview_status/interview_status.json
  53. +33
    -0
      hrms/hr/dashboard_chart/job_applicant_pipeline/job_applicant_pipeline.json
  54. +33
    -0
      hrms/hr/dashboard_chart/job_applicant_source/job_applicant_source.json
  55. +32
    -0
      hrms/hr/dashboard_chart/job_applicants_by_country/job_applicants_by_country.json
  56. +32
    -0
      hrms/hr/dashboard_chart/job_application_frequency/job_application_frequency.json
  57. +30
    -0
      hrms/hr/dashboard_chart/job_application_status/job_application_status.json
  58. +33
    -0
      hrms/hr/dashboard_chart/job_offer_status/job_offer_status.json
  59. +32
    -0
      hrms/hr/dashboard_chart/shift_assignment_breakup/shift_assignment_breakup.json
  60. +34
    -0
      hrms/hr/dashboard_chart/timesheet_activity_breakup/timesheet_activity_breakup.json
  61. +32
    -0
      hrms/hr/dashboard_chart/training_type/training_type.json
  62. +31
    -0
      hrms/hr/dashboard_chart/y_o_y_promotions/y_o_y_promotions.json
  63. +31
    -0
      hrms/hr/dashboard_chart/y_o_y_transfers/y_o_y_transfers.json
  64. +0
    -0
      hrms/hr/dashboard_chart_source/__init__.py
  65. +0
    -0
      hrms/hr/dashboard_chart_source/employees_by_age/__init__.py
  66. +14
    -0
      hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.js
  67. +13
    -0
      hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.json
  68. +87
    -0
      hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.py
  69. +0
    -0
      hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/__init__.py
  70. +35
    -0
      hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.js
  71. +13
    -0
      hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.json
  72. +69
    -0
      hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.py
  73. +0
    -0
      hrms/hr/doctype/__init__.py
  74. +0
    -0
      hrms/hr/doctype/appointment_letter/__init__.py
  75. +30
    -0
      hrms/hr/doctype/appointment_letter/appointment_letter.js
  76. +128
    -0
      hrms/hr/doctype/appointment_letter/appointment_letter.json
  77. +29
    -0
      hrms/hr/doctype/appointment_letter/appointment_letter.py
  78. +9
    -0
      hrms/hr/doctype/appointment_letter/test_appointment_letter.py
  79. +0
    -0
      hrms/hr/doctype/appointment_letter_content/__init__.py
  80. +39
    -0
      hrms/hr/doctype/appointment_letter_content/appointment_letter_content.json
  81. +10
    -0
      hrms/hr/doctype/appointment_letter_content/appointment_letter_content.py
  82. +0
    -0
      hrms/hr/doctype/appointment_letter_template/__init__.py
  83. +8
    -0
      hrms/hr/doctype/appointment_letter_template/appointment_letter_template.js
  84. +81
    -0
      hrms/hr/doctype/appointment_letter_template/appointment_letter_template.json
  85. +10
    -0
      hrms/hr/doctype/appointment_letter_template/appointment_letter_template.py
  86. +9
    -0
      hrms/hr/doctype/appointment_letter_template/test_appointment_letter_template.py
  87. +1
    -0
      hrms/hr/doctype/appraisal/README.md
  88. +0
    -0
      hrms/hr/doctype/appraisal/__init__.py
  89. +148
    -0
      hrms/hr/doctype/appraisal/appraisal.js
  90. +382
    -0
      hrms/hr/doctype/appraisal/appraisal.json
  91. +314
    -0
      hrms/hr/doctype/appraisal/appraisal.py
  92. +324
    -0
      hrms/hr/doctype/appraisal/test_appraisal.py
  93. +0
    -0
      hrms/hr/doctype/appraisal_cycle/__init__.py
  94. +120
    -0
      hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js
  95. +245
    -0
      hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json
  96. +272
    -0
      hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py
  97. +86
    -0
      hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py
  98. +1
    -0
      hrms/hr/doctype/appraisal_goal/README.md
  99. +0
    -0
      hrms/hr/doctype/appraisal_goal/__init__.py
  100. +220
    -0
      hrms/hr/doctype/appraisal_goal/appraisal_goal.json

+ 37
- 0
.flake8 Dosyayı Görüntüle

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

+ 18
- 0
.git-blame-ignore-revs Dosyayı Görüntüle

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

+ 7
- 0
.github/CODEOWNERS Dosyayı Görüntüle

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

BIN
.github/frappe-hr-logo.png Dosyayı Görüntüle

Önce Sonra
Genişlik: 237  |  Yükseklik: 57  |  Boyut: 4.2 KiB

+ 73
- 0
.github/helper/.flake8_strict Dosyayı Görüntüle

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

+ 46
- 0
.github/helper/documentation.py Dosyayı Görüntüle

@@ -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... 🏃")

+ 52
- 0
.github/helper/install.sh Dosyayı Görüntüle

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

+ 16
- 0
.github/helper/site_config.json Dosyayı Görüntüle

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

+ 60
- 0
.github/helper/translation.py Dosyayı Görüntüle

@@ -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!')

+ 4
- 0
.github/labeler.yml Dosyayı Görüntüle

@@ -0,0 +1,4 @@
# Any python files modifed but no test files modified
add-test-cases:
- any: ['hrms/**/*.py']
all: ['!hrms/**/test*.py']

+ 116
- 0
.github/workflows/ci.yml Dosyayı Görüntüle

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

+ 25
- 0
.github/workflows/docs_checker.yml Dosyayı Görüntüle

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

+ 12
- 0
.github/workflows/labeller.yml Dosyayı Görüntüle

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

+ 29
- 0
.github/workflows/linters.yml Dosyayı Görüntüle

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

+ 32
- 0
.github/workflows/on_release.yml Dosyayı Görüntüle

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

+ 9
- 0
.gitignore Dosyayı Görüntüle

@@ -0,0 +1,9 @@
.DS_Store
*.pyc
*.egg-info
*.swp
tags
hrms/docs/current
node_modules/
dist/
__pycache__/

+ 56
- 0
.mergify.yml Dosyayı Görüntüle

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

+ 44
- 0
.pre-commit-config.yaml Dosyayı Görüntüle

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

+ 24
- 0
.releaserc Dosyayı Görüntüle

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

+ 1
- 0
.semgrepignore Dosyayı Görüntüle

@@ -0,0 +1 @@
hrms/patches/post_install/

+ 18
- 0
MANIFEST.in Dosyayı Görüntüle

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

+ 66
- 0
README.md Dosyayı Görüntüle

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

[![CI](https://github.com/frappe/hrms/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/frappe/hrms/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/frappe/hrms/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](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!

![HRMS](hrms.png)

## 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.

+ 24
- 0
codecov.yml Dosyayı Görüntüle

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


BIN
hrms.png Dosyayı Görüntüle

Önce Sonra
Genişlik: 2144  |  Yükseklik: 1638  |  Boyut: 1.1 MiB

+ 1
- 0
hrms/__init__.py Dosyayı Görüntüle

@@ -0,0 +1 @@
__version__ = "14.3.0"

+ 0
- 0
hrms/config/__init__.py Dosyayı Görüntüle


+ 5
- 0
hrms/config/desktop.py Dosyayı Görüntüle

@@ -0,0 +1,5 @@
from frappe import _


def get_data():
return [{"module_name": "HRMS", "type": "module", "label": _("HRMS")}]

+ 11
- 0
hrms/config/docs.py Dosyayı Görüntüle

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

+ 198
- 0
hrms/controllers/employee_boarding_controller.py Dosyayı Görüntüle

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

+ 275
- 0
hrms/controllers/employee_reminders.py Dosyayı Görüntüle

@@ -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"),
)

+ 269
- 0
hrms/controllers/tests/test_employee_reminders.py Dosyayı Görüntüle

@@ -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()

+ 308
- 0
hrms/hooks.py Dosyayı Görüntüle

@@ -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 = []

+ 6
- 0
hrms/hr/README.md Dosyayı Görüntüle

@@ -0,0 +1,6 @@
Key features:

- Leave and Attendance
- Payroll
- Appraisal
- Expense Claim

+ 0
- 0
hrms/hr/__init__.py Dosyayı Görüntüle


+ 28
- 0
hrms/hr/dashboard_chart/attendance_count/attendance_count.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/claims_by_type/claims_by_type.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/department_wise_employee_count/department_wise_employee_count.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/department_wise_expense_claims/department_wise_expense_claims.json Dosyayı Görüntüle

@@ -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": []
}

+ 31
- 0
hrms/hr/dashboard_chart/department_wise_openings/department_wise_openings.json Dosyayı Görüntüle

@@ -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": []
}

+ 34
- 0
hrms/hr/dashboard_chart/department_wise_timesheet_hours/department_wise_timesheet_hours.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/designation_wise_employee_count/designation_wise_employee_count.json Dosyayı Görüntüle

@@ -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": []
}

+ 31
- 0
hrms/hr/dashboard_chart/designation_wise_openings/designation_wise_openings.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/employee_advance_status/employee_advance_status.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/employees_by_age/employees_by_age.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/employees_by_branch/employees_by_branch.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/employees_by_grade/employees_by_grade.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/employees_by_type/employees_by_type.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/expense_claims/expense_claims.json Dosyayı Görüntüle

@@ -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": []
}

+ 29
- 0
hrms/hr/dashboard_chart/gender_diversity_ratio/gender_diversity_ratio.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/grievance_type/grievance_type.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/hiring_vs_attrition_count/hiring_vs_attrition_count.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/interview_status/interview_status.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/job_applicant_pipeline/job_applicant_pipeline.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/job_applicant_source/job_applicant_source.json Dosyayı Görüntüle

@@ -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": []
}

+ 32
- 0
hrms/hr/dashboard_chart/job_applicants_by_country/job_applicants_by_country.json Dosyayı Görüntüle

@@ -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": []
}

+ 32
- 0
hrms/hr/dashboard_chart/job_application_frequency/job_application_frequency.json Dosyayı Görüntüle

@@ -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": []
}

+ 30
- 0
hrms/hr/dashboard_chart/job_application_status/job_application_status.json Dosyayı Görüntüle

@@ -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": []
}

+ 33
- 0
hrms/hr/dashboard_chart/job_offer_status/job_offer_status.json Dosyayı Görüntüle

@@ -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": []
}

+ 32
- 0
hrms/hr/dashboard_chart/shift_assignment_breakup/shift_assignment_breakup.json Dosyayı Görüntüle

@@ -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": []
}

+ 34
- 0
hrms/hr/dashboard_chart/timesheet_activity_breakup/timesheet_activity_breakup.json Dosyayı Görüntüle

@@ -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": []
}

+ 32
- 0
hrms/hr/dashboard_chart/training_type/training_type.json Dosyayı Görüntüle

@@ -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": []
}

+ 31
- 0
hrms/hr/dashboard_chart/y_o_y_promotions/y_o_y_promotions.json Dosyayı Görüntüle

@@ -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": []
}

+ 31
- 0
hrms/hr/dashboard_chart/y_o_y_transfers/y_o_y_transfers.json Dosyayı Görüntüle

@@ -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
hrms/hr/dashboard_chart_source/__init__.py Dosyayı Görüntüle


+ 0
- 0
hrms/hr/dashboard_chart_source/employees_by_age/__init__.py Dosyayı Görüntüle


+ 14
- 0
hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.js Dosyayı Görüntüle

@@ -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")
},
]
};

+ 13
- 0
hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.json Dosyayı Görüntüle

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

+ 87
- 0
hrms/hr/dashboard_chart_source/employees_by_age/employees_by_age.py Dosyayı Görüntüle

@@ -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
hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/__init__.py Dosyayı Görüntüle


+ 35
- 0
hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.js Dosyayı Görüntüle

@@ -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
},
]
};

+ 13
- 0
hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.json Dosyayı Görüntüle

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

+ 69
- 0
hrms/hr/dashboard_chart_source/hiring_vs_attrition_count/hiring_vs_attrition_count.py Dosyayı Görüntüle

@@ -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
hrms/hr/doctype/__init__.py Dosyayı Görüntüle


+ 0
- 0
hrms/hr/doctype/appointment_letter/__init__.py Dosyayı Görüntüle


+ 30
- 0
hrms/hr/doctype/appointment_letter/appointment_letter.js Dosyayı Görüntüle

@@ -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();
}
}

});
}
},
});

+ 128
- 0
hrms/hr/doctype/appointment_letter/appointment_letter.json Dosyayı Görüntüle

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

+ 29
- 0
hrms/hr/doctype/appointment_letter/appointment_letter.py Dosyayı Görüntüle

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

+ 9
- 0
hrms/hr/doctype/appointment_letter/test_appointment_letter.py Dosyayı Görüntüle

@@ -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
hrms/hr/doctype/appointment_letter_content/__init__.py Dosyayı Görüntüle


+ 39
- 0
hrms/hr/doctype/appointment_letter_content/appointment_letter_content.json Dosyayı Görüntüle

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

+ 10
- 0
hrms/hr/doctype/appointment_letter_content/appointment_letter_content.py Dosyayı Görüntüle

@@ -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
hrms/hr/doctype/appointment_letter_template/__init__.py Dosyayı Görüntüle


+ 8
- 0
hrms/hr/doctype/appointment_letter_template/appointment_letter_template.js Dosyayı Görüntüle

@@ -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) {

// }
});

+ 81
- 0
hrms/hr/doctype/appointment_letter_template/appointment_letter_template.json Dosyayı Görüntüle

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

+ 10
- 0
hrms/hr/doctype/appointment_letter_template/appointment_letter_template.py Dosyayı Görüntüle

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

+ 9
- 0
hrms/hr/doctype/appointment_letter_template/test_appointment_letter_template.py Dosyayı Görüntüle

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

+ 1
- 0
hrms/hr/doctype/appraisal/README.md Dosyayı Görüntüle

@@ -0,0 +1 @@
Performance of an Employee in a Time Period against given goals.

+ 0
- 0
hrms/hr/doctype/appraisal/__init__.py Dosyayı Görüntüle


+ 148
- 0
hrms/hr/doctype/appraisal/appraisal.js Dosyayı Görüntüle

@@ -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");
}
});

+ 382
- 0
hrms/hr/doctype/appraisal/appraisal.json Dosyayı Görüntüle

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

+ 314
- 0
hrms/hr/doctype/appraisal/appraisal.py Dosyayı Görüntüle

@@ -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,
)

+ 324
- 0
hrms/hr/doctype/appraisal/test_appraisal.py Dosyayı Görüntüle

@@ -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
hrms/hr/doctype/appraisal_cycle/__init__.py Dosyayı Görüntüle


+ 120
- 0
hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js Dosyayı Görüntüle

@@ -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");
}
});
}
});

+ 245
- 0
hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json Dosyayı Görüntüle

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

+ 272
- 0
hrms/hr/doctype/appraisal_cycle/appraisal_cycle.py Dosyayı Görüntüle

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

+ 86
- 0
hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py Dosyayı Görüntüle

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

+ 1
- 0
hrms/hr/doctype/appraisal_goal/README.md Dosyayı Görüntüle

@@ -0,0 +1 @@
Goal for the parent Appraisal.

+ 0
- 0
hrms/hr/doctype/appraisal_goal/__init__.py Dosyayı Görüntüle


+ 220
- 0
hrms/hr/doctype/appraisal_goal/appraisal_goal.json Dosyayı Görüntüle

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

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor

Yükleniyor…
İptal
Kaydet