├── .gitignore
├── MANIFEST.in
├── README.md
├── hr_addon
├── __init__.py
├── config
│ ├── __init__.py
│ ├── desktop.py
│ └── docs.py
├── custom_scripts
│ ├── __init__.py
│ └── custom_python
│ │ ├── __init__.py
│ │ └── weekly_working_hours.py
├── hooks.py
├── hr_addon
│ ├── __init__.py
│ ├── api
│ │ ├── export_calendar.py
│ │ └── utils.py
│ ├── doctype
│ │ ├── __init__.py
│ │ ├── daily_hours_detail
│ │ │ ├── __init__.py
│ │ │ ├── daily_hours_detail.json
│ │ │ └── daily_hours_detail.py
│ │ ├── employee_checkins
│ │ │ ├── __init__.py
│ │ │ ├── employee_checkins.json
│ │ │ └── employee_checkins.py
│ │ ├── employee_item
│ │ │ ├── __init__.py
│ │ │ ├── employee_item.json
│ │ │ └── employee_item.py
│ │ ├── hr_addon_settings
│ │ │ ├── __init__.py
│ │ │ ├── hr_addon_settings.js
│ │ │ ├── hr_addon_settings.json
│ │ │ ├── hr_addon_settings.py
│ │ │ └── test_hr_addon_settings.py
│ │ ├── weekly_working_hours
│ │ │ ├── __init__.py
│ │ │ ├── test_weekly_working_hours.py
│ │ │ ├── weekly_working_hours.js
│ │ │ ├── weekly_working_hours.json
│ │ │ └── weekly_working_hours.py
│ │ └── workday
│ │ │ ├── __init__.py
│ │ │ ├── test_workday.py
│ │ │ ├── workday.js
│ │ │ ├── workday.json
│ │ │ ├── workday.py
│ │ │ └── workday_list.js
│ └── report
│ │ ├── __init__.py
│ │ └── work_hour_report
│ │ ├── __init__.py
│ │ ├── work_hour_report.js
│ │ ├── work_hour_report.json
│ │ └── work_hour_report.py
├── modules.txt
├── patches.txt
├── patches
│ ├── __init__.py
│ └── v15_0
│ │ ├── __init__.py
│ │ └── add_custom_field_for_employee.py
├── public
│ ├── .gitkeep
│ └── js
│ │ ├── hr_settings.js
│ │ └── list_view.js
└── templates
│ ├── __init__.py
│ └── pages
│ └── __init__.py
├── license.txt
├── requirements.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *.egg-info
4 | *.swp
5 | tags
6 | hr_addon/docs/current
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include MANIFEST.in
2 | include requirements.txt
3 | include *.json
4 | include *.md
5 | include *.py
6 | include *.txt
7 | recursive-include hr_addon *.css
8 | recursive-include hr_addon *.csv
9 | recursive-include hr_addon *.html
10 | recursive-include hr_addon *.ico
11 | recursive-include hr_addon *.js
12 | recursive-include hr_addon *.json
13 | recursive-include hr_addon *.md
14 | recursive-include hr_addon *.png
15 | recursive-include hr_addon *.py
16 | recursive-include hr_addon *.svg
17 | recursive-include hr_addon *.txt
18 | recursive-exclude hr_addon *.pyc
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HR Addon
2 |
3 | Addon for Erpnext attendance and employee checkins.
4 |
5 | HR Addon gives you a detailed report on employees working hours
6 | 
7 |
8 |
9 | # Installation
10 | ## Frappecloud
11 | Your best to install HR Addon via the frappecloud.com marketplace
12 |
13 | ## Self hosted
14 | From the frappe-bench folder, execute
15 |
16 | $ bench get-app HR-Addon https://github.com/phamos-eu/HR-Addon.git
17 |
18 | $ bench install-app HR-Addon If you are using a multi-tenant environment, use the following command
19 |
20 | $ bench --site site_name install-app hr_addon
21 |
22 | # Documentation
23 | The comprehensive documentation can be found at https://doku.phamos.eu/books/hr-addon
24 |
25 | # License
26 |
27 | MIT
28 |
--------------------------------------------------------------------------------
/hr_addon/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/hr_addon/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/config/__init__.py
--------------------------------------------------------------------------------
/hr_addon/config/desktop.py:
--------------------------------------------------------------------------------
1 | from frappe import _
2 |
3 | def get_data():
4 | return [
5 | {
6 | "module_name": "HR Addon",
7 | "color": "grey",
8 | "icon": "octicon octicon-file-directory",
9 | "type": "module",
10 | "label": _("HR Addon")
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/hr_addon/config/docs.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration for docs
3 | """
4 |
5 | # source_link = "https://github.com/[org_name]/hr_addon"
6 | # docs_base_url = "https://[org_name].github.io/hr_addon"
7 | # headline = "App that does everything"
8 | # sub_heading = "Yes, you got that right the first time, everything"
9 |
10 | def get_context(context):
11 | context.brand_html = "HR Addon"
12 |
--------------------------------------------------------------------------------
/hr_addon/custom_scripts/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/hr_addon/custom_scripts/custom_python/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/hr_addon/custom_scripts/custom_python/weekly_working_hours.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from frappe.utils import getdate
3 | from frappe.model.document import Document
4 |
5 | @frappe.whitelist()
6 | def set_from_to_dates():
7 | # Ensure fiscal year data is present
8 | fiscal_year = frappe.db.sql("""
9 | SELECT year_start_date, year_end_date
10 | FROM `tabFiscal Year`
11 | WHERE disabled = 0
12 | """, as_dict=True)
13 |
14 | if not fiscal_year:
15 | frappe.throw("No active fiscal year found.")
16 |
17 | year_start_date = fiscal_year[0].year_start_date
18 | year_end_date = fiscal_year[0].year_end_date
19 |
20 | # Update the valid_from and valid_to fields
21 | frappe.db.sql("""
22 | UPDATE
23 | `tabWeekly Working Hours`
24 | SET
25 | valid_from = %(year_start_date)s,
26 | valid_to = %(year_end_date)s
27 | WHERE
28 | employee IN (
29 | SELECT
30 | name
31 | FROM
32 | `tabEmployee`
33 | WHERE
34 | permanent = 1
35 | )
36 | """, {
37 | "year_start_date": year_start_date,
38 | "year_end_date": year_end_date
39 | })
40 |
41 | frappe.db.commit()
42 |
43 |
44 |
--------------------------------------------------------------------------------
/hr_addon/hooks.py:
--------------------------------------------------------------------------------
1 | from . import __version__ as app_version
2 |
3 | app_name = "hr_addon"
4 | app_title = "HR Addon"
5 | app_publisher = "Jide Olayinka"
6 | app_description = "Addon for Erpnext attendance and employee checkins"
7 | app_icon = "octicon octicon-file-directory"
8 | app_color = "grey"
9 | app_email = "olajamesjide@gmail.com"
10 | app_license = "MIT"
11 |
12 | # Includes in
13 | # ------------------
14 |
15 | # include js, css files in header of desk.html
16 | # app_include_css = "/assets/hr_addon/css/hr_addon.css"
17 | # app_include_js = "/assets/hr_addon/js/hr_addon.js"
18 |
19 | # include js, css files in header of web template
20 | # web_include_css = "/assets/hr_addon/css/hr_addon.css"
21 | # web_include_js = "/assets/hr_addon/js/hr_addon.js"
22 |
23 | # include custom scss in every website theme (without file extension ".scss")
24 | # website_theme_scss = "hr_addon/public/scss/website"
25 |
26 | # include js, css files in header of web form
27 | # webform_include_js = {"doctype": "public/js/doctype.js"}
28 | # webform_include_css = {"doctype": "public/css/doctype.css"}
29 |
30 | # include js in page
31 | # page_js = {"page" : "public/js/file.js"}
32 | doctype_js = {
33 | "HR Settings" : "public/js/hr_settings.js"
34 | }
35 |
36 | # include js in doctype views
37 | # doctype_js = {"doctype" : "public/js/doctype.js"}
38 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
39 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
40 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
41 |
42 | # Home Pages
43 | # ----------
44 |
45 | # application home page (will override Website Settings)
46 | # home_page = "login"
47 |
48 | # website user home page (by Role)
49 | # role_home_page = {
50 | # "Role": "home_page"
51 | # }
52 |
53 | # Generators
54 | # ----------
55 |
56 | # automatically create page for each record of this doctype
57 | # website_generators = ["Web Page"]
58 |
59 | # Installation
60 | # ------------
61 |
62 | # before_install = "hr_addon.install.before_install"
63 | # after_install = "hr_addon.install.after_install"
64 |
65 | # Uninstallation
66 | # ------------
67 |
68 | # before_uninstall = "hr_addon.uninstall.before_uninstall"
69 | # after_uninstall = "hr_addon.uninstall.after_uninstall"
70 |
71 | # Desk Notifications
72 | # ------------------
73 | # See frappe.core.notifications.get_notification_config
74 |
75 | # notification_config = "hr_addon.notifications.get_notification_config"
76 |
77 | # Permissions
78 | # -----------
79 | # Permissions evaluated in scripted ways
80 |
81 | # permission_query_conditions = {
82 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
83 | # }
84 | #
85 | # has_permission = {
86 | # "Event": "frappe.desk.doctype.event.event.has_permission",
87 | # }
88 |
89 | # DocType Class
90 | # ---------------
91 | # Override standard doctype classes
92 |
93 | # override_doctype_class = {
94 | # "ToDo": "custom_app.overrides.CustomToDo"
95 | # }
96 |
97 | # Document Events
98 | # ---------------
99 | # Hook on document methods and events
100 |
101 | # doc_events = {
102 | # "*": {
103 | # "on_update": "method",
104 | # "on_cancel": "method",
105 | # "on_trash": "method"
106 | # }
107 | # }
108 |
109 | # Scheduled Tasks
110 | # ---------------
111 | # scheduler_events = {
112 | # "all": [
113 | # "hr_addon.tasks.all"
114 | # ],
115 | # "daily": [
116 | # "hr_addon.tasks.daily"
117 | # ],
118 | # "hourly": [
119 | # "hr_addon.tasks.hourly"
120 | # ],
121 | # "weekly": [
122 | # "hr_addon.tasks.weekly"
123 | # ]
124 | # "monthly": [
125 | # "hr_addon.tasks.monthly"
126 | # ]
127 | # }
128 |
129 |
130 | # Testing
131 | # -------
132 |
133 | # before_tests = "hr_addon.install.before_tests"
134 |
135 | # Overriding Methods
136 | # ------------------------------
137 | #
138 | # override_whitelisted_methods = {
139 | # "frappe.desk.doctype.event.event.get_events": "hr_addon.event.get_events"
140 | # }
141 | #
142 | # each overriding function accepts a `data` argument;
143 | # generated from the base implementation of the doctype dashboard,
144 | # along with any modifications made in other Frappe apps
145 | # override_doctype_dashboards = {
146 | # "Task": "hr_addon.task.get_dashboard_data"
147 | # }
148 |
149 | # exempt linked doctypes from being automatically cancelled
150 | #
151 | # auto_cancel_exempted_doctypes = ["Auto Repeat"]
152 |
153 |
154 | # User Data Protection
155 | # --------------------
156 |
157 | user_data_fields = [
158 | {
159 | "doctype": "{doctype_1}",
160 | "filter_by": "{filter_by}",
161 | "redact_fields": ["{field_1}", "{field_2}"],
162 | "partial": 1,
163 | },
164 | {
165 | "doctype": "{doctype_2}",
166 | "filter_by": "{filter_by}",
167 | "partial": 1,
168 | },
169 | {
170 | "doctype": "{doctype_3}",
171 | "strict": False,
172 | },
173 | {
174 | "doctype": "{doctype_4}"
175 | }
176 | ]
177 |
178 | # Authentication and authorization
179 | # --------------------------------
180 |
181 | # auth_hooks = [
182 | # "hr_addon.auth.validate"
183 | # ]
184 |
185 | # Translation
186 | # --------------------------------
187 |
188 | # Make link fields search translated document names for these DocTypes
189 | # Recommended only for DocTypes which have limited documents with untranslated names
190 | # For example: Role, Gender, etc.
191 | # translated_search_doctypes = []
192 |
193 | required_apps = ["hrms"]
194 |
195 | doc_events = {
196 | "Leave Application": {
197 | "on_change": "hr_addon.hr_addon.api.export_calendar.export_calendar",
198 | "on_cancel": "hr_addon.hr_addon.api.export_calendar.export_calendar"
199 | }
200 | }
201 |
202 | doctype_list_js = {"Weekly Working Hours" : "public/js/list_view.js"}
203 |
204 | scheduler_events = {
205 | "hourly": [
206 | "hr_addon.hr_addon.doctype.hr_addon_settings.hr_addon_settings.generate_workdays_scheduled_job"
207 | ],
208 | "yearly": [
209 | "hr_addon.custom_scripts.custom_python.weekly_working_hours.set_from_to_dates",
210 | ],
211 | "daily": [
212 | "hr_addon.hr_addon.api.utils.send_work_anniversary_notification"
213 | ]
214 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/api/export_calendar.py:
--------------------------------------------------------------------------------
1 | import io, os
2 | import frappe
3 | from icalendar import Event, Calendar
4 | from datetime import datetime
5 | from frappe.utils.file_manager import save_file
6 |
7 | def generate_leave_ical_file(leave_applications):
8 | cal = Calendar()
9 |
10 | for leave_application in leave_applications:
11 | event = Event()
12 |
13 | # Extract data from the Leave Application document
14 | start_date = leave_application.get('from_date')
15 | end_date = leave_application.get('to_date')
16 | end_date += frappe.utils.datetime.timedelta(days=1)
17 | employee_name = leave_application.get('employee_name')
18 | leave_type = leave_application.get('leave_type')
19 | description = leave_application.get('description')
20 | if not description:
21 | description = ""
22 |
23 | uid = leave_application.name
24 | if uid.count("-") == 4 and uid.find("CANCELLED") < 0:
25 | uid = uid[:-2]
26 |
27 | event.add('dtstart', start_date)
28 | event.add('dtend', end_date)
29 | summary = ""
30 | if leave_application.get("cancelled"):
31 | summary = "CANCELLED - "
32 | event.add('summary', f'{summary}{employee_name} - {leave_type}')
33 | event.add('description', description)
34 | event.add("uid", uid)
35 |
36 | cal.add_component(event)
37 |
38 | # Generate the iCalendar data
39 | ical_data = cal.to_ical()
40 |
41 | return ical_data
42 |
43 | def export_calendar(doc, method=None):
44 | """
45 | This function is triggered when a Leave Application is created/changed/updated.
46 | """
47 | if doc.status == "Approved" or doc.status == "Cancelled":
48 | leave_applications = frappe.db.get_list("Leave Application",
49 | filters=[["status", "in", ["Approved", "Cancelled"]]],
50 | fields=["name", "status", "from_date", "to_date", "employee_name", "leave_type", "description", "amended_from"])
51 |
52 | index = 0
53 | for la in leave_applications:
54 | if la["status"] == "Cancelled":
55 | la["cancelled"] = False
56 | if la["name"] in [app["amended_from"] for app in leave_applications]:
57 | del leave_applications[index]
58 | else:
59 | la["cancelled"] = True
60 | index = index + 1
61 |
62 | ical_data = generate_leave_ical_file(leave_applications)
63 |
64 | # Save the iCalendar data as a File document
65 | file_name = frappe.db.get_single_value("HR Addon Settings", "name_of_calendar_export_ics_file")
66 | file_name = "{}.ics".format(file_name) # Set the desired filename here
67 | create_file(file_name, ical_data, doc.name)
68 |
69 |
70 | def create_file(file_name, file_content, doc_name):
71 | """
72 | Creates a file in user defined folder
73 | """
74 | folder_path = frappe.db.get_single_value("HR Addon Settings", "ics_folder_path")
75 | if not folder_path:
76 | folder_path = "{}/public/files/".format(frappe.utils.get_site_path())
77 | file_path = os.path.join(folder_path, file_name)
78 | with open(file_path, 'wb') as ical_file:
79 | ical_file.write(file_content)
80 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/api/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | import frappe
3 | from frappe import _
4 | from frappe.utils.data import date_diff, time_diff_in_hours
5 | from frappe.utils import get_datetime, getdate, today, comma_sep, flt
6 | from frappe.core.doctype.role.role import get_info_based_on_role
7 |
8 |
9 | def get_employee_checkin(employee,atime):
10 | ''' select DATE('date time');'''
11 | employee = employee
12 | atime = atime
13 | checkin_list = frappe.db.sql(
14 | """
15 | SELECT name,log_type,time,skip_auto_attendance,attendance FROM `tabEmployee Checkin`
16 | WHERE employee='%s' AND DATE(time)= DATE('%s') ORDER BY time ASC
17 | """%(employee,atime), as_dict=1
18 | )
19 | return checkin_list or []
20 |
21 | def get_employee_default_work_hour(employee,adate):
22 | ''' weekly working hour'''
23 | employee = employee
24 | adate = adate
25 | #validate current or active FY year WHERE --
26 | # AND YEAR(valid_from) = CAST(%(year)s as INT) AND YEAR(valid_to) = CAST(%(year)s as INT)
27 | # AND YEAR(w.valid_from) = CAST(('2022-01-01') as INT) AND YEAR(w.valid_to) = CAST(('2022-12-30') as INT);
28 | target_work_hours= frappe.db.sql(
29 | """
30 | SELECT w.name,w.employee,w.valid_from,w.valid_to,d.day,d.hours,d.break_minutes FROM `tabWeekly Working Hours` w
31 | LEFT JOIN `tabDaily Hours Detail` d ON w.name = d.parent
32 | WHERE w.employee='%s' AND d.day = DAYNAME('%s') and w.valid_from <= '%s' and w.valid_to >= '%s' and w.docstatus = 1
33 | """%(employee,adate,adate,adate), as_dict=1
34 | )
35 |
36 | if not target_work_hours:
37 | frappe.throw(_('Please create Weekly Working Hours for the selected Employee:{0} first for date : {1}.').format(employee,adate))
38 |
39 | if len(target_work_hours) > 1:
40 | target_work_hours= "
".join([frappe.get_desk_link("Weekly Working Hours", w.name) for w in target_work_hours])
41 | frappe.throw(_('There exist multiple Weekly Working Hours exist for the Date {0}:
{1}
').format(adate, target_work_hours))
42 |
43 | return target_work_hours[0]
44 |
45 |
46 | @frappe.whitelist()
47 | def get_actual_employee_log(aemployee, adate):
48 | '''total actual log'''
49 | employee_checkins = get_employee_checkin(aemployee,adate)
50 | employee_default_work_hour = get_employee_default_work_hour(aemployee,adate)
51 | is_date_in_holiday_list = date_is_in_holiday_list(aemployee,adate)
52 | fields=["name", "no_break_hours", "set_target_hours_to_zero_when_date_is_holiday"]
53 | weekly_working_hours = frappe.db.get_list(doctype="Weekly Working Hours", filters={"employee": aemployee}, fields=fields)
54 | is_target_hours_zero_on_holiday = len(weekly_working_hours) > 0 and weekly_working_hours[0]["set_target_hours_to_zero_when_date_is_holiday"] == 1
55 |
56 | # check empty or none
57 | if employee_checkins:
58 | no_break_hours = True if len(weekly_working_hours) > 0 and weekly_working_hours[0]["no_break_hours"] == 1 else False
59 | new_workday = get_workday(employee_checkins, employee_default_work_hour, no_break_hours, is_target_hours_zero_on_holiday, is_date_in_holiday_list)
60 | return new_workday
61 | else :
62 | view_employee_attendance = get_employee_attendance(aemployee, adate)
63 |
64 | break_minutes = employee_default_work_hour.break_minutes
65 | expected_break_hours = flt(break_minutes / 60)
66 |
67 | if is_target_hours_zero_on_holiday and is_date_in_holiday_list:
68 | new_workday = {
69 | "target_hours": 0,
70 | "total_target_seconds": 0,
71 | "break_minutes": employee_default_work_hour.break_minutes,
72 | "actual_working_hours": 0,
73 | "hours_worked": 0,
74 | "nbreak": 0,
75 | "attendance": view_employee_attendance[0].name if len(view_employee_attendance) > 0 else "",
76 | "break_hours": 0,
77 | "total_work_seconds": 0,
78 | "total_break_seconds": 0,
79 | "employee_checkins": [],
80 | "first_checkin": "",
81 | "last_checkout": "",
82 | "expected_break_hours": 0,
83 | }
84 | else:
85 | new_workday = {
86 | "target_hours": employee_default_work_hour.hours,
87 | "total_target_seconds": employee_default_work_hour.hours * 60 * 60,
88 | "break_minutes": employee_default_work_hour.break_minutes,
89 | "actual_working_hours": -employee_default_work_hour.hours,
90 | "manual_workday": 1,
91 | "hours_worked": 0,
92 | "nbreak": 0,
93 | "attendance": view_employee_attendance[0].name if len(view_employee_attendance) > 0 else "",
94 | "break_hours": 0,
95 | "employee_checkins": [],
96 | "expected_break_hours": expected_break_hours,
97 | }
98 |
99 | return new_workday
100 |
101 |
102 |
103 |
104 | def get_workday(employee_checkins, employee_default_work_hour, no_break_hours, is_target_hours_zero_on_holiday,is_date_in_holiday_list=False):
105 | hr_addon_settings = frappe.get_doc("HR Addon Settings")
106 | is_break_from_checkins_with_swapped_hours = hr_addon_settings.workday_break_calculation_mechanism == "Break Hours from Employee Checkins" and hr_addon_settings.swap_hours_worked_and_actual_working_hours
107 | new_workday = {}
108 |
109 | hours_worked = 0.0
110 | total_duration = 0
111 |
112 | # not pair of IN/OUT either missing
113 | if len(employee_checkins)% 2 != 0:
114 | hours_worked = -36.0
115 | employee_checkin_message = ""
116 | for d in employee_checkins:
117 | employee_checkin_message += "CheckIn Type:{0} for {1}".format(d.log_type, frappe.get_desk_link("Employee Checkin", d.name))
118 |
119 | #frappe.msgprint("CheckIns must be in pair for the given date:".format(employee_checkin_message))
120 |
121 | if (len(employee_checkins) % 2 == 0):
122 | # seperate 'IN' from 'OUT'
123 | clockin_list = [get_datetime(kin.time) for x,kin in enumerate(employee_checkins) if x % 2 == 0]
124 | clockout_list = [get_datetime(kout.time) for x,kout in enumerate(employee_checkins) if x % 2 != 0]
125 |
126 | # get total worked hours
127 | for i in range(len(clockin_list)):
128 | wh = time_diff_in_hours(clockout_list[i],clockin_list[i])
129 | hours_worked += float(str(wh))
130 |
131 | # Calculate difference between first check-in and last checkout
132 | if clockin_list and clockout_list:
133 | first_checkin = clockin_list[0]
134 | last_checkout = clockout_list[-1] # Last element of clockout_list
135 | total_duration = time_diff_in_hours(last_checkout, first_checkin)
136 |
137 | if is_break_from_checkins_with_swapped_hours:
138 | total_duration, hours_worked = hours_worked, total_duration
139 |
140 | default_break_minutes = employee_default_work_hour.break_minutes
141 | default_break_hours = flt(default_break_minutes / 60)
142 | target_hours = employee_default_work_hour.hours
143 |
144 | if len(employee_checkins) % 2 == 0:
145 | break_from_checkins = 0.0
146 | for i in range(len(clockout_list) - 1):
147 | wh = time_diff_in_hours(clockin_list[i + 1], clockout_list[i])
148 | break_from_checkins += float(wh)
149 |
150 | if hr_addon_settings.workday_break_calculation_mechanism == "Break Hours from Employee Checkins":
151 | break_hours = break_from_checkins
152 |
153 | elif hr_addon_settings.workday_break_calculation_mechanism == "Break Hours from Weekly Working Hours":
154 | break_hours = default_break_hours
155 |
156 | elif hr_addon_settings.workday_break_calculation_mechanism == "Break Hours from Weekly Working Hours if Shorter breaks":
157 | if break_from_checkins <= default_break_hours:
158 | break_hours = default_break_hours
159 | else:
160 | break_hours = 0.0
161 |
162 | else:
163 | break_hours = flt(-360.0)
164 |
165 | total_target_seconds = target_hours * 60 * 60
166 | total_work_seconds = flt(hours_worked * 60 * 60)
167 | expected_break_hours = flt(default_break_minutes / 60)
168 | total_break_seconds = flt(break_hours * 60 * 60)
169 | hours_worked = flt(hours_worked)
170 |
171 | if is_break_from_checkins_with_swapped_hours:
172 | #swapping for gall
173 | if hours_worked > 0:
174 | actual_working_hours = hours_worked - break_hours
175 | else:
176 | actual_working_hours = total_duration - expected_break_hours
177 |
178 | else:
179 | if total_duration > 0:
180 | actual_working_hours = total_duration - break_hours
181 | else:
182 | actual_working_hours = hours_worked - expected_break_hours
183 | attendance = employee_checkins[0].attendance if len(employee_checkins) > 0 else ""
184 |
185 | if no_break_hours and hours_worked < 6 and not is_break_from_checkins_with_swapped_hours: # TODO: set 6 as constant
186 | default_break_minutes = 0
187 | total_break_seconds = 0
188 | #expected_break_hours = 0
189 | actual_working_hours = hours_worked
190 |
191 | if is_target_hours_zero_on_holiday and is_date_in_holiday_list:
192 | target_hours = 0
193 | total_target_seconds = 0
194 |
195 | #if comp_off_doc:
196 | # hours_worked = 0
197 | # actual_working_hours = 0
198 | # #frappe.msgprint(frappe.get_desk_link("Leave Application", comp_off_doc) )
199 | # frappe.msgprint("The selected employee: {} has a Leave Application with the leave type: 'Freizeitausgleich (Nicht buchen!)' on the given date :{}.".format(aemployee,adate))
200 |
201 | # if target_hours == 0:
202 | # expected_break_hours = 0
203 | # total_break_seconds = 0
204 |
205 | new_workday.update({
206 | "target_hours": target_hours,
207 | "total_target_seconds": total_target_seconds,
208 | "break_minutes": default_break_minutes,
209 | "hours_worked": hours_worked,
210 | "expected_break_hours": expected_break_hours,
211 | "actual_working_hours": actual_working_hours,
212 | "total_work_seconds": total_work_seconds,
213 | "nbreak": 0,
214 | "attendance": attendance,
215 | "break_hours": break_hours,
216 | "total_break_seconds": total_break_seconds,
217 | "employee_checkins":employee_checkins,
218 | })
219 |
220 | return new_workday
221 |
222 |
223 | @frappe.whitelist()
224 | def get_actual_employee_log_for_bulk_process(aemployee, adate):
225 | employee_checkins = get_employee_checkin(aemployee, adate)
226 | employee_default_work_hour = get_employee_default_work_hour(aemployee, adate)
227 | is_date_in_holiday_list = date_is_in_holiday_list(aemployee, adate)
228 |
229 | # Initialize 'fields' before it's used
230 | fields = ["name", "no_break_hours", "set_target_hours_to_zero_when_date_is_holiday"]
231 |
232 | # Fetch weekly working hours using the 'fields' variable
233 | weekly_working_hours = frappe.db.get_list(doctype="Weekly Working Hours", filters={"employee": aemployee}, fields=fields)
234 | is_target_hours_zero_on_holiday = len(weekly_working_hours) > 0 and weekly_working_hours[0]["set_target_hours_to_zero_when_date_is_holiday"] == 1
235 |
236 | if employee_checkins:
237 | # Determine if 'no_break_hours' should be set to True or False
238 | no_break_hours = True if len(weekly_working_hours) > 0 and weekly_working_hours[0]["no_break_hours"] == 1 else False
239 | new_workday = get_workday(employee_checkins, employee_default_work_hour, no_break_hours, is_target_hours_zero_on_holiday, is_date_in_holiday_list)
240 | else:
241 | view_employee_attendance = get_employee_attendance(aemployee, adate)
242 |
243 | break_minutes = employee_default_work_hour.break_minutes
244 | expected_break_hours = flt(break_minutes / 60)
245 |
246 | if is_target_hours_zero_on_holiday and is_date_in_holiday_list:
247 | new_workday = {
248 | "target_hours": 0,
249 | "total_target_seconds": 0,
250 | "break_minutes": employee_default_work_hour.break_minutes,
251 | "actual_working_hours": 0,
252 | "hours_worked": 0,
253 | "nbreak": 0,
254 | "attendance": view_employee_attendance[0].name if len(view_employee_attendance) > 0 else "",
255 | "break_hours": 0,
256 | "total_work_seconds": 0,
257 | "total_break_seconds": 0,
258 | "employee_checkins": [],
259 | "first_checkin": "",
260 | "last_checkout": "",
261 | "expected_break_hours": 0,
262 | }
263 | else:
264 | new_workday = {
265 | "target_hours": employee_default_work_hour.hours,
266 | "total_target_seconds": employee_default_work_hour.hours * 60 * 60,
267 | "break_minutes": employee_default_work_hour.break_minutes,
268 | "actual_working_hours": -employee_default_work_hour.hours,
269 | "manual_workday": 1,
270 | "hours_worked": 0,
271 | "nbreak": 0,
272 | "attendance": view_employee_attendance[0].name if len(view_employee_attendance) > 0 else "",
273 | "break_hours": 0,
274 | "employee_checkins": [],
275 | "expected_break_hours": expected_break_hours,
276 | }
277 |
278 | return new_workday
279 |
280 |
281 |
282 | def get_employee_attendance(employee,atime):
283 | ''' select DATE('date time');'''
284 | employee = employee
285 | atime = atime
286 |
287 | attendance_list = frappe.db.sql(
288 | """
289 | SELECT name,employee,status,attendance_date,shift FROM `tabAttendance`
290 | WHERE employee='%s' AND DATE(attendance_date)= DATE('%s') AND docstatus = 1 ORDER BY attendance_date ASC
291 | """%(employee,atime), as_dict=1
292 | )
293 | return attendance_list
294 |
295 |
296 | @frappe.whitelist()
297 | def date_is_in_holiday_list(employee, date):
298 | holiday_list = frappe.db.get_value("Employee", employee, "holiday_list")
299 | if not holiday_list:
300 | frappe.msgprint(_("Holiday list not set in {0}").format(employee))
301 | return False
302 |
303 | holidays = frappe.db.sql(
304 | """
305 | SELECT holiday_date FROM `tabHoliday`
306 | WHERE parent=%s AND holiday_date=%s
307 | """,(holiday_list, getdate(date))
308 | )
309 |
310 | return len(holidays) > 0
311 |
312 |
313 |
314 | # ----------------------------------------------------------------------
315 | # WORK ANNIVERSARY REMINDERS SEND TO EMPLOYEES LIST IN HR-ADDON-SETTINGS
316 | # ----------------------------------------------------------------------
317 | def send_work_anniversary_notification():
318 | """Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
319 | if not int(frappe.db.get_single_value("HR Addon Settings", "enable_work_anniversaries_notification")):
320 | return
321 |
322 | ############## Sending email to specified employees in HR Addon Settings field anniversary_notification_email_list
323 | emp_email_list = frappe.db.get_all("Employee Item", {"parent": "HR Addon Settings", "parentfield": "anniversary_notification_email_list"}, "employee")
324 | recipients = []
325 | for employee in emp_email_list:
326 | employee_doc = frappe.get_doc("Employee", employee)
327 | employee_email = employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
328 | if employee_email:
329 | recipients.append({"employee_email": employee_email, "company": employee_doc.company})
330 | else:
331 | frappe.throw(_("Email not set for {0}".format(employee)))
332 |
333 | if not recipients:
334 | frappe.throw(_("Recipient Employees not set in field 'Anniversary Notification Email List'"))
335 |
336 | joining_date = today()
337 | employees_joined_today = get_employees_having_an_event_on_given_date("work_anniversary", joining_date)
338 | send_emails(employees_joined_today, recipients, joining_date)
339 |
340 | ############## Sending email to specified employees with Role in HR Addon Settings field anniversary_notification_email_recipient_role
341 | email_recipient_role = frappe.db.get_single_value("HR Addon Settings", "anniversary_notification_email_recipient_role")
342 | notification_x_days_before = int(frappe.db.get_single_value("HR Addon Settings", "notification_x_days_before"))
343 | joining_date = frappe.utils.add_days(today(), notification_x_days_before)
344 | employees_joined_seven_days_later = get_employees_having_an_event_on_given_date("work_anniversary", joining_date)
345 | if email_recipient_role:
346 | role_email_recipients = []
347 | users_with_role = get_info_based_on_role(email_recipient_role, field="email")
348 | for user in users_with_role:
349 | emp_data = frappe.get_cached_value("Employee", {"user_id": user}, ["company", "user_id"], as_dict=True)
350 | if emp_data:
351 | role_email_recipients.extend([{"employee_email": emp_data.get("user_id"), "company": emp_data.get("company")}])
352 | else:
353 | # leave approver not set
354 | pass
355 | # frappe.msgprint(cstr(anniversary_person))
356 |
357 | if role_email_recipients:
358 | send_emails(employees_joined_seven_days_later, role_email_recipients, joining_date)
359 |
360 | ############## Sending email to specified employee leave approvers if HR Addon Settings field enable_work_anniversaries_notification_for_leave_approvers is checked
361 | if int(frappe.db.get_single_value("HR Addon Settings", "enable_work_anniversaries_notification_for_leave_approvers")):
362 | for company, anniversary_persons in employees_joined_seven_days_later.items():
363 | for anniversary_person in anniversary_persons:
364 | if anniversary_person.get("leave_approver"):
365 | leave_approver_recipients = [anniversary_person.get("leave_approver")]
366 |
367 | reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons, joining_date)
368 | send_work_anniversary_reminder(leave_approver_recipients, reminder_text, anniversary_persons, message)
369 |
370 | else:
371 | # leave approver not set
372 | pass
373 | # frappe.msgprint(cstr(anniversary_person))
374 |
375 |
376 | def send_emails(employees_joined_today, recipients, joining_date):
377 |
378 | for company, anniversary_persons in employees_joined_today.items():
379 | reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons, joining_date)
380 | recipients_by_company = [d.get('employee_email') for d in recipients if d.get('company') == company ]
381 | if recipients_by_company:
382 | send_work_anniversary_reminder(recipients_by_company, reminder_text, anniversary_persons, message)
383 |
384 |
385 | def get_employees_having_an_event_on_given_date(event_type, date):
386 | """Get all employee who have `event_type` on specific_date
387 | & group them based on their company. `event_type`
388 | can be `birthday` or `work_anniversary`"""
389 |
390 | from collections import defaultdict
391 |
392 | # Set column based on event type
393 | if event_type == "birthday":
394 | condition_column = "date_of_birth"
395 | elif event_type == "work_anniversary":
396 | condition_column = "date_of_joining"
397 | else:
398 | return
399 |
400 | employees_born_on_given_date = frappe.db.sql("""
401 | SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `leave_approver`, `image`, `date_of_joining`
402 | FROM `tabEmployee`
403 | WHERE
404 | DAY({0}) = DAY(%(date)s)
405 | AND
406 | MONTH({0}) = MONTH(%(date)s)
407 | AND
408 | YEAR({0}) < YEAR(%(date)s)
409 | AND
410 | `status` = 'Active'
411 | """.format(condition_column), {"date": date}, as_dict=1
412 | )
413 | grouped_employees = defaultdict(lambda: [])
414 |
415 | for employee_doc in employees_born_on_given_date:
416 | grouped_employees[employee_doc.get("company")].append(employee_doc)
417 |
418 | return grouped_employees
419 |
420 |
421 | def get_work_anniversary_reminder_text_and_message(anniversary_persons, joining_date):
422 | today_date = today()
423 | if joining_date == today_date:
424 | days_alias = "Today"
425 | completed = "completed"
426 |
427 | elif joining_date > today_date:
428 | days_alias = "{0} days later".format(date_diff(joining_date, today_date))
429 | completed = "will complete"
430 |
431 | if len(anniversary_persons) == 1:
432 | anniversary_person = anniversary_persons[0]["name"]
433 | persons_name = anniversary_person
434 | # Number of years completed at the company
435 | completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
436 | anniversary_person += f" {completed} {get_pluralized_years(completed_years)}"
437 | else:
438 | person_names_with_years = []
439 | names = []
440 | for person in anniversary_persons:
441 | person_text = person["name"]
442 | names.append(person_text)
443 | # Number of years completed at the company
444 | completed_years = getdate().year - person["date_of_joining"].year
445 | person_text += f" {completed} {get_pluralized_years(completed_years)}"
446 | person_names_with_years.append(person_text)
447 |
448 | # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
449 | anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False)
450 | persons_name = comma_sep(names, frappe._("{0} & {1}"), False)
451 |
452 | reminder_text = _("{0} {1} at our Company! 🎉").format(days_alias, anniversary_person)
453 | message = _("A friendly reminder of an important date for our team.")
454 | message += "
"
455 | message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name)
456 |
457 | return reminder_text, message
458 |
459 |
460 | def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
461 | frappe.sendmail(
462 | recipients=recipients,
463 | subject=_("Work Anniversary Reminder"),
464 | template="anniversary_reminder",
465 | args=dict(
466 | reminder_text=reminder_text,
467 | anniversary_persons=anniversary_persons,
468 | message=message,
469 | ),
470 | header=_("Work Anniversary Reminder"),
471 | )
472 |
473 |
474 | def get_pluralized_years(years):
475 | if years == 1:
476 | return "1 year"
477 | return f"{years} years"
478 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/daily_hours_detail/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/daily_hours_detail/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/daily_hours_detail/daily_hours_detail.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "autoname": "hash",
4 | "creation": "2022-05-05 01:56:46.939267",
5 | "doctype": "DocType",
6 | "editable_grid": 1,
7 | "engine": "InnoDB",
8 | "field_order": [
9 | "day",
10 | "hours",
11 | "hour_time",
12 | "break_minutes"
13 | ],
14 | "fields": [
15 | {
16 | "fieldname": "day",
17 | "fieldtype": "Select",
18 | "in_list_view": 1,
19 | "label": "Day",
20 | "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
21 | "reqd": 1
22 | },
23 | {
24 | "fieldname": "hours",
25 | "fieldtype": "Float",
26 | "in_list_view": 1,
27 | "label": "Hours",
28 | "reqd": 1
29 | },
30 | {
31 | "fieldname": "hour_time",
32 | "fieldtype": "Data",
33 | "label": "Hour Time"
34 | },
35 | {
36 | "fieldname": "break_minutes",
37 | "fieldtype": "Int",
38 | "in_list_view": 1,
39 | "label": "Break Minutes"
40 | }
41 | ],
42 | "istable": 1,
43 | "links": [],
44 | "modified": "2023-05-08 11:33:55.822930",
45 | "modified_by": "Administrator",
46 | "module": "HR Addon",
47 | "name": "Daily Hours Detail",
48 | "owner": "Administrator",
49 | "permissions": [],
50 | "sort_field": "modified",
51 | "sort_order": "DESC"
52 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/daily_hours_detail/daily_hours_detail.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | # import frappe
5 | from frappe.model.document import Document
6 |
7 | class DailyHoursDetail(Document):
8 | pass
9 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_checkins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/employee_checkins/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_checkins/employee_checkins.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_rename": 1,
4 | "autoname": "hash",
5 | "creation": "2022-05-06 15:37:58.407050",
6 | "doctype": "DocType",
7 | "editable_grid": 1,
8 | "engine": "InnoDB",
9 | "field_order": [
10 | "employee_checkin",
11 | "log_type",
12 | "log_time",
13 | "skip_auto_attendance"
14 | ],
15 | "fields": [
16 | {
17 | "fieldname": "employee_checkin",
18 | "fieldtype": "Link",
19 | "in_list_view": 1,
20 | "label": "Employee Checkin",
21 | "options": "Employee Checkin"
22 | },
23 | {
24 | "fetch_from": "employee_checkin.log_type",
25 | "fetch_if_empty": 1,
26 | "fieldname": "log_type",
27 | "fieldtype": "Data",
28 | "in_list_view": 1,
29 | "label": "Log Type",
30 | "read_only": 1
31 | },
32 | {
33 | "fetch_from": "employee_checkin.time",
34 | "fetch_if_empty": 1,
35 | "fieldname": "log_time",
36 | "fieldtype": "Data",
37 | "in_list_view": 1,
38 | "label": "Log Time"
39 | },
40 | {
41 | "fetch_from": "employee_checkin.skip_auto_attendance",
42 | "fieldname": "skip_auto_attendance",
43 | "fieldtype": "Data",
44 | "label": "Skip Auto Attendance"
45 | }
46 | ],
47 | "index_web_pages_for_search": 1,
48 | "istable": 1,
49 | "links": [],
50 | "modified": "2022-05-06 15:54:04.142729",
51 | "modified_by": "Administrator",
52 | "module": "HR Addon",
53 | "name": "Employee Checkins",
54 | "owner": "Administrator",
55 | "permissions": [],
56 | "sort_field": "modified",
57 | "sort_order": "DESC"
58 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_checkins/employee_checkins.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | # import frappe
5 | from frappe.model.document import Document
6 |
7 | class EmployeeCheckins(Document):
8 | pass
9 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_item/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/employee_item/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_item/employee_item.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_rename": 1,
4 | "creation": "2024-05-31 11:01:34.865180",
5 | "doctype": "DocType",
6 | "engine": "InnoDB",
7 | "field_order": [
8 | "section_break_m5my",
9 | "employee"
10 | ],
11 | "fields": [
12 | {
13 | "fieldname": "section_break_m5my",
14 | "fieldtype": "Section Break"
15 | },
16 | {
17 | "fieldname": "employee",
18 | "fieldtype": "Link",
19 | "in_list_view": 1,
20 | "label": "Employee",
21 | "options": "Employee"
22 | }
23 | ],
24 | "index_web_pages_for_search": 1,
25 | "istable": 1,
26 | "links": [],
27 | "modified": "2024-05-31 11:02:40.853991",
28 | "modified_by": "Administrator",
29 | "module": "HR Addon",
30 | "name": "Employee Item",
31 | "owner": "Administrator",
32 | "permissions": [],
33 | "sort_field": "modified",
34 | "sort_order": "DESC",
35 | "states": []
36 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/employee_item/employee_item.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | # import frappe
5 | from frappe.model.document import Document
6 |
7 |
8 | class EmployeeItem(Document):
9 | pass
10 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/hr_addon_settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/hr_addon_settings/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/hr_addon_settings/hr_addon_settings.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022, Jide Olayinka and contributors
2 | // For license information, please see license.txt
3 |
4 | frappe.ui.form.on('HR Addon Settings', {
5 | refresh: function(frm) {
6 | },
7 |
8 | validate: function(frm){
9 | if (frm.doc.name_of_calendar_export_ics_file && frm.doc.name_of_calendar_export_ics_file.length == 0){
10 | const randomString = generateRandomString(24);
11 | frm.set_value("name_of_calendar_export_ics_file", randomString)
12 | }
13 | },
14 |
15 | after_save: function(frm){
16 | if (frm.doc.name_of_calendar_export_ics_file && frm.doc.name_of_calendar_export_ics_file.length < 24){
17 | frappe.msgprint("The filename is less than 24 characters. Please, consider to have a longer filename or leave it empty to get a random filename.")
18 | }
19 | },
20 |
21 | download_ics_file: function(frm){
22 | frappe.call({
23 | method: 'hr_addon.hr_addon.doctype.hr_addon_settings.hr_addon_settings.download_ics_file',
24 | callback: function (r) {
25 | var blob = new Blob([r.message], { type: 'application/octet-stream' });
26 | var link = document.createElement('a');
27 | link.href = window.URL.createObjectURL(blob);
28 | link.download = frm.doc.name_of_calendar_export_ics_file + ".ics";
29 | link.click();
30 | }
31 | });
32 | },
33 |
34 | generate_workdays_for_past_7_days_now: function(frm){
35 | frappe.call({
36 | method: "hr_addon.hr_addon.doctype.hr_addon_settings.hr_addon_settings.generate_workdays_for_past_7_days_now",
37 | }).then(r => {
38 | frappe.msgprint("The workdays have been generated.")
39 | })
40 | }
41 | });
42 |
43 | function generateRandomString(length) {
44 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
45 | let randomString = '';
46 |
47 | for (let i = 0; i < length; i++) {
48 | const randomIndex = Math.floor(Math.random() * characters.length);
49 | randomString += characters.charAt(randomIndex);
50 | }
51 |
52 | return randomString;
53 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/hr_addon_settings/hr_addon_settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_rename": 1,
4 | "creation": "2022-05-13 22:53:32.840880",
5 | "doctype": "DocType",
6 | "editable_grid": 1,
7 | "engine": "InnoDB",
8 | "field_order": [
9 | "general_section",
10 | "runapp",
11 | "allow_bulk_processing",
12 | "name_of_calendar_export_ics_file",
13 | "ics_folder_path",
14 | "download_ics_file",
15 | "scheduled_job_section",
16 | "day",
17 | "time",
18 | "generate_workdays_for_past_7_days_now",
19 | "enabled",
20 | "column_break_jozi",
21 | "workday_break_calculation_mechanism",
22 | "swap_hours_worked_and_actual_working_hours",
23 | "notification_section",
24 | "anniversary_notification_email_list",
25 | "enable_work_anniversaries_notification",
26 | "column_break_dvlg",
27 | "anniversary_notification_email_recipient_role",
28 | "notification_x_days_before",
29 | "enable_work_anniversaries_notification_for_leave_approvers"
30 | ],
31 | "fields": [
32 | {
33 | "fieldname": "general_section",
34 | "fieldtype": "Section Break",
35 | "label": "General"
36 | },
37 | {
38 | "fieldname": "runapp",
39 | "fieldtype": "Select",
40 | "label": "Run App",
41 | "options": "\nYes\nNo"
42 | },
43 | {
44 | "default": "0",
45 | "fieldname": "allow_bulk_processing",
46 | "fieldtype": "Check",
47 | "label": "Allow Bulk Processing"
48 | },
49 | {
50 | "description": "Just the filename without the extension .ics.
\nNormally the public file Urlaubskalender.ics is created in the following location: Your domain/files/Urlaubskalender.ics\nIf you adjust the name and path in this field, the old file will be deleted the next time the calendar file is overwritten.\nThe file will be regenerated every time an approved \"Leave Application\" is saved.",
51 | "fieldname": "name_of_calendar_export_ics_file",
52 | "fieldtype": "Data",
53 | "label": "Name of calendar export ICS file"
54 | },
55 | {
56 | "description": "Absolute Path of the folder in which the ICS file will be saved, for example: /home/xxxxxxxxxx/owncloud/calendar/. Make sure this folder is writable by frappe user. Leave this field empty if you want to make the file public.",
57 | "fieldname": "ics_folder_path",
58 | "fieldtype": "Data",
59 | "label": "ICS folder path"
60 | },
61 | {
62 | "fieldname": "download_ics_file",
63 | "fieldtype": "Button",
64 | "label": "Download ICS File"
65 | },
66 | {
67 | "description": "To avoid having to generate a workday for each employee individually, you can use this function to generate the workdays for the last 7 days for all employees at the same time. These are the basis for the Work Hour Report.\nTo do this, you should ensure that a Holiday List is linked in all employees and that a Weekly Working Hour has been created for each employee. \nAlso, be sure that all Employee Checkins are entered correctly before generating.",
68 | "fieldname": "scheduled_job_section",
69 | "fieldtype": "Section Break",
70 | "label": "Generate Workdays automatically"
71 | },
72 | {
73 | "description": "It will be generated for every Employee",
74 | "fieldname": "generate_workdays_for_past_7_days_now",
75 | "fieldtype": "Button",
76 | "label": "Generate Workdays for past 7 days now"
77 | },
78 | {
79 | "default": "Sunday",
80 | "fieldname": "day",
81 | "fieldtype": "Select",
82 | "label": "Day",
83 | "options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
84 | },
85 | {
86 | "default": "00",
87 | "fieldname": "time",
88 | "fieldtype": "Select",
89 | "label": "Time",
90 | "options": "00\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23"
91 | },
92 | {
93 | "default": "1",
94 | "fieldname": "enabled",
95 | "fieldtype": "Check",
96 | "label": "Enabled"
97 | },
98 | {
99 | "fieldname": "notification_section",
100 | "fieldtype": "Section Break",
101 | "label": "Anniversary Notification"
102 | },
103 | {
104 | "depends_on": "eval: doc.enable_work_anniversaries_notification",
105 | "description": "Employees selected in this fields will receive all anniversary notification for employees in same company",
106 | "fieldname": "anniversary_notification_email_list",
107 | "fieldtype": "Table MultiSelect",
108 | "label": "Anniversary Notification Email List",
109 | "mandatory_depends_on": "eval: doc.enable_work_anniversaries_notification",
110 | "options": "Employee Item"
111 | },
112 | {
113 | "fieldname": "column_break_dvlg",
114 | "fieldtype": "Column Break"
115 | },
116 | {
117 | "depends_on": "eval: doc.enable_work_anniversaries_notification",
118 | "description": "Employees who has Role selected in this fields will receive all anniversary notification of employees in same company",
119 | "fieldname": "anniversary_notification_email_recipient_role",
120 | "fieldtype": "Link",
121 | "label": "Anniversary Notification Email Recipient Role",
122 | "options": "Role"
123 | },
124 | {
125 | "default": "0",
126 | "depends_on": "eval: doc.enable_work_anniversaries_notification",
127 | "description": "Employees who are leaver approver for other other employees will receive anniversary notification of their leave approvee",
128 | "fieldname": "enable_work_anniversaries_notification_for_leave_approvers",
129 | "fieldtype": "Check",
130 | "label": "Enable Work Anniversaries notification for Leave Approvers",
131 | "mandatory_depends_on": "eval: doc.enable_work_anniversaries_notification"
132 | },
133 | {
134 | "default": "0",
135 | "fieldname": "enable_work_anniversaries_notification",
136 | "fieldtype": "Check",
137 | "label": "Enable Work Anniversaries notification"
138 | },
139 | {
140 | "depends_on": "eval: doc.enable_work_anniversaries_notification",
141 | "description": "Email Recipient Role and Leavers Approvers will receive advance notification (no. of days selected in this field) ",
142 | "fieldname": "notification_x_days_before",
143 | "fieldtype": "Int",
144 | "label": "Notification (x days) before",
145 | "mandatory_depends_on": "eval: doc.enable_work_anniversaries_notification && (doc.anniversary_notification_email_recipient_role || doc.enable_work_anniversaries_notification_for_leave_approvers)"
146 | },
147 | {
148 | "fieldname": "column_break_jozi",
149 | "fieldtype": "Column Break"
150 | },
151 | {
152 | "default": "Break Hours from Employee Checkins",
153 | "fieldname": "workday_break_calculation_mechanism",
154 | "fieldtype": "Select",
155 | "label": "Workday break Calculation Mechanism",
156 | "options": "Break Hours from Employee Checkins\nBreak Hours from Weekly Working Hours\nBreak Hours from Weekly Working Hours if Shorter breaks"
157 | },
158 | {
159 | "default": "0",
160 | "description": "Swapping behavior in Workday for some specific cases",
161 | "fieldname": "swap_hours_worked_and_actual_working_hours",
162 | "fieldtype": "Check",
163 | "label": "Swap Hours worked and Actual Working Hours"
164 | }
165 | ],
166 | "index_web_pages_for_search": 1,
167 | "issingle": 1,
168 | "links": [],
169 | "modified": "2024-11-19 13:18:51.730286",
170 | "modified_by": "Administrator",
171 | "module": "HR Addon",
172 | "name": "HR Addon Settings",
173 | "owner": "Administrator",
174 | "permissions": [
175 | {
176 | "create": 1,
177 | "delete": 1,
178 | "email": 1,
179 | "print": 1,
180 | "read": 1,
181 | "role": "System Manager",
182 | "share": 1,
183 | "write": 1
184 | },
185 | {
186 | "create": 1,
187 | "delete": 1,
188 | "email": 1,
189 | "print": 1,
190 | "read": 1,
191 | "role": "HR Manager",
192 | "share": 1,
193 | "write": 1
194 | }
195 | ],
196 | "sort_field": "modified",
197 | "sort_order": "DESC",
198 | "states": [],
199 | "track_changes": 1
200 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/hr_addon_settings/hr_addon_settings.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe, os
5 | from frappe.model.document import Document
6 |
7 | from hr_addon.hr_addon.doctype.workday.workday import get_unmarked_range, bulk_process_workdays_background
8 |
9 | class HRAddonSettings(Document):
10 | def before_save(self):
11 | # remove the old ics file
12 | old_doc = self.get_doc_before_save()
13 | if old_doc:
14 | old_file_name = old_doc.name_of_calendar_export_ics_file
15 | if old_file_name != self.name_of_calendar_export_ics_file:
16 | os.remove("{}/public/files/{}.ics".format(frappe.utils.get_site_path(), old_file_name))
17 |
18 | # remove also the Urlaubskalender.ics, if exist
19 | if os.path.exists("{}/public/files/Urlaubskalender.ics".format(frappe.utils.get_site_path())):
20 | os.remove("{}/public/files/Urlaubskalender.ics".format(frappe.utils.get_site_path()))
21 |
22 |
23 | @frappe.whitelist()
24 | def download_ics_file():
25 | settings = frappe.get_doc("HR Addon Settings")
26 |
27 | file_name = ""
28 | if settings.ics_folder_path:
29 | file_name = os.path.join(settings.ics_folder_path, settings.name_of_calendar_export_ics_file + ".ics")
30 | else:
31 | file_name = "{}/public/files/{}.ics".format(frappe.utils.get_site_path(), settings.name_of_calendar_export_ics_file)
32 |
33 | if os.path.exists(file_name):
34 | with open(file_name, 'r') as file:
35 | file_content = file.read()
36 | return file_content
37 | else:
38 | frappe.throw(f"File '{file_name}' not found.")
39 |
40 |
41 | @frappe.whitelist()
42 | def generate_workdays_scheduled_job():
43 | try:
44 | hr_addon_settings = frappe.get_doc("HR Addon Settings")
45 | frappe.logger("Creating Workday").error(f"HR Addon Enabled: {hr_addon_settings.enabled}")
46 |
47 | # Check if the HR Addon is enabled
48 | if hr_addon_settings.enabled == 0:
49 | frappe.logger("Creating Workday").error("HR Addon is disabled. Exiting...")
50 | return
51 |
52 | # Mapping weekday numbers to names
53 | number2name_dict = {
54 | 0: "Monday",
55 | 1: "Tuesday",
56 | 2: "Wednesday",
57 | 3: "Thursday",
58 | 4: "Friday",
59 | 5: "Saturday",
60 | 6: "Sunday"
61 | }
62 |
63 | # Get the current date and time
64 | now = frappe.utils.get_datetime()
65 | today_weekday_number = now.weekday()
66 | weekday_name = number2name_dict[today_weekday_number]
67 |
68 | # Log the current day and hour
69 | frappe.logger("Creating Workday").error(f"Today is {weekday_name}, current hour is {now.hour}")
70 | frappe.logger("Creating Workday").error(f"HR Addon Settings day is {hr_addon_settings.day}, time is {hr_addon_settings.time}")
71 |
72 | # Check if the current day and hour match the settings
73 | if weekday_name == hr_addon_settings.day:
74 | frappe.logger("Creating Workday").error("Day matched.")
75 | if now.hour == int(hr_addon_settings.time):
76 | frappe.logger("Creating Workday").error("Time matched. Generating workdays...")
77 | # Trigger workdays generation
78 | generate_workdays_for_past_7_days_now()
79 | else:
80 | frappe.logger("Creating Workday").error(f"Time mismatch. Current hour: {now.hour}, Expected hour: {hr_addon_settings.time}")
81 | else:
82 | frappe.logger("Creating Workday").error(f"Day mismatch. Today: {weekday_name}, Expected: {hr_addon_settings.day}")
83 | except Exception as e:
84 | frappe.log_error("Error in generate_workdays_scheduled_job: {}".format(str(e)), "Scheduled Job Error")
85 |
86 |
87 |
88 | @frappe.whitelist()
89 | def generate_workdays_for_past_7_days_now():
90 | try:
91 | today = frappe.utils.get_datetime()
92 | a_week_ago = today - frappe.utils.datetime.timedelta(days=7)
93 | frappe.logger("Creating Workday").error(f"Processing from {a_week_ago} to {today}")
94 |
95 | # Get all active employees
96 | employees = frappe.db.get_list("Employee", filters={"status": "Active"})
97 |
98 | # Log the list of employees for debugging
99 | frappe.logger("Creating Workday").error(f"Active employees: {employees}")
100 |
101 | # Process each employee
102 | for employee in employees:
103 | employee_name = employee["name"]
104 |
105 | # Log each employee name for debugging
106 | frappe.logger("Creating Workday").error(f"Processing employee: {employee_name}")
107 |
108 | try:
109 | # Get unmarked workdays for the past 7 days
110 | unmarked_days = get_unmarked_range(employee_name, a_week_ago.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d"))
111 | frappe.logger("Creating Workday").error(f"Unmarked days for {employee_name}: {unmarked_days}")
112 |
113 | # Prepare data and trigger bulk processing
114 | data = {
115 | "employee": employee_name,
116 | "unmarked_days": unmarked_days
117 | }
118 | flag = "Create workday"
119 |
120 | # Add a try-catch block for the bulk processing
121 | try:
122 | bulk_process_workdays_background(data, flag)
123 | frappe.logger("Creating Workday").error(f"Workdays successfully processed for {employee_name}")
124 | except Exception as e:
125 | frappe.log_error(
126 | "employee_name: {}, error: {} \n{}".format(employee_name, str(e), frappe.get_traceback()),
127 | "Error during bulk processing for employee"
128 | )
129 | except Exception as e:
130 | frappe.log_error(
131 | "Creating Workday, Got Error: {} while fetching unmarked days for: {}".format(str(e), employee_name),
132 | "Error during fetching unmarked days"
133 | )
134 | except Exception as e:
135 | frappe.log_error(
136 | "Creating Workday: Error in generate_workdays_for_past_7_days_now: {}".format(str(e)),
137 | "Error during generate_workdays_for_past_7_days_now"
138 | )
139 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/hr_addon_settings/test_hr_addon_settings.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and Contributors
2 | # See license.txt
3 |
4 | # import frappe
5 | import unittest
6 |
7 | class TestHRAddonSettings(unittest.TestCase):
8 | pass
9 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/weekly_working_hours/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/weekly_working_hours/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/weekly_working_hours/test_weekly_working_hours.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and Contributors
2 | # See license.txt
3 |
4 | # import frappe
5 | import unittest
6 |
7 | class TestWeeklyWorkingHours(unittest.TestCase):
8 | pass
9 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/weekly_working_hours/weekly_working_hours.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022, Jide Olayinka and contributors
2 | // For license information, please see license.txt
3 |
4 | frappe.ui.form.on('Weekly Working Hours', {
5 | setup: function(frm){
6 | frm.check_days_for_duplicates = function(frm,row){
7 | frm.doc.hours.forEach(tm => {
8 | if(!( row.day =='' || tm.idx==row.idx)){
9 | if(row.day==tm.day){
10 | row.hours = '';
11 | //console.log('Work hour already set for ');
12 | }
13 |
14 | }
15 | });
16 | }
17 |
18 | frm.get_total_hours = function(frm){
19 | let total_hour = 0;
20 | let t_hour_time = 0;
21 | frm.doc.hours.forEach(tm=>{
22 | total_hour += flt(tm.hours);
23 | //in sec total
24 | t_hour_time += flt(tm.hour_time);
25 | })
26 | frm.set_value("total_work_hours",total_hour);
27 | // set total in seconds
28 | frm.set_value("total_hours_time",t_hour_time);
29 | }
30 |
31 | frm.set_work_hour_in_seconds = function(frm,tm){
32 | $.each(frm.doc.hours|| [], function(i,d){
33 | d.hour_time = flt(d.hours)*60*60;
34 | });
35 |
36 | }
37 | }
38 |
39 | // refresh: function(frm) {
40 |
41 | // }
42 | });
43 |
44 | frappe.ui.form.on('Daily Hours Detail',{
45 |
46 | hours: function(frm, cdt, cdn){
47 | //set in time
48 | let row = locals[cdt][cdn];
49 | frm.set_work_hour_in_seconds(frm);
50 |
51 | frm.check_days_for_duplicates(frm, row);
52 | frm.get_total_hours(frm);
53 |
54 | frm.refresh_field('hours');
55 |
56 | },
57 |
58 | hours_remove: function(frm,cdt,cdn){
59 | frm.get_total_hours(frm);
60 | }
61 | });
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/weekly_working_hours/weekly_working_hours.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_rename": 1,
4 | "autoname": "field:title_hour",
5 | "creation": "2022-05-05 01:42:08.757292",
6 | "doctype": "DocType",
7 | "editable_grid": 1,
8 | "engine": "InnoDB",
9 | "field_order": [
10 | "title_hour",
11 | "employee",
12 | "employee_name",
13 | "valid_from",
14 | "valid_to",
15 | "column_break_6",
16 | "holiday_list",
17 | "shift",
18 | "note",
19 | "weekly_working_hours_section",
20 | "hours",
21 | "section_break_11",
22 | "total_work_hours",
23 | "no_break_hours",
24 | "set_target_hours_to_zero_when_date_is_holiday",
25 | "column_break_14",
26 | "total_hours_time",
27 | "amended_from",
28 | "company"
29 | ],
30 | "fields": [
31 | {
32 | "fieldname": "title_hour",
33 | "fieldtype": "Data",
34 | "in_list_view": 1,
35 | "label": "Hour Title",
36 | "read_only": 1,
37 | "set_only_once": 1,
38 | "unique": 1
39 | },
40 | {
41 | "fieldname": "employee",
42 | "fieldtype": "Link",
43 | "label": "Employee",
44 | "options": "Employee",
45 | "reqd": 1
46 | },
47 | {
48 | "fetch_from": "employee.employee_name",
49 | "fieldname": "employee_name",
50 | "fieldtype": "Data",
51 | "label": "Employee Name",
52 | "read_only": 1
53 | },
54 | {
55 | "fieldname": "valid_from",
56 | "fieldtype": "Date",
57 | "label": "From Date",
58 | "reqd": 1
59 | },
60 | {
61 | "fieldname": "valid_to",
62 | "fieldtype": "Date",
63 | "label": "To Date",
64 | "reqd": 1
65 | },
66 | {
67 | "fieldname": "column_break_6",
68 | "fieldtype": "Column Break"
69 | },
70 | {
71 | "fieldname": "holiday_list",
72 | "fieldtype": "Link",
73 | "hidden": 1,
74 | "label": "Holiday List",
75 | "options": "Holiday List"
76 | },
77 | {
78 | "fieldname": "shift",
79 | "fieldtype": "Link",
80 | "label": "Shift",
81 | "options": "Shift Type"
82 | },
83 | {
84 | "fieldname": "note",
85 | "fieldtype": "Small Text",
86 | "label": "Note"
87 | },
88 | {
89 | "fieldname": "weekly_working_hours_section",
90 | "fieldtype": "Section Break",
91 | "label": "Weekly Working Hours"
92 | },
93 | {
94 | "fieldname": "section_break_11",
95 | "fieldtype": "Section Break"
96 | },
97 | {
98 | "default": "0",
99 | "fieldname": "total_work_hours",
100 | "fieldtype": "Float",
101 | "label": "Total Work Hours",
102 | "read_only": 1
103 | },
104 | {
105 | "fieldname": "total_hours_time",
106 | "fieldtype": "Data",
107 | "label": "Total Hours (Time)",
108 | "read_only": 1
109 | },
110 | {
111 | "fieldname": "amended_from",
112 | "fieldtype": "Link",
113 | "label": "Amended From",
114 | "no_copy": 1,
115 | "options": "Weekly Working Hours",
116 | "print_hide": 1,
117 | "read_only": 1
118 | },
119 | {
120 | "allow_bulk_edit": 1,
121 | "fieldname": "hours",
122 | "fieldtype": "Table",
123 | "label": "Hours",
124 | "options": "Daily Hours Detail",
125 | "reqd": 1
126 | },
127 | {
128 | "fieldname": "column_break_14",
129 | "fieldtype": "Column Break"
130 | },
131 | {
132 | "fieldname": "company",
133 | "fieldtype": "Link",
134 | "label": "Company",
135 | "options": "Company"
136 | },
137 | {
138 | "default": "0",
139 | "fieldname": "no_break_hours",
140 | "fieldtype": "Check",
141 | "label": "No break hours if target hours is less than 6 hours"
142 | },
143 | {
144 | "default": "0",
145 | "fieldname": "set_target_hours_to_zero_when_date_is_holiday",
146 | "fieldtype": "Check",
147 | "label": "Set Target Hours to Zero when date is holiday"
148 | }
149 | ],
150 | "is_submittable": 1,
151 | "links": [],
152 | "modified": "2023-07-27 09:16:56.101160",
153 | "modified_by": "Administrator",
154 | "module": "HR Addon",
155 | "name": "Weekly Working Hours",
156 | "owner": "Administrator",
157 | "permissions": [
158 | {
159 | "create": 1,
160 | "delete": 1,
161 | "email": 1,
162 | "export": 1,
163 | "print": 1,
164 | "read": 1,
165 | "report": 1,
166 | "role": "System Manager",
167 | "share": 1,
168 | "write": 1
169 | },
170 | {
171 | "create": 1,
172 | "delete": 1,
173 | "email": 1,
174 | "export": 1,
175 | "print": 1,
176 | "read": 1,
177 | "report": 1,
178 | "role": "HR Manager",
179 | "share": 1,
180 | "write": 1
181 | }
182 | ],
183 | "sort_field": "modified",
184 | "sort_order": "DESC",
185 | "track_changes": 1
186 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/weekly_working_hours/weekly_working_hours.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe
5 | from frappe.model.document import Document
6 | from frappe.utils import getdate
7 | from frappe.model.naming import make_autoname
8 | from frappe import _
9 |
10 | class WeeklyWorkingHours(Document):
11 | def autoname(self):
12 | coy = frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
13 | e_name = self.employee
14 | name_key = coy+'-.YYYY.-'+e_name+'-.####'
15 | self.name = make_autoname(name_key)
16 | self.title_hour= self.name
17 |
18 | def validate(self):
19 | self.validate_if_employee_is_active()
20 | self.validate_overlapping_records_in_specific_interval()
21 |
22 | def validate_if_employee_is_active(self):
23 | if self.employee and frappe.get_value('Employee', self.employee, 'status') != "Active":
24 | frappe.throw(_("{0} is not active").format(frappe.get_desk_link('Employee', self.employee)))
25 |
26 | def validate_overlapping_records_in_specific_interval(self):
27 |
28 | if not self.valid_from or not self.valid_to:
29 | frappe.throw("From Date and To Date are required.")
30 |
31 | if not self.employee:
32 | frappe.throw("Employee required.")
33 |
34 | valid_from = getdate(self.valid_from)
35 | valid_to = getdate(self.valid_to)
36 |
37 | filters = {"valid_from": valid_from, "valid_to": valid_to, "employee": self.employee}
38 |
39 | condition = ""
40 | if not self.is_new():
41 | condition += "AND name != %(name)s "
42 | filters["name"] = self.name
43 |
44 | overlapping_records = frappe.db.sql("""
45 | SELECT name
46 | FROM `tabWeekly Working Hours`
47 | WHERE
48 | (
49 | (valid_from <= %(valid_from)s AND valid_to >= %(valid_to)s) OR
50 | (valid_from <= %(valid_from)s AND valid_to >= %(valid_to)s) OR
51 | (valid_from >= %(valid_from)s AND valid_to <= %(valid_to)s)
52 | ) AND employee = %(employee)s AND docstatus = 1 {condition}
53 | """.format(condition=condition), filters, as_dict=True)
54 |
55 | if overlapping_records:
56 | overlapping_records = "
".join([frappe.get_desk_link("Weekly Working Hours", d.name) for d in overlapping_records])
57 | frappe.throw("Following Weekly Working Hours record already exists for {0} for the specified date range:
{1}".format(frappe.get_desk_link("Employee", self.employee), overlapping_records))
58 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/doctype/workday/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/test_workday.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and Contributors
2 | # See license.txt
3 |
4 | # import frappe
5 | import unittest
6 |
7 | class TestWorkday(unittest.TestCase):
8 | pass
9 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/workday.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022, Jide Olayinka and contributors
2 | // For license information, please see license.txt
3 |
4 | frappe.ui.form.on("Workday", {
5 | refresh: function (frm) {
6 | set_color_red(frm);
7 | },
8 | setup: function (frm) {
9 | frm.set_query("attendance", function () {
10 | return {
11 | filters: [
12 | ["Attendance", "employee", "=", frm.doc.employee],
13 | ["Attendance", "attendance_date", "=", frm.doc.log_date],
14 | ],
15 | };
16 | });
17 | /* frm.set_query('Employee Checkins','employee_checkins', function(){
18 | return{
19 | 'filters':[
20 | ['employee_checkins','employee_checkin','=',frm.doc.attendance]
21 |
22 | ],
23 | };
24 | }); */
25 | },
26 | /* onload: function(frm){
27 | frm.set_query('Employee Checkins','employee_checkins', function(){
28 | return{
29 | 'filters':{
30 | 'employee_checkin':['=',frm.attendance]
31 | }
32 | };
33 | });
34 | }, */
35 |
36 | attendance: function (frm) {
37 | get_hours(frm);
38 | },
39 | log_date: function (frm) {
40 | if (frm.doc.employee && frm.doc.log_date) {
41 | frappe.call({
42 | method: "hr_addon.hr_addon.api.utils.date_is_in_holiday_list",
43 | args: {
44 | employee: frm.doc.employee,
45 | date: frm.doc.log_date,
46 | },
47 | callback: function (r) {
48 | if (r.message == true) {
49 | frappe.msgprint("Given Date is Holiday");
50 | unset_fields(frm);
51 | } else {
52 | get_hours(frm);
53 | }
54 | },
55 | });
56 | }
57 | },
58 |
59 | status(frm) {
60 | if (frm.doc.status === "On Leave") {
61 | setTimeout(() => {
62 | frm.set_value("target_hours", 0);
63 | frm.set_value("expected_break_hours", 0);
64 | frm.set_value("actual_working_hours", 0);
65 | frm.set_value("total_target_seconds", 0);
66 | frm.set_value("total_break_seconds", 0);
67 | frm.set_value("total_work_seconds", 0);
68 | }, 1000);
69 | } // TODO: consider case of frm.doc.status === "Half Day"
70 | },
71 | });
72 |
73 | var get_hours = function (frm) {
74 | let aemployee = frm.doc.employee;
75 | let adate = frm.doc.log_date;
76 | if (aemployee && adate) {
77 | frappe
78 | .call({
79 | method: "hr_addon.hr_addon.api.utils.get_actual_employee_log",
80 | args: { aemployee: aemployee, adate: adate },
81 | })
82 | .done((r) => {
83 | if (r.message && Object.keys(r.message).length > 0) {
84 | frm.doc.employee_checkins = [];
85 | let alog = r.message;
86 |
87 | frm.set_value("hours_worked", alog.hours_worked);
88 | frm.set_value("break_hours", alog.break_hours);
89 | frm.set_value("total_work_seconds", alog.total_work_seconds);
90 | frm.set_value("total_break_seconds", alog.total_break_seconds);
91 | frm.set_value("target_hours", alog.target_hours);
92 | frm.set_value("expected_break_hours", alog.expected_break_hours);
93 | frm.set_value("total_target_seconds", alog.total_target_seconds);
94 | frm.set_value("manual_workday",alog.manual_workday);
95 | frm.set_value("actual_working_hours", alog.actual_working_hours);
96 | let employee_checkins = alog.employee_checkins;
97 | if (employee_checkins.length > 0) {
98 | frm.set_value("first_checkin", employee_checkins[0].time);
99 | frm.set_value(
100 | "last_checkout",
101 | employee_checkins[employee_checkins.length - 1].time
102 | );
103 | $.each(employee_checkins, function (i, e) {
104 | let nw_checkins = frm.add_child("employee_checkins");
105 | nw_checkins.employee_checkin = e.name;
106 | nw_checkins.log_type = e.log_type;
107 | nw_checkins.log_time = e.time;
108 | nw_checkins.skip_auto_attendance = e.skip_auto_attendance;
109 | refresh_field("employee_checkins");
110 | set_color_red(frm);
111 | });
112 | }
113 | } else {
114 | unset_fields(frm);
115 | }
116 | });
117 | }
118 | };
119 |
120 | var unset_fields = function (frm) {
121 | frm.set_value("hours_worked", 0);
122 | frm.set_value("break_hours", 0);
123 | frm.set_value("total_work_seconds", 0);
124 | frm.set_value("total_break_seconds", 0);
125 | frm.set_value("target_hours", 0);
126 | frm.set_value("total_target_seconds", 0);
127 | frm.set_value("expected_break_hours", 0);
128 | frm.set_value("actual_working_hours", 0);
129 | frm.set_value("employee_checkins", []);
130 | frm.set_value("first_checkin", "");
131 | frm.set_value("last_checkout", "");
132 | frm.refresh_fields();
133 | };
134 |
135 | var set_color_red = function (frm) {
136 | if (frm.doc.break_hours == -360 && frm.doc.hours_worked == -36) {
137 | $(".control-input-wrapper > .control-value.like-disabled-input").css(
138 | "color",
139 | "red"
140 | );
141 | } else {
142 | // Reset the color to default (or any other color) when the condition is not met
143 | $(".control-input-wrapper > .control-value.like-disabled-input").css(
144 | "color",
145 | ""
146 | );
147 | }
148 | };
149 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/workday.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "allow_rename": 1,
4 | "autoname": "format:WD-{YYYY}-{#####}",
5 | "creation": "2022-05-06 15:25:52.037042",
6 | "doctype": "DocType",
7 | "editable_grid": 1,
8 | "engine": "InnoDB",
9 | "field_order": [
10 | "employee",
11 | "attendance",
12 | "acolumn_break_1",
13 | "log_date",
14 | "status",
15 | "manual_workday",
16 | "section_break_7",
17 | "employee_checkins",
18 | "section_break_9",
19 | "target_hours",
20 | "hours_worked",
21 | "actual_working_hours",
22 | "column_break_5",
23 | "expected_break_hours",
24 | "break_hours",
25 | "number_of_breaks",
26 | "section_break_14",
27 | "first_checkin",
28 | "last_checkout",
29 | "column_break_17",
30 | "company",
31 | "total_work_seconds",
32 | "total_break_seconds",
33 | "total_target_seconds"
34 | ],
35 | "fields": [
36 | {
37 | "fieldname": "employee",
38 | "fieldtype": "Link",
39 | "label": "Employee",
40 | "options": "Employee"
41 | },
42 | {
43 | "fieldname": "log_date",
44 | "fieldtype": "Date",
45 | "label": "Date"
46 | },
47 | {
48 | "fieldname": "attendance",
49 | "fieldtype": "Link",
50 | "label": "Attendance",
51 | "options": "Attendance"
52 | },
53 | {
54 | "fetch_from": "attendance.status",
55 | "fetch_if_empty": 1,
56 | "fieldname": "status",
57 | "fieldtype": "Data",
58 | "label": "Status"
59 | },
60 | {
61 | "fieldname": "target_hours",
62 | "fieldtype": "Float",
63 | "label": "Target Hours",
64 | "permlevel": 1
65 | },
66 | {
67 | "fieldname": "hours_worked",
68 | "fieldtype": "Float",
69 | "in_list_view": 1,
70 | "label": "Hours worked",
71 | "permlevel": 1
72 | },
73 | {
74 | "fieldname": "number_of_breaks",
75 | "fieldtype": "Int",
76 | "label": "Number of Breaks",
77 | "permlevel": 1
78 | },
79 | {
80 | "fieldname": "break_hours",
81 | "fieldtype": "Float",
82 | "label": "Break Hours",
83 | "permlevel": 1
84 | },
85 | {
86 | "fieldname": "employee_checkins",
87 | "fieldtype": "Table",
88 | "label": "Employee Checkins",
89 | "options": "Employee Checkins",
90 | "read_only": 1
91 | },
92 | {
93 | "fieldname": "company",
94 | "fieldtype": "Link",
95 | "label": "Company",
96 | "options": "Company"
97 | },
98 | {
99 | "fieldname": "column_break_5",
100 | "fieldtype": "Column Break"
101 | },
102 | {
103 | "fieldname": "acolumn_break_1",
104 | "fieldtype": "Column Break"
105 | },
106 | {
107 | "fieldname": "section_break_7",
108 | "fieldtype": "Section Break"
109 | },
110 | {
111 | "fieldname": "section_break_9",
112 | "fieldtype": "Section Break"
113 | },
114 | {
115 | "fieldname": "section_break_14",
116 | "fieldtype": "Section Break"
117 | },
118 | {
119 | "fieldname": "first_checkin",
120 | "fieldtype": "Data",
121 | "label": "First Checkin",
122 | "read_only": 1
123 | },
124 | {
125 | "fieldname": "last_checkout",
126 | "fieldtype": "Data",
127 | "label": "Last Checkout",
128 | "read_only": 1
129 | },
130 | {
131 | "fieldname": "column_break_17",
132 | "fieldtype": "Column Break"
133 | },
134 | {
135 | "fieldname": "total_work_seconds",
136 | "fieldtype": "Float",
137 | "hidden": 1,
138 | "label": "Worked In Seconds",
139 | "read_only": 1
140 | },
141 | {
142 | "fieldname": "total_break_seconds",
143 | "fieldtype": "Float",
144 | "hidden": 1,
145 | "label": "Break In Seconds",
146 | "read_only": 1
147 | },
148 | {
149 | "fieldname": "total_target_seconds",
150 | "fieldtype": "Float",
151 | "hidden": 1,
152 | "label": " Target In Seconds",
153 | "read_only": 1
154 | },
155 | {
156 | "fieldname": "actual_working_hours",
157 | "fieldtype": "Float",
158 | "label": "Actual Working Hours",
159 | "permlevel": 1
160 | },
161 | {
162 | "fieldname": "expected_break_hours",
163 | "fieldtype": "Float",
164 | "label": "Expected Break Hours",
165 | "permlevel": 1
166 | },
167 | {
168 | "default": "0",
169 | "fieldname": "manual_workday",
170 | "fieldtype": "Check",
171 | "label": "Manual Workday",
172 | "permlevel": 1
173 | }
174 | ],
175 | "links": [],
176 | "modified": "2024-12-05 22:28:58.266573",
177 | "modified_by": "Administrator",
178 | "module": "HR Addon",
179 | "name": "Workday",
180 | "naming_rule": "Expression",
181 | "owner": "Administrator",
182 | "permissions": [
183 | {
184 | "create": 1,
185 | "delete": 1,
186 | "email": 1,
187 | "export": 1,
188 | "print": 1,
189 | "read": 1,
190 | "report": 1,
191 | "role": "System Manager",
192 | "share": 1,
193 | "write": 1
194 | },
195 | {
196 | "create": 1,
197 | "delete": 1,
198 | "email": 1,
199 | "export": 1,
200 | "print": 1,
201 | "read": 1,
202 | "report": 1,
203 | "role": "HR Manager",
204 | "share": 1,
205 | "write": 1
206 | },
207 | {
208 | "email": 1,
209 | "export": 1,
210 | "permlevel": 1,
211 | "print": 1,
212 | "report": 1,
213 | "role": "System Manager",
214 | "share": 1,
215 | "write": 1
216 | },
217 | {
218 | "email": 1,
219 | "export": 1,
220 | "permlevel": 1,
221 | "print": 1,
222 | "read": 1,
223 | "report": 1,
224 | "role": "HR Manager",
225 | "share": 1
226 | },
227 | {
228 | "export": 1,
229 | "read": 1,
230 | "report": 1,
231 | "role": "Employee",
232 | "select": 1
233 | }
234 | ],
235 | "sort_field": "modified",
236 | "sort_order": "DESC",
237 | "states": []
238 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/workday.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe
5 | from frappe import _
6 | from frappe.model.document import Document
7 | from frappe.utils import cint, get_datetime, getdate ,add_days,formatdate,flt
8 | from frappe.utils.data import date_diff
9 | import traceback
10 | from hr_addon.hr_addon.api.utils import get_actual_employee_log_for_bulk_process
11 |
12 |
13 | class Workday(Document):
14 | def validate(self):
15 | self.date_is_in_comp_off()
16 | self.validate_duplicate_workday()
17 | self.set_status_for_leave_application()
18 | # self.set_manual_workday()
19 |
20 | def set_status_for_leave_application(self):
21 | leave_application = frappe.db.exists(
22 | "Leave Application", {
23 | "employee": self.employee,
24 | "from_date": ("<=", self.log_date),
25 | "to_date": (">=", self.log_date),
26 | "leave_type": ['not in',["Freizeitausgleich (Nicht buchen!)","Compensatory Off"]],
27 | 'docstatus': 1
28 | }
29 | )
30 | #'Compensatory Off'
31 | if leave_application :
32 | self.target_hours = 0
33 | self.expected_break_hours= 0
34 | self.actual_working_hours= 0
35 | self.total_target_seconds= 0
36 | self.total_break_seconds= 0
37 | self.total_work_seconds= 0
38 | self.status = "On Leave"
39 |
40 |
41 | def date_is_in_comp_off(self):
42 | leave_application_freizeit = frappe.db.exists(
43 | "Leave Application", {
44 | "employee": self.employee,
45 | "from_date": ("<=", self.log_date),
46 | "to_date": (">=", self.log_date),
47 | "leave_type": "Freizeitausgleich (Nicht buchen!)"
48 | }
49 | )
50 | leave_application_comp_off = frappe.db.exists(
51 | "Leave Application", {
52 | "employee": self.employee,
53 | "from_date": ("<=", self.log_date),
54 | "to_date": (">=", self.log_date),
55 | "leave_type": "Compensatory Off",
56 | 'docstatus': 1
57 | }
58 | )
59 | if leave_application_comp_off or leave_application_freizeit:
60 | self.hours_worked = 0.0
61 | self.actual_working_hours = -self.target_hours
62 | self.break_hours = 0.0
63 | self.total_break_seconds = 0.0
64 | self.total_work_seconds = flt(self.actual_working_hours * 60 * 60)
65 |
66 | def validate_duplicate_workday(self):
67 | workday = frappe.db.exists("Workday", {
68 | 'employee': self.employee,
69 | 'log_date': self.log_date
70 | })
71 |
72 | if workday and self.is_new():
73 | frappe.throw(
74 | _("Workday already exists for employee: {0}, on the given date: {1}")
75 | .format(self.employee, frappe.utils.formatdate(self.log_date))
76 | )
77 |
78 | # def set_manual_workday(self):
79 | # if self.manual_workday:
80 | # self.employee_checkins = []
81 | # self.total_work_seconds = self.hours_worked * 60 * 60
82 | # self.expected_break_hours = 0.0
83 |
84 |
85 | def bulk_process_workdays_background(data,flag):
86 | '''bulk workday processing'''
87 | frappe.logger("Creating Workday").error("bulk_process_workdays_background")
88 | frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True)
89 | frappe.enqueue(
90 | 'hr_addon.hr_addon.doctype.workday.workday.bulk_process_workdays',
91 | queue='long',
92 | data=data,
93 | flag=flag
94 | )
95 |
96 |
97 | @frappe.whitelist()
98 | def bulk_process_workdays(data,flag):
99 | import json
100 | if isinstance(data, str):
101 | data = json.loads(data)
102 | data = frappe._dict(data)
103 |
104 | if data.employee and frappe.get_value('Employee', data.employee, 'status') != "Active":
105 | frappe.throw(_("{0} is not active").format(frappe.get_desk_link('Employee', data.employee)))
106 |
107 | company = frappe.get_value('Employee', data.employee, 'company')
108 | if not data.unmarked_days:
109 | frappe.throw(_("Please select a date"))
110 | return
111 |
112 | missing_dates = []
113 |
114 | for date in data.unmarked_days:
115 | try:
116 | single = get_actual_employee_log_for_bulk_process(data.employee, get_datetime(date))
117 |
118 |
119 | # Check if the workday already exists
120 | existing_workday = frappe.get_value('Workday', {
121 | 'employee': data.employee,
122 | 'log_date': get_datetime(date)
123 | })
124 |
125 | if existing_workday:
126 | continue # Skip creating if it already exists
127 |
128 | doc_dict = {
129 | "doctype": 'Workday',
130 | "employee": data.employee,
131 | "log_date": get_datetime(date),
132 | "company": company,
133 | "attendance": single.get("attendance"),
134 | "hours_worked": single.get("hours_worked"),
135 | "break_hours": single.get("break_hours"),
136 | "target_hours": single.get("target_hours"),
137 | "total_work_seconds": single.get("total_work_seconds"),
138 | "expected_break_hours": single.get("expected_break_hours"),
139 | "total_break_seconds": single.get("total_break_seconds"),
140 | "total_target_seconds": single.get("total_target_seconds"),
141 | "actual_working_hours": single.get("actual_working_hours"),
142 | "manual_workday": single.get("manual_workday")
143 | }
144 |
145 | workday = frappe.get_doc(doc_dict)
146 |
147 | if (workday.status == 'Half Day'):
148 | workday.target_hours = workday.target_hours / 2
149 | elif (workday.status == 'On Leave'):
150 | workday.target_hours = 0
151 |
152 | employee_checkins = single.get("employee_checkins")
153 | if employee_checkins:
154 | workday.first_checkin = employee_checkins[0].time
155 | workday.last_checkout = employee_checkins[-1].time
156 |
157 | for employee_checkin in employee_checkins:
158 | workday.append("employee_checkins", {
159 | "employee_checkin": employee_checkin.get("name"),
160 | "log_type": employee_checkin.get("log_type"),
161 | "log_time": employee_checkin.get("time"),
162 | "skip_auto_attendance": employee_checkin.get("skip_auto_attendance"),
163 | })
164 |
165 | if len(employee_checkins) % 2 != 0:
166 | formatted_date = frappe.utils.formatdate(workday.log_date)
167 | #frappe.msgprint("CheckIns must be in pairs for the given date: " + formatted_date)
168 | if flag == "Create workday":
169 | frappe.logger("Creating Workday").error(flag)
170 | workday.insert()
171 | frappe.logger("Creating Workday").error(workday)
172 |
173 | missing_dates.append(get_datetime(date))
174 |
175 | except Exception:
176 | message = _("Something went wrong in Workday Creation: {0}".format(traceback.format_exc()))
177 | frappe.msgprint(message)
178 | frappe.log_error("bulk_process_workdays() error", message)
179 | formatted_missing_dates = []
180 | for missing_date in missing_dates:
181 | formatted_m_date = formatdate(missing_date,'dd.MM.yyyy')
182 | formatted_missing_dates.append(formatted_m_date)
183 |
184 | return {
185 | "message": 1,
186 | "missing_dates": formatted_missing_dates,
187 | "flag":flag
188 | }
189 |
190 | def get_month_map():
191 | return frappe._dict({
192 | "January": 1,
193 | "February": 2,
194 | "March": 3,
195 | "April": 4,
196 | "May": 5,
197 | "June": 6,
198 | "July": 7,
199 | "August": 8,
200 | "September": 9,
201 | "October": 10,
202 | "November": 11,
203 | "December": 12
204 | })
205 |
206 | @frappe.whitelist()
207 | def get_unmarked_days(employee, month, exclude_holidays=0):
208 | '''get_umarked_days(employee,month,excludee_holidays=0, year)'''
209 | import calendar
210 | month_map = get_month_map()
211 | today = get_datetime() #get year from year
212 |
213 |
214 | joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
215 | start_day = 1
216 | end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
217 |
218 | if joining_date and joining_date.month == month_map[month]:
219 | start_day = joining_date.day
220 |
221 | if relieving_date and relieving_date.month == month_map[month]:
222 | end_day = relieving_date.day + 1
223 |
224 | dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
225 | month_start, month_end = dates_of_month[0], dates_of_month[-1]
226 |
227 | """ ["docstatus", "!=", 2]"""
228 | rcords = frappe.get_list("Workday", fields=['log_date','employee'], filters=[
229 | ["log_date",">=",month_start],
230 | ["log_date","<=",month_end],
231 | ["employee","=",employee]
232 | ])
233 |
234 | marked_days = []
235 | if cint(exclude_holidays):
236 | if get_version() == 14:
237 | from hrms.hr.utils import get_holiday_dates_for_employee
238 |
239 | holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
240 | holidays = [get_datetime(rcord) for rcord in holiday_dates]
241 | marked_days.extend(holidays)
242 |
243 |
244 |
245 | unmarked_days = []
246 |
247 | for date in dates_of_month:
248 | date_time = get_datetime(date)
249 | if today.day <= date_time.day and today.month <= date_time.month:
250 | break
251 | if date_time not in marked_days:
252 | unmarked_days.append(date)
253 |
254 |
255 | return unmarked_days
256 |
257 |
258 | @frappe.whitelist()
259 | def get_unmarked_range(employee, from_day, to_day):
260 | '''get_umarked_days(employee,month,excludee_holidays=0, year)'''
261 | import calendar
262 | month_map = get_month_map()
263 | today = get_datetime() #get year from year
264 |
265 | joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
266 |
267 | start_day = from_day
268 | end_day = to_day #calendar.monthrange(today.year, month_map[month])[1] + 1
269 |
270 | if joining_date and joining_date >= getdate(from_day):
271 | start_day = joining_date
272 | if relieving_date and relieving_date >= getdate(to_day):
273 | end_day = relieving_date
274 |
275 | delta = date_diff(end_day, start_day)
276 | days_of_list = ['{}'.format(add_days(start_day,i)) for i in range(delta + 1)]
277 | month_start, month_end = days_of_list[0], days_of_list[-1]
278 |
279 | """ ["docstatus", "!=", 2]"""
280 | rcords = frappe.get_list("Workday", fields=['log_date','employee'], filters=[
281 | ["log_date",">=",month_start],
282 | ["log_date","<=",month_end],
283 | ["employee","=",employee]
284 | ])
285 |
286 | marked_days = [get_datetime(rcord.log_date) for rcord in rcords] #[]
287 | unmarked_days = []
288 |
289 | for date in days_of_list:
290 | date_time = get_datetime(date)
291 | # considering today date
292 | # if today.day <= date_time.day and today.month <= date_time.month and today.year <= date_time.year:
293 | # break
294 | if date_time not in marked_days:
295 | unmarked_days.append(date)
296 |
297 | return unmarked_days
298 |
299 |
300 | def get_version():
301 | branch_name = get_app_branch("erpnext")
302 | if "14" in branch_name:
303 | return 14
304 | else:
305 | return 13
306 |
307 | def get_app_branch(app):
308 | """Returns branch of an app"""
309 | import subprocess
310 |
311 | try:
312 | branch = subprocess.check_output(
313 | "cd ../apps/{0} && git rev-parse --abbrev-ref HEAD".format(app), shell=True
314 | )
315 | branch = branch.decode("utf-8")
316 | branch = branch.strip()
317 | return branch
318 | except Exception:
319 | return ""
320 |
321 |
322 | @frappe.whitelist()
323 | def get_created_workdays(employee, date_from, date_to):
324 | workday_list = frappe.get_list(
325 | "Workday",
326 | filters={
327 | "employee": employee,
328 | "log_date": ["between", [date_from, date_to]],
329 | },
330 | fields=["log_date","name"],
331 | order_by="log_date asc"
332 | )
333 |
334 | # Format the dates
335 | formatted_workdays = []
336 | for workday in workday_list:
337 | # Convert to date object
338 | date_obj = frappe.utils.getdate(workday['log_date'])
339 | # Format the date to 'd.m.yy'
340 | formatted_date = formatdate(date_obj, 'dd.MM.yyyy')
341 | formatted_workdays.append({
342 | 'log_date': formatted_date,
343 | 'name':workday['name']
344 | })
345 |
346 | return formatted_workdays
347 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/doctype/workday/workday_list.js:
--------------------------------------------------------------------------------
1 | frappe.listview_settings["Workday"] = {
2 | //add_fields: ["status", "attendance_date"],
3 | add_fields: ["status"],
4 | get_indicator: function (doc) {
5 | if (["Present", "Work From Home"].includes(doc.status)) {
6 | return [__(doc.status), "green", "status,=," + doc.status];
7 | } else if (["Absent", "On Leave"].includes(doc.status)) {
8 | return [__(doc.status), "red", "status,=," + doc.status];
9 | } else if (doc.status == "Half Day") {
10 | return [__(doc.status), "orange", "status,=," + doc.status];
11 | }
12 | },
13 |
14 | onload: function (list_view) {
15 | let me = this;
16 | const months = moment.months();
17 | list_view.page.add_inner_button(__("Process Workdays"), function () {
18 | let dialog = new frappe.ui.Dialog({
19 | title: __("Process Workdays"),
20 | fields: [
21 | {
22 | fieldname: "employee",
23 | label: __("For Employee"),
24 | fieldtype: "Link",
25 | options: "Employee",
26 | get_query: () => {
27 | return { query: "erpnext.controllers.queries.employee_query" };
28 | },
29 | reqd: 1,
30 | onchange: function () {
31 | dialog.set_df_property("unmarked_days", "hidden", 1);
32 | //dialog.set_df_property("status", "hidden", 1);
33 | dialog.set_df_property("exclude_holidays", "hidden", 1);
34 | //dialog.set_df_property("month", "value", '');
35 | dialog.set_df_property("date_from", "value", "");
36 | dialog.set_df_property("date_to", "value", "");
37 | dialog.set_df_property("unmarked_days", "options", []);
38 | dialog.no_unmarked_days_left = false;
39 | },
40 | },
41 | {
42 | fieldname: "date_from",
43 | label: __("Start Date"),
44 | fieldtype: "Date",
45 | reqd: 1,
46 | },
47 | {
48 | fieldname: "date_to",
49 | label: __("End Date"),
50 | fieldtype: "Date",
51 | reqd: 1,
52 | onchange: function () {
53 | let start_date = dialog.fields_dict.date_from.value;
54 | let end_date = dialog.fields_dict.date_to.value;
55 |
56 | // Validation: Ensure start date is less than end date
57 | if (!start_date || frappe.datetime.str_to_obj(start_date) > frappe.datetime.str_to_obj(end_date)) {
58 | frappe.msgprint({
59 | title: __('Validation Error'),
60 | message: __('Start Date should be less than End Date.'),
61 | indicator: 'red'
62 | });
63 | return; // Stop execution if validation fails
64 | }
65 |
66 | if (
67 | dialog.fields_dict.employee.value &&
68 | dialog.fields_dict.date_from.value
69 | ) {
70 | dialog.set_df_property("unmarked_days", "options", []);
71 | dialog.no_unmarked_days_left = false;
72 | me.get_day_range_options(
73 | dialog.fields_dict.employee.value,
74 | dialog.fields_dict.date_from.value,
75 | dialog.fields_dict.date_to.value
76 | ).then((options) => {
77 | if (options.length > 0) {
78 | //dialog.set_df_property("unmarked_days", "hidden", 0);
79 | dialog.set_df_property("unmarked_days", "hidden", 1);
80 | dialog.set_df_property("unmarked_days", "options", options);
81 | } else {
82 | dialog.no_unmarked_days_left = true;
83 | }
84 | });
85 | }
86 | },
87 | },
88 | {
89 | label: __("Toggle Days to process"),
90 | fieldtype: "Check",
91 | fieldname: "toggle_days",
92 | hidden: 0,
93 | onchange: function () {
94 | if (
95 | dialog.fields_dict.employee.value &&
96 | dialog.fields_dict.date_to.value
97 | ) {
98 | dialog.set_df_property(
99 | "unmarked_days",
100 | "hidden",
101 | !dialog.fields_dict.toggle_days.get_value()
102 | );
103 | dialog.set_df_property(
104 | "exclude_holidays",
105 | "hidden",
106 | !dialog.fields_dict.toggle_days.get_value()
107 | );
108 | }
109 | },
110 | },
111 | {
112 | label: __("Exclude Holidays"),
113 | fieldtype: "Check",
114 | fieldname: "exclude_holidays",
115 | hidden: 1,
116 | read_only: 1,
117 | onchange: function () {
118 | if (
119 | dialog.fields_dict.employee.value &&
120 | dialog.fields_dict.month.value
121 | ) {
122 | //dialog.set_df_property("status", "hidden", 0);
123 | dialog.set_df_property("unmarked_days", "options", []);
124 | dialog.no_unmarked_days_left = false;
125 | me.get_multi_select_options(
126 | dialog.fields_dict.employee.value,
127 | dialog.fields_dict.month.value,
128 | dialog.fields_dict.exclude_holidays.get_value()
129 | ).then((options) => {
130 | if (options.length > 0) {
131 | //dialog.set_df_property("unmarked_days", "hidden", 0);
132 | dialog.set_df_property("unmarked_days", "hidden", 1);
133 | dialog.set_df_property("unmarked_days", "options", options);
134 | } else {
135 | dialog.no_unmarked_days_left = true;
136 | }
137 | });
138 | }
139 | },
140 | },
141 | {
142 | label: __("Unprocessed Workdays for days"),
143 | fieldname: "unmarked_days",
144 | fieldtype: "MultiCheck",
145 | options: [],
146 | columns: 2,
147 | hidden: 1,
148 | },
149 | ],
150 | primary_action(data) {
151 | if (cur_dialog.no_unmarked_days_left) {
152 | frappe.call({
153 | method: "hr_addon.hr_addon.doctype.workday.workday.get_created_workdays", // Adjust to the correct path
154 | args: {
155 | employee: dialog.fields_dict.employee.value,
156 | date_from: dialog.fields_dict.date_from.value,
157 | date_to: dialog.fields_dict.date_to.value
158 | },
159 | callback: function(response) {
160 | if (response.message) {
161 | let workdays = response.message;
162 | console.log("Matched Workdays: ", workdays);
163 |
164 | // Extract the list of dates from the matched workdays
165 | let workday_dates = workdays.map(workday => workday.log_date);
166 |
167 | // Convert the list of dates into a comma-separated string
168 | let workday_dates_string = workday_dates.map(date => `• ${date.trim()}`).join('
');
169 | frappe.msgprint(
170 | __("Workday for the period: {0} - {1}, has already been processed for the Employee {2}.
For following dates workdays are available:
{3}",
171 | [
172 | frappe.datetime.str_to_user(dialog.fields_dict.date_to.value) ,
173 | frappe.datetime.str_to_user(dialog.fields_dict.date_from.value),
174 | dialog.fields_dict.employee.value,
175 | workday_dates_string // Insert the formatted list of workday dates
176 | ]));
177 | }
178 | }
179 | });
180 |
181 | } else {
182 | frappe.call({
183 | method: "hr_addon.hr_addon.doctype.workday.workday.bulk_process_workdays",
184 | args: {
185 | data: data,
186 | flag : "Do not create workday"
187 |
188 | },
189 | callback: function (response) {
190 | if (response.message) {
191 | let missingDates = response.message.missing_dates;
192 | console.log(response.message.flag)
193 | let missing_dates_string = missingDates.length > 0 ? missingDates.join(", ") : "None";
194 | frappe.confirm(
195 | __("Are you sure you want to process the workday for {0} from {1} to {2}?
For the following dates workdays will be created:
{3}", [
196 | data.employee,
197 | frappe.datetime.str_to_user(data.date_from),
198 | frappe.datetime.str_to_user(data.date_to),
199 | missing_dates_string.split(',').map(date => `• ${date.trim()}`).join('
')
200 | ]),
201 | function () {
202 | // If user clicks "Yes"
203 | flag = ""
204 | frappe.call({
205 | method: "hr_addon.hr_addon.doctype.workday.workday.bulk_process_workdays",
206 | args: {
207 | data: data,
208 | flag : "Create workday"
209 |
210 | },
211 | callback: function (r) {
212 | if (r.message === 1) {
213 | console.log(r.message.flag)
214 | frappe.show_alert({
215 | message: __("Workdays Processed"),
216 | indicator: "blue",
217 | });
218 | cur_dialog.hide();
219 | }
220 | },
221 | });
222 | },
223 | function () {
224 | console.log('no')
225 | // If user clicks "No"
226 | //frappe.msgprint('You clicked No!');
227 | // Cancel the action here or do nothing
228 | }
229 | );
230 |
231 |
232 | }
233 | }
234 | });
235 |
236 | }
237 | dialog.hide();
238 | list_view.refresh();
239 | },
240 | primary_action_label: __("Process Workdays"),
241 | });
242 | //dialog.$wrapper.find('.btn-modal-primary').css("color","red");
243 | dialog.$wrapper
244 | .find(".btn-modal-primary")
245 | .removeClass("btn-primary")
246 | .addClass("btn-dark");
247 | dialog.show();
248 | });
249 | list_view.page.change_inner_button_type("Process Workdays", null, "dark");
250 | },
251 | get_day_range_options: function (employee, from_day, to_day) {
252 | return new Promise((resolve) => {
253 | frappe
254 | .call({
255 | method:
256 | "hr_addon.hr_addon.doctype.workday.workday.get_unmarked_range",
257 | async: false,
258 | args: {
259 | employee: employee,
260 | from_day: from_day,
261 | to_day: to_day,
262 | },
263 | })
264 | .then((r) => {
265 | var options = [];
266 | for (var d in r.message) {
267 | var momentObj = moment(r.message[d], "YYYY-MM-DD");
268 | var date = momentObj.format("DD-MM-YYYY");
269 | options.push({
270 | label: date,
271 | value: r.message[d],
272 | checked: 1,
273 | });
274 | }
275 | resolve(options);
276 | });
277 | });
278 | },
279 | };
280 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/report/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/report/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/report/work_hour_report/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/hr_addon/report/work_hour_report/__init__.py
--------------------------------------------------------------------------------
/hr_addon/hr_addon/report/work_hour_report/work_hour_report.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2022, Jide Olayinka and contributors
2 | // For license information, please see license.txt
3 | /* eslint-disable */
4 |
5 | frappe.query_reports["Work Hour Report"] = {
6 | "filters": [
7 | {
8 | "fieldname":"date_from_filter",
9 | "label": __("From Date"),
10 | "fieldtype": "Date",
11 | "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
12 | "reqd": 1,
13 | "width": "35px"
14 | },
15 | {
16 | "fieldname":"date_to_filter",
17 | "label": __("To Date"),
18 | "fieldtype": "Date",
19 | "default": frappe.datetime.get_today(),
20 | "reqd": 1,
21 | "width": "35px"
22 | },
23 | {
24 | "fieldname":"employee_id",
25 | "label": __("Employee Id"),
26 | "fieldtype": "Link",
27 | "options": "Employee",
28 | "reqd": 1,
29 | "width": "35px"
30 | },
31 | ],
32 | "formatter": function (value, row, column, data, default_formatter) {
33 |
34 | value = default_formatter(value, row, column, data);
35 |
36 | if (column.fieldname == "name" ) {
37 | //if (!(row ===undefined)) console.log('yt: ',row.meta);
38 |
39 | if (!(row ===undefined)) {
40 | //console.log('yt: ',row.length);
41 | //console.log(row);
42 | if (row.meta.rowIndex===row.length){
43 | //console.log('yt: ',row.meta.rowIndex);
44 |
45 | }
46 | }
47 |
48 | }
49 | if (column.fieldname == "total_work_seconds" ) {
50 | if(value < 0) {
51 |
52 |
53 | value = "" +'-' + hitt(value ,true) + "";
54 |
55 |
56 | }
57 | else if(value > 0){
58 | value = "" + hitt(value,true) + "";
59 | }
60 | else{
61 | value = hitt(value);
62 | }
63 |
64 | }
65 |
66 | if (column.fieldname == "total_break_seconds" ) {
67 | if(value < 0) {
68 | value = "" + hitt(value) + "";
69 | }
70 | else if(value > 0){
71 | value = "" + hitt(value) + "";
72 | }
73 | else{
74 | value = hitt(value);
75 | }
76 |
77 | }
78 |
79 | if (column.fieldname == "actual_working_seconds" ) {
80 | if(value < 0) {
81 | value = "" +'-'+ hitt(value,true) + "";
82 | }
83 | else if(value > 0){
84 | value = "" + hitt(value,true) + "";
85 | }
86 | else{
87 | value = hitt(value);
88 | }
89 |
90 | }
91 |
92 | if (column.fieldname == "total_target_seconds" ) {
93 | value = hitt(value);
94 | }
95 | if (column.fieldname == "expected_break_hours" ) {
96 | value = hitt(value);
97 | }
98 |
99 | if (column.fieldname == "diff_log" ) {
100 | if(value < 0) {
101 | value = "" + hitt(value,true) + "";
102 |
103 | }
104 | else if(value > 0){
105 | value = "" + hitt(value,true) + "";
106 | }
107 | else{
108 | value = hitt(value,true);
109 | }
110 |
111 |
112 | }
113 |
114 | if (column.fieldname == "actual_diff_log" ) {
115 | if(value < 0) {
116 | // value = "" + hitt(value,true) + "";
117 | value = "" +"-"+ hitt(value,true) + "";
118 | }
119 | else if(value > 0){
120 | value = "" + hitt(value,true) + "";
121 | }
122 | else{
123 | value = hitt(value,true);
124 | }
125 |
126 |
127 | }
128 |
129 |
130 | return value;
131 | },
132 | };
133 | hitt = (fir, calDiff=false) => {
134 | // Handle negative values and calDiff case
135 | if (fir < 0 && !calDiff) {
136 | console.log(fir); // Log the negative value
137 | return fir; // Return the negative value directly
138 | }
139 |
140 | if (fir < 0 && calDiff) {
141 | fir = fir.toString().replace(/^-+/, ''); // Convert to positive for display
142 |
143 | }
144 |
145 | const d = Number(fir);
146 | if (d == 0) return "0"; // Return string "0" for zero
147 |
148 |
149 |
150 | var h = Math.floor(d / (60 * 60));
151 | var m = Math.floor(d % (60 * 60) / 60);
152 |
153 | var hDisplay = h > 0 ? h + "h " : "";
154 | var mDisplay = m > 0 ? m + "m " : "";
155 | const results = hDisplay + mDisplay;
156 |
157 | // If the original fir value was negative, prepend the negative sign
158 | return (fir < 0 ? "-" : "") + results;
159 | };
160 |
161 |
--------------------------------------------------------------------------------
/hr_addon/hr_addon/report/work_hour_report/work_hour_report.json:
--------------------------------------------------------------------------------
1 | {
2 | "add_total_row": 1,
3 | "columns": [],
4 | "creation": "2022-06-03 00:18:21.420312",
5 | "disable_prepared_report": 0,
6 | "disabled": 0,
7 | "docstatus": 0,
8 | "doctype": "Report",
9 | "filters": [],
10 | "idx": 0,
11 | "is_standard": "Yes",
12 | "letter_head": "Sunlight",
13 | "modified": "2024-12-05 22:27:41.292454",
14 | "modified_by": "Administrator",
15 | "module": "HR Addon",
16 | "name": "Work Hour Report",
17 | "owner": "Administrator",
18 | "prepared_report": 0,
19 | "ref_doctype": "Workday",
20 | "report_name": "Work Hour Report",
21 | "report_type": "Script Report",
22 | "roles": [
23 | {
24 | "role": "System Manager"
25 | },
26 | {
27 | "role": "HR User"
28 | },
29 | {
30 | "role": "HR Manager"
31 | },
32 | {
33 | "role": "Employee"
34 | }
35 | ]
36 | }
--------------------------------------------------------------------------------
/hr_addon/hr_addon/report/work_hour_report/work_hour_report.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2022, Jide Olayinka and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe
5 | from frappe import _
6 |
7 |
8 | def execute(filters=None):
9 | columns, data = [], []
10 | condition_date,condition_employee = "",""
11 | if filters.date_from_filter and filters.date_to_filter :
12 | if filters.date_from_filter == None:
13 | filters.date_from_filter = frappe.datetime.get_today()
14 | if filters.date_to_filter == None:
15 | filters.date_to_filter = frappe.datetime.get_today()
16 | condition_date = "AND log_date BETWEEN '"+ filters.date_from_filter + \
17 | "' AND '" + filters.date_to_filter + "'"
18 |
19 | if filters.get("employee_id"):
20 | empid = filters.get("employee_id")
21 | condition_employee += f" AND employee = '{empid}'"
22 | # #{'fieldname':'employee','label':'Employee','width':160},
23 | # {'fieldname':'target_hours','label':'Target Hours','width':80},
24 | columns = [
25 | {'fieldname':'log_date','label':'Date','width':110},
26 | {'fieldname':'name','label':'Work Day', "fieldtype": "Link", "options": "Workday", 'width':200,},
27 | {'fieldname':'status','label':'Status', "width": 80},
28 | {'fieldname':'total_work_seconds','label':_('Work Hours'), "width": 110, },
29 | # {'fieldname':'total_break_seconds','label':_('Break Hours'), "width": 110, },
30 | {'fieldname':'expected_break_hours','label':'Expected Break Hours','width':80},
31 | {'fieldname':'actual_working_seconds','label':_('Actual Working Hours'), "width": 110, },
32 | {'fieldname':'total_target_seconds','label':'Target Seconds','width':80},
33 | # {'fieldname':'diff_log','label':'Diff (Work Hours - Target Seconds)','width':90},
34 | {'fieldname':'actual_diff_log','label':'Diff (Actual Working Hours - Target Seconds)','width':110},
35 | {'fieldname':'first_in','label':'First Checkin','width':100},
36 | {'fieldname':'last_out','label':'Last Checkout','width':100},
37 | {'fieldname':'attendance','label':'Attendance','width': 160},
38 |
39 | ]
40 | work_data = frappe.db.sql(
41 | """
42 | SELECT
43 | name,
44 | hours_worked,
45 | log_date,
46 | employee,
47 | attendance,
48 | status,
49 | CASE
50 | WHEN total_work_seconds < 0 and total_work_seconds != -129600
51 | THEN 0
52 | ELSE total_work_seconds
53 | END AS total_work_seconds,
54 | total_break_seconds,
55 | actual_working_hours * 60 * 60 AS actual_working_seconds,
56 | expected_break_hours * 60 * 60 AS expected_break_hours,
57 | target_hours,
58 | total_target_seconds,
59 | (CASE
60 | WHEN total_work_seconds < 0
61 | THEN 0
62 | ELSE total_work_seconds
63 | END - total_target_seconds) AS diff_log,
64 | (CASE
65 | WHEN actual_working_hours < 0
66 | THEN actual_working_hours * 60 * 60
67 | ELSE (actual_working_hours * 60 * 60 - total_target_seconds)
68 | END) AS actual_diff_log,
69 | TIME(first_checkin) AS first_in,
70 | TIME(last_checkout) AS last_out
71 | FROM `tabWorkday`
72 | WHERE docstatus < 2 %s %s
73 | ORDER BY log_date ASC
74 | """ % (condition_date, condition_employee),
75 | as_dict=1,
76 | )
77 |
78 |
79 | data = work_data
80 |
81 | return columns, data
82 | #(actual_working_hours * 60 * 60 - total_target_seconds) AS actual_diff_log,64to68
--------------------------------------------------------------------------------
/hr_addon/modules.txt:
--------------------------------------------------------------------------------
1 | HR Addon
--------------------------------------------------------------------------------
/hr_addon/patches.txt:
--------------------------------------------------------------------------------
1 | hr_addon.patches.v15_0.add_custom_field_for_employee
--------------------------------------------------------------------------------
/hr_addon/patches/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/hr_addon/patches/v15_0/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | __version__ = '0.0.1'
3 |
4 |
--------------------------------------------------------------------------------
/hr_addon/patches/v15_0/add_custom_field_for_employee.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_field
3 | from frappe.custom.doctype.property_setter.property_setter import make_property_setter
4 |
5 | def execute():
6 |
7 | frappe.reload_doc("Setup", "doctype", "employee")
8 |
9 | create_custom_field("Employee",
10 | dict(fieldname="permanent", label="Permanent",
11 | fieldtype="Check", insert_after="employment_type",
12 | )
13 | )
14 |
--------------------------------------------------------------------------------
/hr_addon/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/public/.gitkeep
--------------------------------------------------------------------------------
/hr_addon/public/js/hr_settings.js:
--------------------------------------------------------------------------------
1 | frappe.ui.form.on("HR Settings", {
2 | refresh: (frm) => {
3 | if(!frm.is_new()) {
4 | frm.dashboard.set_headline_alert(`
5 | There's some additional configuration for 'Work Anniversaries' which you'll find in HR Addon Settings
6 | `);
7 | }
8 |
9 | frm.set_df_property("send_work_anniversary_reminders", "description", "There's some additional configuration for this notification in 'HR Addon Settings' please check.");
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/hr_addon/public/js/list_view.js:
--------------------------------------------------------------------------------
1 | frappe.provide("hr_addon.frappe.views");
2 | frappe.listview_settings["Weekly Working Hours"] = {
3 | onload: function (list_view) {
4 |
5 | list_view.page.add_button(__("Update Year"), function () {
6 | frappe.call({
7 | method: "hr_addon.custom_scripts.custom_python.weekly_working_hours.set_from_to_dates",
8 | args: {},
9 | callback(r) {}
10 | });
11 | window.location.reload();
12 | });
13 |
14 | },
15 | };
16 |
17 |
18 |
--------------------------------------------------------------------------------
/hr_addon/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/templates/__init__.py
--------------------------------------------------------------------------------
/hr_addon/templates/pages/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phamos-eu/HR-Addon/4db0ef3dcecf87f2d862f88b634cf571b82ba646/hr_addon/templates/pages/__init__.py
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | License: MIT
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # frappe -- https://github.com/frappe/frappe is installed via 'bench init'
2 | #paytmchecksum~=1.7.0
3 | #hrms
4 | icalendar
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | with open("requirements.txt") as f:
4 | install_requires = f.read().strip().split("\n")
5 |
6 | # get version from __version__ variable in hr_addon/__init__.py
7 | from hr_addon import __version__ as version
8 |
9 | setup(
10 | name="hr_addon",
11 | version=version,
12 | description="Addon for Erpnext attendance and employee checkins",
13 | author="Jide Olayinka",
14 | author_email="olajamesjide@gmail.com",
15 | packages=find_packages(),
16 | zip_safe=False,
17 | include_package_data=True,
18 | install_requires=install_requires
19 | )
20 |
--------------------------------------------------------------------------------