Merge pull request #13951 from ESS-LLP/jam_enterprise_sprint

Attendance Request, Leave Period, Compensatory Leave Request, Leave Policy
This commit is contained in:
Saurabh 2018-05-14 20:57:55 +05:30 committed by GitHub
commit 8b04d431c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 830 additions and 73 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -5,7 +5,7 @@ of co-workers. Most important feature here is processing the payroll by using
Payroll Entry to generate Salary Slips. Most countries have complex tax
rules stating which expenses the company can make on behalf of the Employees.
There are a set of rules for the company to deduct taxes and social security
from employee payroll. ERPNext allows to accomodate all types of taxes and
from employee payroll. ERPNext allows to accommodate all types of taxes and
their calculation.
It also maintains a complete employee database including contact information,

View File

@ -5,13 +5,13 @@ to be able to qualify as paid leaveas, you can create Leave Application to
track approval and usage of leaves. You have to mention the Employee, Leave
Type and the period for which the leave is taken.
> Human Resources > Leave Application > New Leave Application
> Human Resources > Leaves and Holiday > Leave Application > New Leave Application
<img class="screenshot" alt="Leave Application" src="{{docs_base_url}}/assets/img/human-resources/leave-application.png">
###Setting Leave Approver
* A leave approver is a user who can approve an leave application for an employee.
* A leave approver is a user who can approve an leave application for an employee.
* You need to mention a list of Leave Approvers against an Employee in the Employee Master.
@ -22,11 +22,11 @@ their “Employee ID” as a match rule in the Leave Application Permission
settings. See the earlier discussion on [Setting Up Permissions](/docs/user/manual/en/setting-up/users-and-permissions/user-permissions.html)
for more info.
You assign Leaves aginast an Employee check [Leave Allocation](/docs/user/manual/en/human-resources/leave.html)
To understand how ERPNext allows you configure leaves for employees, check [Leaves - Overview](/docs/user/manual/en/human-resources/leave.html)
<div class="embed-container">
<iframe src="https://www.youtube.com/embed/fc0p_AXebc8?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>
</iframe>
</div>
{next}
{next}

View File

@ -1,29 +1,48 @@
#Overview
This section enables you to manage leave schedule of your organization. It also explains the way employees can apply for leaves.
Employees create leave request and manager (leave approver) approves or rejects the request. You can select from a number of leave types such as sick leave, casual leave, privilege leave and so on. You can also allocate leaves to your employees and generate reports to track leaves record.
#Leaves - Overview
This section will help you understand how ERPNext enables you to effectively manage the leave schedule of your organization. It also explains the way employees can apply for leaves.
Employees create leave requests, which their respective managers (leave approver) can approve or reject. An Employee can select from a number of leave types such as sick leave, casual leave, privilege leave and so on. The number and type of leaves an Employee can apply is controlled by Leave Allocations. You can create Leave Allocations for a Leave Period based on the company's Leave Policy. You can also allocate additional leaves to your employees and generate reports to track leaves taken by Employees.
---
#Leave Type
> Human Resources > Leaves and Holiday > Leave Type > New Leave Type
> Human Resources > Setup > Leave Type > New Leave Type
Leave Type refers to types of leave allotted to an employee by a company. An employee can select a particular Leave Type while requesting for a leave. You can create any number of Leave Types based on your companys
Leave Type refers to types of leave allotted to an employee by a company. An employee can select a particular Leave Type while requesting for a leave. You can create any number of Leave Types based on your companys
requirement.
<img class="screenshot" alt="New Leave Type"
<img class="screenshot" alt="New Leave Type"
src="{{docs_base_url}}/assets/img/human-resources/new-leave-type.png">
**Max Days Leave Allowed:** It refers to maximum number of days this particular Leave Type can be availed at a stretch. If an employee exceeds the maximum number of days under a particular Leave Type, his/her extended leave may be considered as Leave Without Pay and this may affect his/her salary calculation.
**Max Leaves Allowed:** This field allows you to set the maximum number of leaves of this Leave Type that Employees can apply within a Leave Period.
**Applicable After (Working Days):** Employees who have worked with the company for this number of days are only allowed to apply for this Leave Type. Do note that any other leaves availed by the Employee after her joining date is also considered while calculating working days.
**Maximum Continuous Days Applicable:** It refers to maximum number of days this particular Leave Type can be availed at a stretch. If an employee exceeds the maximum number of days under a particular Leave Type, his/her extended leave may be considered as Leave Without Pay and this may affect his/her salary calculation.
**Is Carry Forward:** If checked, the balance leave will be carried forwarded to the next allocation period.
**Is Leave Without Pay:** This ensures that the Leave Type will be treated as leaves whithout pay and salary will get deducted for this Leave Type.
**Is Leave Without Pay:** This ensures that the Leave Type will be treated as leaves without pay and salary will get deducted for this Leave Type.
**Allow Nagative Balance:** If checked, system will always allow to approve leave application for the Leave Type, even if there is no leave balance.
**Allow Negative Balance:** If checked, system will always allow to approve leave application for the Leave Type, even if there is no leave balance.
**Include holidays within leaves as leaves:** Check this option if you wish to count holidays within leaves as a leave. Such holidays will be deducted from the total number of leaves.
**Is Compensatory:** Compensatory leaves are leaves granted for working overtime or on holidays, normally compensated as an encashable leave. You can check this option to mark the Leave Type as compensatory. An Employee can request for compensatory leaves (Compensatory Leave Request) and on approval of such requests, Leave Allocations are created allowing her to apply for leaves of this type later on.
**Is Optional:** Check this Optional Leaves are holidays which Employees can choose to avail from a list of holidays published by the company. The Holiday List for optional leaves can have any number of holidays but you can restrict the number of such leaves granted to an Employee in a Leave Period by setting the Max Days Leave Allowed field.
**Encashment:** It is possible that Employees can receive cash from their Employer for unused leaves granted to them in a Leave Period. Not all Leave Types need to be encashable, so you should set "Allow Encashment" for Leave Types which are encashable. Leave encashment is allowed only in the last month of the Leave Period.
<img class="screenshot" alt="Leave Encashment"
src="{{docs_base_url}}/assets/img/human-resources/leave-encashment.png">
You can set the **Encashment Threshold Days** field so that the Employees wont be able to encash that many days. These days should be carry forwarded to the next Leave Period so that it can be either encashed or availed. You may also want to set the **Earning Component** for use in Salary Slip while paying out the encashed amount to Employees as part of their Salary.
**Earned Leave:** Earned Leaves are leaves earned by an employee after working with the company for a certain amount of time. Checking "Is Earned Leave" will allot leaves pro rata by automatically updating Leave Allocation for leaves of this type at intervals set by **Earned Leave Frequency**. For example, if an employee earns 2 leaves of type Paid Leaves monthly, ERPNext automatically increments the Leave Allocation for Paid Leave at the end of every month by 2. The leave allotment process (background job) will only allot leaves considering the max leaves for the leave type, and will round to **Rounding** for fractions.
<img class="screenshot" alt="Earned Leave"
src="{{docs_base_url}}/assets/img/human-resources/earned-leave.png">
###Default Leave Types
There are some pre-loaded Leave Types in the system, as below:
@ -35,30 +54,60 @@ There are some pre-loaded Leave Types in the system, as below:
---
#Leave Allocation
#Leave Policy
> Human Resources > Leaves and Holiday > Leave Policy > New Leave Policy
Leave Allocation enables you to allot a specific number of leaves to a particular employee. You can allocate a number of leaves to different types of leave. You also have the option to allocate leaves to your employees manually or via the Leave Allocation Tool.
It is a practice for many enterprises to enforce a general Leave Policy to effectively track and manage Employee leaves. ERPNext allows you to create and manage multiple Leave Policies and allocate leaves to Employees as defined by the policy.
###Manual Allocation
> Human Resources > Setup > Leave Allocation > New Leave Allocation
<img class="screenshot" alt="Leave Policy"
src="{{docs_base_url}}/assets/img/human-resources/leave-policy.png">
To allocate leaves to an Employee, select the period and the number of leaves you want to allocate. You can also add unused leaves from previous allocation period.
### Enforcing the Leave Policy
To enforce the Leave Policy, you can either:
* Apply the Leave Policy in Employee Grade
<img class="screenshot" alt="Employee Grade"
src="{{docs_base_url}}/assets/img/human-resources/employee-grade.png">
<img class="screenshot" alt="Manual Leave Allocation"
This will ensure all leave allocations for all employees of this grade will be as per the Leave Policy
* Update Employee record with appropriate Leave Policy. In case you need to selectively update the Leave Policy for a particular Employee, you can do so by updating the Employee record.
<img class="screenshot" alt="Employee Leave Policy"
src="{{docs_base_url}}/assets/img/human-resources/employee-leave-policy.png">
#Leave Period
Most companies manage leaves based on a Leave Period. ERPNext allows you to create a Leave period by going to
> Human Resources > Leaves and Holiday > Leave Period > New Leave Period
<img class="screenshot" alt="Leave Period"
src="{{docs_base_url}}/assets/img/human-resources/leave-period.png">
#Granting Leaves to Employees
Leave Management in ERPNext is based on Leave Allocations created for each employee. This means, Employees can only avail as many leaves (of each Leave Type) allocated to them. There are multiple ways by which you can create Leave Allocations for Employees.
###Leave Allocation
Leave Allocation enables you to allot a specific number of leaves to a particular employee. You can allocate a number of leaves to different types of leave.
###Allocating leaves for a Leave Period
> Human Resources > Leaves and Holiday > Leave Period
Leave Period helps you manage leaves for a period and also doubles up as a tool to help you grant leaves for a category of employees. The **Grant** button will generate Leave Allocations based on the Leave Policy applicable to each Employee. You can allocate leaves based on Employee Grade, Department or Designation. Also, note that **Carry Forward Leaves** check will enable you to carry forward any unused leaves (for Leave Types with Is Carry Forward turned on) from previous allocations to new ones.
<img class="screenshot" alt="Grant Leaves from Leave Period"
src="{{docs_base_url}}/assets/img/human-resources/leave-period-grant.png">
###Manual Allocation of leaves
> Human Resources > Leaves and Holiday > Leave Allocation > New Leave Allocation
To manually allocate leaves for an Employee, select the period and the number of leaves you want to allocate. You can also add unused leaves from previous allocation period.
<img class="screenshot" alt="Manual Leave Allocation"
src="{{docs_base_url}}/assets/img/human-resources/manual-leave-allocation.png">
###Via Leave Allocation Tool
> Human Resources > Tools > Leave Allocation Tool
This tool enables you to allocate leaves for a category of employees, instead of individual ones. You can allocate leaves based on Employee Type, Branch, Department and Designation. Leave Allocation Tool is also known as Leave Control Panel.
<img class="screenshot" alt="Leave Allocation Tool"
src="{{docs_base_url}}/assets/img/human-resources/leave-allocation-tool.png">
---
#Leave Application
> Human Resources > Documents > Leave Application > New Leave Application
> Human Resources > Leaves and Holiday > Leave Application > New Leave Application
Leave Application section enables an employee to apply for leaves. Employee can select the type of leave and the Leave Approver who will authorize the Leave Application. User with "Leave Approver" role are considered as Leave approver. Leave Approvers can also be restricted/pre-defined in the Employee record. Based on selected dates and applicable Holiday List, total leave days is calculated automatically.
@ -73,7 +122,7 @@ Leave Application section enables an employee to apply for leaves. Employee can
<img class="screenshot" alt="Leave Allocation Tool"
src="{{docs_base_url}}/assets/img/human-resources/new-leave-application.png">
**Notes:**
- Leave Application period must be within a single Leave Allocation period. In case, you are applying for leave across leave allocation period, you have to create two Leave Application records.
@ -84,9 +133,9 @@ Leave Application section enables an employee to apply for leaves. Employee can
#Leave Block List
> Human Resources > Setup > Leave Block List > New Leave Block List
> Human Resources > Leaves and Holiday > Leave Block List > New Leave Block List
Leave Block List is a list of dates in a year, on which employees can not apply for leave. You can define a list of users who can approve Leave Application on blocked days, in case of urgency. You can also define whether the list will applied on entire company or any specific departments.
<img class="screenshot" alt="Leave Allocation Tool"
src="{{docs_base_url}}/assets/img/human-resources/leave-block-list.png">
src="{{docs_base_url}}/assets/img/human-resources/leave-block-list.png">

View File

@ -295,6 +295,39 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attendance_request",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Attendance Request",
"length": 0,
"no_copy": 0,
"options": "Attendance Request",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -339,7 +372,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-06-13 14:29:11.771376",
"modified": "2018-05-07 16:56:32.314683",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance",

View File

@ -1,8 +1,14 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
cur_frm.add_fetch('employee', 'company', 'company');
frappe.ui.form.on('Attendance Request', {
refresh: function(frm) {
half_day: function(frm) {
if(frm.doc.half_day == 1){
frm.set_df_property('half_day_date', 'reqd', true);
}
else{
frm.set_df_property('half_day_date', 'reqd', false);
}
}
});

View File

@ -145,19 +145,18 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reason",
"fieldtype": "Select",
"fieldname": "half_day",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reason",
"label": "Half Day",
"length": 0,
"no_copy": 0,
"options": "Work From Home\nOn Duty",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -165,7 +164,39 @@
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "half_day",
"fieldname": "half_day_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Half Day Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
@ -232,6 +263,70 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reason",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reason",
"length": 0,
"no_copy": 0,
"options": "Work From Home\nOn Duty",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 1,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -274,7 +369,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-14 15:38:14.344570",
"modified": "2018-05-14 18:18:56.936880",
"modified_by": "Administrator",
"module": "HR",
"name": "Attendance Request",

View File

@ -4,7 +4,58 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.hr.utils import validate_dates
class AttendanceRequest(Document):
pass
def validate(self):
validate_dates(self, self.from_date, self.to_date)
if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):
frappe.throw(_("Half day date should be in between from date and to date"))
def on_submit(self):
self.create_attendance()
def on_cancel(self):
attendance_list = frappe.get_list("Attendance", {'employee': self.employee, 'attendance_request': self.name})
if attendance_list:
for attendance in attendance_list:
attendance_obj = frappe.get_doc("Attendance", attendance['name'])
attendance_obj.cancel()
def create_attendance(self):
request_days = date_diff(self.to_date, self.from_date) + 1
for number in range(request_days):
attendance_date = add_days(self.from_date, number)
skip_attendance = self.validate_if_attendance_not_applicable(attendance_date)
if not skip_attendance:
attendance = frappe.new_doc("Attendance")
attendance.employee = self.employee
attendance.employee_name = self.employee_name
if self.half_day and date_diff(getdate(self.half_day_date), getdate(attendance_date)) == 0:
attendance.status = "Half Day"
else:
attendance.status = "Present"
attendance.attendance_date = attendance_date
attendance.company = self.company
attendance.attendance_request = self.name
attendance.save(ignore_permissions=True)
attendance.submit()
def validate_if_attendance_not_applicable(self, attendance_date):
# Check if attendance_date is a Holiday
if is_holiday(self.employee, attendance_date):
return True
# Check if employee on Leave
leave_record = frappe.db.sql("""select half_day from `tabLeave Application`
where employee = %s and %s between from_date and to_date
and docstatus = 1""", (self.employee, attendance_date), as_dict=True)
if leave_record:
return True
return False

View File

@ -3,6 +3,12 @@
frappe.ui.form.on('Compensatory Leave Request', {
refresh: function(frm) {
frm.set_query("leave_type", function() {
return {
filters: {
"is_compensatory": true
}
};
});
}
});

View File

@ -4,9 +4,68 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import date_diff, add_days
from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period
class CompensatoryLeaveRequest(Document):
def validate_present(self):
pass
def validate(self):
validate_dates(self, self.work_from_date, self.work_end_date)
validate_overlap(self, self.work_from_date, self.work_end_date)
def on_submit(self):
if not self.leave_type:
frappe.throw(_("Please select a leave type to submit the request"))
else:
company = frappe.db.get_value("Employee", self.employee, "company")
date_difference = date_diff(self.work_end_date, self.work_from_date) + 1
leave_period = get_leave_period(self.work_from_date, self.work_end_date, company)
if leave_period:
leave_allocation = self.exists_allocation_for_period(leave_period)
if leave_allocation:
leave_allocation.new_leaves_allocated += date_difference
leave_allocation.submit()
else:
self.create_leave_allocation(leave_period, date_difference)
else:
frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date))
def exists_allocation_for_period(self, leave_period):
leave_allocation = frappe.db.sql("""
select name
from `tabLeave Allocation`
where employee=%(employee)s and leave_type=%(leave_type)s
and docstatus=1
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
""", {
"from_date": leave_period[0].from_date,
"to_date": leave_period[0].to_date,
"employee": self.employee,
"leave_type": self.leave_type
}, as_dict=1)
if leave_allocation:
return frappe.get_doc("Leave Allocation", leave_allocation[0].name)
else:
return False
def create_leave_allocation(self, leave_period, date_difference):
is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward")
allocation = frappe.new_doc("Leave Allocation")
allocation.employee = self.employee
allocation.employee_name = self.employee_name
allocation.leave_type = self.leave_type
allocation.from_date = add_days(self.work_end_date, 1)
allocation.to_date = leave_period[0].to_date
allocation.new_leaves_allocated = date_difference
allocation.total_leaves_allocated = date_difference
allocation.compensatory_request = self.name
allocation.description = self.reason
if is_carry_forward == 1:
allocation.carry_forward = True
allocation.save(ignore_permissions = True)
allocation.submit()

View File

@ -432,6 +432,71 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "compensatory_request",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Compensatory Leave Request",
"length": 0,
"no_copy": 0,
"options": "Compensatory Leave Request",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "leave_period",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Leave Period",
"length": 0,
"no_copy": 0,
"options": "Leave Period",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -478,7 +543,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-11-10 18:41:38.845159",
"modified": "2018-04-19 19:34:59.248428",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",

View File

@ -6,7 +6,7 @@ import frappe
from frappe.utils import flt, date_diff, formatdate
from frappe import _
from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name
from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass
@ -25,6 +25,20 @@ class LeaveAllocation(Document):
self.validate_total_leaves_allocated()
self.validate_lwp()
set_employee_name(self)
self.validate_leave_allocation_days()
def validate_leave_allocation_days(self):
company = frappe.db.get_value("Employee", self.employee, "company")
leave_period = get_leave_period(self.from_date, self.to_date, company)
max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")
if max_leaves_allowed > 0:
leave_allocated = 0
if leave_period:
leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date)
leave_allocated += self.new_leaves_allocated
if leave_allocated > max_leaves_allowed:
frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")\
.format(self.leave_type, self.employee))
def on_update_after_submit(self):
self.validate_new_leaves_allocated_value()
@ -97,6 +111,29 @@ class LeaveAllocation(Document):
else:
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
def get_leave_allocation_for_period(employee, leave_type, from_date, to_date):
leave_allocated = 0
leave_allocations = frappe.db.sql("""
select employee, leave_type, from_date, to_date, total_leaves_allocated
from `tabLeave Allocation`
where employee=%(employee)s and leave_type=%(leave_type)s
and docstatus=1
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or (from_date < %(from_date)s and to_date > %(to_date)s))
""", {
"from_date": from_date,
"to_date": to_date,
"employee": employee,
"leave_type": leave_type
}, as_dict=1)
if leave_allocations:
for leave_alloc in leave_allocations:
leave_allocated += leave_alloc.total_leaves_allocated
return leave_allocated
@frappe.whitelist()
def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None):
carry_forwarded_leaves = 0

View File

@ -35,6 +35,7 @@ class LeaveApplication(Document):
self.validate_attendance()
if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'):
self.validate_optional_leave()
self.validate_applicable_after()
def on_update(self):
if self.status == "Open" and self.docstatus < 1:
@ -56,6 +57,21 @@ class LeaveApplication(Document):
# notify leave applier about cancellation
self.notify_employee()
def validate_applicable_after(self):
if self.leave_type:
leave_type = frappe.get_doc("Leave Type", self.leave_type)
if leave_type.applicable_after > 0:
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
leave_days = get_approved_leaves_for_period(self.employee, False, date_of_joining, self.from_date)
number_of_days = date_diff(getdate(self.from_date), date_of_joining)
if number_of_days >= 0:
holidays = 0
if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
holidays = get_holidays(self.employee, date_of_joining, self.from_date)
number_of_days = number_of_days - leave_days - holidays
if number_of_days < leave_type.applicable_after:
frappe.throw(_("{0} applicable after {1} working days").format(self.leave_type, leave_type.applicable_after))
def validate_dates(self):
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):
frappe.throw(_("To date cannot be before from date"))

View File

@ -7,7 +7,7 @@ import unittest
from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on
from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, nowdate
from frappe.utils import add_days, nowdate, now_datetime
test_dependencies = ["Leave Allocation", "Leave Block List"]
@ -275,16 +275,119 @@ class TestLeaveApplication(unittest.TestCase):
self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, today), 9)
def test_leaves_allowed(self):
# TODO: test cannot allocate more than max leaves
pass
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name = 'Test Leave Type',
doctype = 'Leave Type',
max_leaves_allowed = 5
)).insert()
date = add_days(nowdate(), -7)
allocate_leaves(employee, leave_period, leave_type.name, 5)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = date,
to_date = add_days(date, 2),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertTrue(leave_application.insert())
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = add_days(date, 4),
to_date = add_days(date, 7),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
def test_applicable_after(self):
# TODO: test not applicable until applicable working days
pass
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name = 'Test Leave Type',
doctype = 'Leave Type',
applicable_after = 15
)).insert()
date = add_days(nowdate(), -7)
allocate_leaves(employee, leave_period, leave_type.name, 10)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type 1", force=1)
leave_type_1 = frappe.get_doc(dict(
leave_type_name = 'Test Leave Type 1',
doctype = 'Leave Type'
)).insert()
allocate_leaves(employee, leave_period, leave_type_1.name, 10)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type_1.name,
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertTrue(leave_application.insert())
def test_max_continuous_leaves(self):
# TODO: test cannot take continuous leaves more than
pass
employee = get_employee()
leave_period = get_leave_period()
frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name = 'Test Leave Type',
doctype = 'Leave Type',
max_leaves_allowed = 15,
max_days_allowed = 3
)).insert()
date = add_days(nowdate(), -7)
allocate_leaves(employee, leave_period, leave_type.name, 10)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
employee = employee.name,
leave_type = leave_type.name,
from_date = date,
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
def test_earned_leave(self):
leave_period = get_leave_period()
@ -320,3 +423,36 @@ def make_allocation_record(employee=None, leave_type=None):
allocation.insert(ignore_permissions=True)
allocation.submit()
def get_employee():
return frappe.get_doc("Employee", "_T-Employee-00001")
def get_leave_period():
leave_period_name = frappe.db.exists({
"doctype": "Leave Period",
"name": "Test Leave Period"
})
if leave_period_name:
return frappe.get_doc("Leave Period", leave_period_name[0][0])
else:
return frappe.get_doc(dict(
name = 'Test Leave Period',
doctype = 'Leave Period',
from_date = "{0}-01-01".format(now_datetime().year),
to_date = "{0}-12-31".format(now_datetime().year),
company = "_Test Company",
is_active = 1
)).insert()
def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, eligible_leaves=0):
frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
"employee_name": employee.employee_name,
"leave_type": leave_type,
"from_date": leave_period.from_date,
"to_date": leave_period.to_date,
"new_leaves_allocated": new_leaves_allocated,
"docstatus": 1
}).insert()

View File

@ -2,13 +2,35 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Period', {
onload: function(frm) {
refresh: (frm)=>{
frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0);
},
from_date: (frm)=>{
if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1));
}
},
grant: (frm)=>{
frappe.call({
doc: frm.doc,
method: "grant_leave_allocation",
callback: function(r) {
if(!r.exc){
frm.reload_doc();
}
},
freeze: true,
freeze_message: __("Grant allocations......")
})
},
onload: (frm) => {
frm.set_query("department", function() {
return {
"filters": {
"company": frm.doc.company,
}
};
});
}
})
}
});

View File

@ -478,7 +478,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-04 18:25:06.719932",
"modified": "2018-05-07 18:25:06.719932",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",

View File

@ -4,7 +4,78 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import getdate, cstr
from frappe.model.document import Document
from erpnext.hr.utils import validate_overlap, get_employee_leave_policy
class LeavePeriod(Document):
pass
def get_employees(self):
conditions, values = [], []
for field in ["grade", "designation", "department"]:
if self.get(field):
conditions.append("{0}=%s".format(field))
values.append(self.get(field))
condition_str = " and " + " and ".join(conditions) if len(conditions) else ""
e = frappe.db.sql("select name from tabEmployee where status='Active' {condition}"
.format(condition=condition_str), tuple(values))
return e
def validate(self):
self.validate_dates()
validate_overlap(self, self.from_date, self.to_date, self.company)
def grant_leave_allocation(self):
if self.employee:
self.grant_leave_alloc(self.employee)
else:
self.grant_leave_alloc_for_employees()
def grant_leave_alloc_for_employees(self):
employees = self.get_employees()
if employees:
for employee in employees:
self.grant_leave_alloc(cstr(employee[0]))
else:
frappe.msgprint(_("No employee found"))
def grant_leave_alloc(self, employee):
self.validate_allocation_exists(employee)
leave_policy = get_employee_leave_policy(employee)
if leave_policy:
for leave_policy_detail in leave_policy.leave_policy_details:
if not frappe.db.get_value("Leave Type", leave_policy_detail.leave_type, "is_lwp"):
self.create_leave_allocation(employee, leave_policy_detail.leave_type, leave_policy_detail.annual_allocation)
def validate_allocation_exists(self, employee):
leave_alloc = frappe.db.exists({
"doctype": "Leave Allocation",
"employee": employee,
"leave_period": self.name,
"docstatus": 1})
if leave_alloc:
frappe.throw(_("Employee {0} already have Leave Allocation {1} for this period").format(employee, leave_alloc[0][0])\
+ """ <b><a href="#Form/Leave Allocation/{0}">{0}</a></b>""".format(leave_alloc[0][0]))
def validate_dates(self):
if getdate(self.from_date) >= getdate(self.to_date):
frappe.throw(_("To date can not be equal or less than from date"))
def create_leave_allocation(self, employee, leave_type, new_leaves_allocated):
allocation = frappe.new_doc("Leave Allocation")
allocation.employee = employee
allocation.employee_name = frappe.db.get_value("Employee", employee, "employee_name")
allocation.leave_type = leave_type
allocation.from_date = self.from_date
allocation.to_date = self.to_date
allocation.new_leaves_allocated = new_leaves_allocated
allocation.leave_period = self.name
if self.carry_forward_leaves:
if frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
allocation.carry_forward = self.carry_forward_leaves
allocation.save(ignore_permissions = True)
allocation.submit()

View File

@ -2,7 +2,30 @@
// For license information, please see license.txt
frappe.ui.form.on('Leave Policy', {
refresh: function(frm) {
});
frappe.ui.form.on('Leave Policy Detail',{
leave_type: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if(child.leave_type){
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Leave Type",
fieldname: "max_leaves_allowed",
filters: { name: child.leave_type }
},
callback: function(r) {
if (r.message) {
child.annual_allocation = r.message.max_leaves_allowed;
refresh_field("leave_policy_details");
}
}
});
}
else{
child.annual_allocation = "";
refresh_field("leave_policy_details");
}
}
});

View File

@ -4,7 +4,13 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class LeavePolicy(Document):
pass
def validate(self):
if self.leave_policy_details:
for lp_detail in self.leave_policy_details:
max_leaves_allowed = frappe.db.get_value("Leave Type", lp_detail.leave_type, "max_leaves_allowed")
if max_leaves_allowed > 0 and lp_detail.annual_allocation > max_leaves_allowed:
frappe.throw(_("Maximum leave allowed in the leave type {0} is {1}").format(lp_detail.leave_type, max_leaves_allowed))

View File

@ -41,6 +41,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -72,6 +73,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -102,6 +104,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -133,6 +136,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -162,6 +166,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -193,6 +198,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -222,6 +228,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -252,6 +259,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -281,6 +289,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -310,6 +319,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -318,7 +328,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_parental_leave",
"fieldname": "is_compensatory",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
@ -327,7 +337,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Parental Leave",
"label": "Is Compensatory",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -340,6 +350,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -370,6 +381,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -401,6 +413,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -431,6 +444,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -462,6 +476,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -494,6 +509,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -524,6 +540,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -554,6 +571,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -586,6 +604,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -618,6 +637,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
@ -632,7 +652,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-03 19:42:23.852331",
"modified": "2018-05-08 18:32:51.803472",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
@ -640,7 +660,6 @@
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@ -660,7 +679,6 @@
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@ -680,7 +698,6 @@
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,

View File

@ -4,8 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import formatdate, format_datetime
from frappe.utils import getdate, get_datetime
from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate
from frappe.model.document import Document
from frappe.desk.form import assign_to
@ -84,7 +83,6 @@ def get_onboarding_details(parent, parenttype):
filters={"parent": parent, "parenttype": parenttype},
order_by= "idx")
def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
@ -139,6 +137,74 @@ def update_employee(employee, details, cancel=False):
setattr(employee, item.fieldname, new_data)
return employee
def validate_dates(doc, from_date, to_date):
date_of_joining, relieving_date = frappe.db.get_value("Employee", doc.employee, ["date_of_joining", "relieving_date"])
if getdate(from_date) > getdate(to_date):
frappe.throw(_("To date can not be less than from date"))
elif getdate(from_date) > getdate(nowdate()):
frappe.throw(_("Future dates not allowed"))
elif date_of_joining and getdate(from_date) < getdate(date_of_joining):
frappe.throw(_("From date can not be less than employee's joining date"))
elif relieving_date and getdate(to_date) > getdate(relieving_date):
frappe.throw(_("To date can not greater than employee's relieving date"))
def validate_overlap(doc, from_date, to_date, company = None):
query = """
select name
from `tab{0}`
where name != %(name)s
"""
query += get_doc_condition(doc.doctype)
if not doc.name:
# hack! if name is null, it could cause problems with !=
doc.name = "New "+doc.doctype
overlap_doc = frappe.db.sql(query.format(doc.doctype),{
"employee": doc.employee,
"from_date": from_date,
"to_date": to_date,
"name": doc.name,
"company": company
}, as_dict = 1)
if overlap_doc:
exists_for = doc.employee
if company:
exists_for = company
throw_overlap_error(doc, exists_for, overlap_doc[0].name, from_date, to_date)
def get_doc_condition(doctype):
if doctype == "Compensatory Leave Request":
return "and employee = %(employee)s and docstatus < 2 \
and (work_from_date between %(from_date)s and %(to_date)s \
or work_end_date between %(from_date)s and %(to_date)s \
or (work_from_date < %(from_date)s and work_end_date > %(to_date)s))"
elif doctype == "Leave Period":
return "and company = %(company)s and (from_date between %(from_date)s and %(to_date)s \
or to_date between %(from_date)s and %(to_date)s \
or (from_date < %(from_date)s and to_date > %(to_date)s))"
def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
msg = _("A {0} exists between {1} and {2} (").format(doc.doctype,
formatdate(from_date), formatdate(to_date)) \
+ """ <b><a href="#Form/{0}/{1}">{1}</a></b>""".format(doc.doctype, overlap_doc) \
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
def get_employee_leave_policy(employee):
leave_policy = frappe.db.get_value("Employee", employee, "leave_policy")
if not leave_policy:
employee_grade = frappe.db.get_value("Employee", employee, "grade")
if employee_grade:
leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy")
if not leave_policy:
frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade))
else:
frappe.throw(_("Employee {0} has no grade to get default leave policy").format(employee))
if leave_policy:
return frappe.get_doc("Leave Policy", leave_policy)
def validate_tax_declaration(declarations):
subcategories = []
for declaration in declarations:
@ -168,4 +234,3 @@ def get_leave_period(from_date, to_date, company):
if leave_period:
return leave_period