From 58c4646199d2848bf621d48501618a3532769e55 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 16 Nov 2015 14:50:14 +0530 Subject: [PATCH 1/2] [fix][cleanup] Leave allocation, application and balance report cleanup and fixes --- .../leave_allocation/leave_allocation.js | 98 ++++---- .../leave_allocation/leave_allocation.py | 140 ++++++----- .../leave_application/leave_application.js | 32 ++- .../leave_application/leave_application.json | 26 +- .../leave_application/leave_application.py | 232 +++++++++++------- .../employee_leave_balance.js | 3 + .../employee_leave_balance.py | 93 +++---- erpnext/projects/doctype/time_log/time_log.js | 12 +- test_sites/apps.txt | 1 - test_sites/languages.txt | 1 - test_sites/test_site/site_config.json | 12 - 11 files changed, 342 insertions(+), 308 deletions(-) delete mode 100644 test_sites/apps.txt delete mode 100644 test_sites/languages.txt delete mode 100644 test_sites/test_site/site_config.json diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 994dc3cdd4..06c7693501 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -1,48 +1,62 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.cscript.onload = function(doc, dt, dn) { - if(!doc.posting_date) set_multiple(dt,dn,{posting_date:get_today()}); -} - - cur_frm.add_fetch('employee','employee_name','employee_name'); -cur_frm.cscript.employee = function(doc, dt, dn) { - calculate_total_leaves_allocated(doc, dt, dn); -} - -cur_frm.cscript.leave_type = function(doc, dt, dn) { - calculate_total_leaves_allocated(doc, dt, dn); -} - -cur_frm.cscript.fiscal_year = function(doc, dt, dn) { - calculate_total_leaves_allocated(doc, dt, dn); -} - -cur_frm.cscript.carry_forward = function(doc, dt, dn) { - calculate_total_leaves_allocated(doc, dt, dn); -} - -cur_frm.cscript.carry_forwarded_leaves = function(doc, dt, dn) { - set_multiple(dt,dn,{total_leaves_allocated : flt(doc.carry_forwarded_leaves)+flt(doc.new_leaves_allocated)}); -} - -cur_frm.cscript.new_leaves_allocated = function(doc, dt, dn) { - set_multiple(dt,dn,{total_leaves_allocated : flt(doc.carry_forwarded_leaves)+flt(doc.new_leaves_allocated)}); -} - -calculate_total_leaves_allocated = function(doc, dt, dn) { - if(cint(doc.carry_forward) == 1 && doc.leave_type && doc.fiscal_year && doc.employee){ - return get_server_fields('get_carry_forwarded_leaves','','', doc, dt, dn, 1); +frappe.ui.form.on("Leave Allocation", { + onload: function(frm) { + if(!frm.doc.from_date) frm.set_value("from_date", get_today()); + + frm.set_query("employee", function() { + return { + query: "erpnext.controllers.queries.employee_query" + } + }) + }, + + employee: function(frm) { + frm.trigger("calculate_total_leaves_allocated"); + }, + + leave_type: function(frm) { + frm.trigger("calculate_total_leaves_allocated"); + }, + + carry_forward: function(frm) { + frm.trigger("calculate_total_leaves_allocated"); + }, + + carry_forwarded_leaves: function(frm) { + frm.set_value("total_leaves_allocated", + flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); + }, + + new_leaves_allocated: function(frm) { + frm.set_value("total_leaves_allocated", + flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); + }, + + calculate_total_leaves_allocated: function(frm) { + if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) { + return frappe.call({ + method: "erpnext.hr.doctype.leave_allocation.leave_allocation.get_carry_forwarded_leaves", + args: { + "employee": frm.doc.employee, + "date": frm.doc.from_date, + "leave_type": frm.doc.leave_type, + "carry_forward": frm.doc.carry_forward + }, + callback: function(r) { + if (!r.exc && r.message) { + frm.set_value('carry_forwarded_leaves', r.message); + frm.set_value("total_leaves_allocated", + flt(r.message) + flt(frm.doc.new_leaves_allocated)); + } + } + }) + } else if (cint(frm.doc.carry_forward) == 0) { + frm.set_value("carry_forwarded_leaves", 0); + frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated)); + } } - else if(cint(doc.carry_forward) == 0){ - set_multiple(dt,dn,{carry_forwarded_leaves : 0,total_leaves_allocated : flt(doc.new_leaves_allocated)}); - } -} - -cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { - return{ - query: "erpnext.controllers.queries.employee_query" - } -} +}) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index f8461241ad..43facf6dcc 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -3,93 +3,105 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, flt, date_diff +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.doctype.leave_application.leave_application import get_approved_leaves_for_period class LeaveAllocation(Document): def validate(self): self.validate_period() self.validate_new_leaves_allocated_value() - self.check_existing_leave_allocation() - if not self.total_leaves_allocated: - self.total_leaves_allocated = self.new_leaves_allocated - + self.validate_allocation_overlap() + self.validate_back_dated_allocation() + self.set_total_leaves_allocated() + self.validate_total_leaves_allocated() set_employee_name(self) def on_update_after_submit(self): self.validate_new_leaves_allocated_value() - - def on_update(self): - self.get_total_allocated_leaves() + self.set_total_leaves_allocated() + + frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves)) + frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated)) + + self.validate_against_leave_applications() def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: - frappe.throw(_("Invalid period")) + frappe.throw(_("To date cannot be before from date")) def validate_new_leaves_allocated_value(self): """validate that leave allocation is in multiples of 0.5""" if flt(self.new_leaves_allocated) % 0.5: frappe.throw(_("Leaves must be allocated in multiples of 0.5")) - def check_existing_leave_allocation(self): - """check whether leave for same type is already allocated or not""" - leave_allocation = frappe.db.sql("""select name from `tabLeave Allocation` - where employee='%s' and leave_type='%s' and to_date >= '%s' and from_date <= '%s' and docstatus=1 - """%(self.employee, self.leave_type, self.from_date, self.to_date)) + def validate_allocation_overlap(self): + leave_allocation = frappe.db.sql(""" + select name from `tabLeave Allocation` + where employee=%s and leave_type=%s and docstatus=1 + and to_date >= %s and from_date <= %s""", + (self.employee, self.leave_type, self.from_date, self.to_date)) if leave_allocation: - frappe.msgprint(_("Leaves for type {0} already allocated for Employee {1} for period {2} - {3}").format(self.leave_type, - self.employee, self.from_date, self.to_date)) - frappe.throw(_('Reference') + ': {0}'.format(leave_allocation[0][0])) + frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} - {3}") + .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date))) + + frappe.throw(_('Reference') + ': {0}' + .format(leave_allocation[0][0])) + + def validate_back_dated_allocation(self): + future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + where employee=%s and leave_type=%s and docstatus=1 and from_date > %s + and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + + if future_allocation: + frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") + .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) - def get_leave_bal(self): - return self.get_leaves_allocated() - self.get_leaves_applied() + def set_total_leaves_allocated(self): + self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, + self.leave_type, self.from_date, self.carry_forward) + + self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated) + + if not self.total_leaves_allocated: + frappe.throw(_("Total leaves allocated is mandatory")) - def get_leaves_applied(self): - leaves_applied = frappe.db.sql("""select SUM(ifnull(total_leave_days, 0)) - from `tabLeave Application` where employee=%s and leave_type=%s - and to_date<=%s and docstatus=1""", - (self.employee, self.leave_type, self.from_date)) - return leaves_applied and flt(leaves_applied[0][0]) or 0 + def validate_total_leaves_allocated(self): + if date_diff(self.to_date, self.from_date) <= flt(self.total_leaves_allocated): + frappe.throw(_("Total allocated leaves are more than days in the period")) + + def validate_against_leave_applications(self): + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + self.from_date, self.to_date) + + if flt(leaves_taken) > flt(self.total_leaves_allocated): + frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) - def get_leaves_allocated(self): - leaves_allocated = frappe.db.sql("""select SUM(ifnull(total_leaves_allocated, 0)) - from `tabLeave Allocation` where employee=%s and leave_type=%s - and to_date<=%s and docstatus=1 and name!=%s""", - (self.employee, self.leave_type, self.from_date, self.name)) - return leaves_allocated and flt(leaves_allocated[0][0]) or 0 - - def allow_carry_forward(self): - """check whether carry forward is allowed or not for this leave type""" - cf = frappe.db.sql("""select is_carry_forward from `tabLeave Type` where name = %s""", - self.leave_type) - cf = cf and cint(cf[0][0]) or 0 - if not cf: - frappe.db.set(self,'carry_forward',0) - frappe.throw(_("Cannot carry forward {0}").format(self.leave_type)) - - def get_carry_forwarded_leaves(self): - if self.carry_forward: - self.allow_carry_forward() - - prev_bal = 0 - if cint(self.carry_forward) == 1: - prev_bal = self.get_leave_bal() - - ret = { - 'carry_forwarded_leaves': prev_bal, - 'total_leaves_allocated': flt(prev_bal) + flt(self.new_leaves_allocated) - } - return ret - - def get_total_allocated_leaves(self): - leave_det = self.get_carry_forwarded_leaves() - self.validate_total_leaves_allocated(leave_det) - frappe.db.set(self,'carry_forwarded_leaves',flt(leave_det['carry_forwarded_leaves'])) - frappe.db.set(self,'total_leaves_allocated',flt(leave_det['total_leaves_allocated'])) - - def validate_total_leaves_allocated(self, leave_det): - if date_diff(self.to_date, self.from_date) <= leave_det['total_leaves_allocated']: - frappe.throw(_("Total allocated leaves are more than period")) +@frappe.whitelist() +def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): + carry_forwarded_leaves = 0 + + if carry_forward: + validate_carry_forward(leave_type) + + previous_allocation = frappe.db.sql(""" + select name, from_date, to_date, total_leaves_allocated + from `tabLeave Allocation` + where employee=%s and leave_type=%s and docstatus=1 and to_date < %s + order by to_date desc limit 1 + """, (employee, leave_type, date), as_dict=1) + if previous_allocation: + leaves_taken = get_approved_leaves_for_period(employee, leave_type, + previous_allocation[0].from_date, previous_allocation[0].to_date) + + carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken) + + return carry_forwarded_leaves + +def validate_carry_forward(leave_type): + if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) + \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 835170a154..e708b77911 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -66,14 +66,18 @@ frappe.ui.form.on("Leave Application", { }, get_leave_balance: function(frm) { - if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) { - return frm.call({ - method: "get_leave_balance", + if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date) { + return frappe.call({ + method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on", args: { employee: frm.doc.employee, - from_date: frm.doc.from_date, - to_date: frm.doc.to_date, + date: frm.doc.from_date, leave_type: frm.doc.leave_type + }, + callback: function(r) { + if (!r.exc && r.message) { + frm.set_value('leave_balance', r.message); + } } }); } @@ -83,14 +87,20 @@ frappe.ui.form.on("Leave Application", { if(frm.doc.from_date && frm.doc.to_date) { if (cint(frm.doc.half_day)==1) { frm.set_value("total_leave_days", 0.5); - } else { + } else if (frm.doc.employee && frm.doc.leave_type){ // server call is done to include holidays in leave days calculations return frappe.call({ - method: 'erpnext.hr.doctype.leave_application.leave_application.get_total_leave_days', - args: { leave_app: frm.doc }, - callback: function(response) { - if (response && response.message) { - frm.set_value('total_leave_days', response.message.total_leave_days); + method: 'erpnext.hr.doctype.leave_application.leave_application.get_number_of_leave_days', + args: { + "employee": frm.doc.employee, + "leave_type": frm.doc.leave_type, + "from_date": frm.doc.from_date, + "to_date": frm.doc.to_date, + "half_day": frm.doc.half_day + }, + callback: function(r) { + if (r && r.message) { + frm.set_value('total_leave_days', r.message); frm.trigger("get_leave_balance"); } } diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 62e4cd8b58..10f6594ec6 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -418,28 +418,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 1, - "in_list_view": 0, - "label": "Fiscal Year", - "no_copy": 0, - "options": "Fiscal Year", - "permlevel": 0, - "print_hide": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 1, "bold": 0, @@ -559,7 +537,7 @@ "issingle": 0, "istable": 0, "max_attachments": 3, - "modified": "2015-10-28 16:14:25.640730", + "modified": "2015-11-15 19:32:32.258397", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", @@ -711,7 +689,7 @@ ], "read_only": 0, "read_only_onload": 0, - "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days,fiscal_year", + "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name" diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 8c91173d36..40f2136b70 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -2,13 +2,13 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, json +import frappe from frappe import _ - from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ comma_or, get_fullname -from frappe import msgprint from erpnext.hr.utils import set_employee_name +from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates + class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass @@ -18,8 +18,7 @@ class LeaveApproverIdentityError(frappe.ValidationError): pass from frappe.model.document import Document class LeaveApplication(Document): def get_feed(self): - return _("{0}: From {0} of type {1}").format(self.status, - self.employee_name, self.leave_type) + return _("{0}: From {0} of type {1}").format(self.status, self.employee_name, self.leave_type) def validate(self): if not getattr(self, "__islocal", None) and frappe.db.exists(self.doctype, self.name): @@ -29,7 +28,7 @@ class LeaveApplication(Document): set_employee_name(self) - self.validate_to_date() + self.validate_dates() self.validate_balance_leaves() self.validate_leave_overlap() self.validate_max_days() @@ -57,10 +56,41 @@ class LeaveApplication(Document): def on_cancel(self): # notify leave applier about cancellation self.notify_employee("cancelled") + + 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")) + + self.validate_dates_acorss_allocation() + self.validate_back_dated_application() + + def validate_dates_acorss_allocation(self): + def _get_leave_alloction_record(date): + allocation = frappe.db.sql("""select name from `tabLeave Allocation` + where employee=%s and leave_type=%s and docstatus=1 + and %s between from_date and to_date""", (self.employee, self.leave_type, date)) + + return allocation and allocation[0][0] + + allocation_based_on_from_date = _get_leave_alloction_record(self.from_date) + allocation_based_on_to_date = _get_leave_alloction_record(self.to_date) + + if not (allocation_based_on_from_date or allocation_based_on_to_date): + frappe.throw(_("Application period cannot be outside leave allocation period")) + + elif allocation_based_on_from_date != allocation_based_on_to_date: + frappe.throw(_("Application period cannot be across two alocation records")) + + def validate_back_dated_application(self): + future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + where employee=%s and leave_type=%s and docstatus=1 and from_date > %s + and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + + if future_allocation: + frappe.throw(_("Leave cannot be applied before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") + .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) def show_block_day_warning(self): - from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates - block_dates = get_applicable_block_dates(self.from_date, self.to_date, self.employee, self.company, all_lists=True) @@ -70,60 +100,39 @@ class LeaveApplication(Document): frappe.msgprint(formatdate(d.block_date) + ": " + d.reason) def validate_block_days(self): - from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates - block_dates = get_applicable_block_dates(self.from_date, self.to_date, self.employee, self.company) - if block_dates: - if self.status == "Approved": - frappe.throw(_("Cannot approve leave as you are not authorized to approve leaves on Block Dates"), - LeaveDayBlockedError) - - def get_holidays(self): - return get_holidays(self) - - def get_total_leave_days(self): - return get_total_leave_days(self) - - def validate_to_date(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")) + if block_dates and self.status == "Approved": + frappe.throw(_("You are not authorized to approve leaves on Block Dates"), LeaveDayBlockedError) def validate_balance_leaves(self): if self.from_date and self.to_date: - self.total_leave_days = self.get_total_leave_days()["total_leave_days"] + self.total_leave_days = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, self.to_date, self.half_day) if self.total_leave_days == 0: - frappe.throw(_("The day(s) on which you are applying for leave are holiday. You need not apply for leave.")) + frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) if not is_lwp(self.leave_type): - self.leave_balance = get_leave_balance(self.employee, - self.leave_type, self.from_date, self.to_date)["leave_balance"] + self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date) - if self.status != "Rejected" \ - and self.leave_balance - self.total_leave_days < 0: - #check if this leave type allow the remaining balance to be in negative. If yes then warn the user and continue to save else warn the user and don't save. + if self.status != "Rejected" and self.leave_balance < self.total_leave_days: if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}").format(self.leave_type)) + frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") + .format(self.leave_type)) else: - frappe.throw(_("There is not enough leave balance for Leave Type {0}").format(self.leave_type)) - + frappe.throw(_("There is not enough leave balance for Leave Type {0}") + .format(self.leave_type)) def validate_leave_overlap(self): if not self.name: self.name = "New Leave Application" - for d in frappe.db.sql("""select name, leave_type, posting_date, - from_date, to_date + for d in frappe.db.sql("""select name, leave_type, posting_date, from_date, to_date from `tabLeave Application` - where - employee = %(employee)s - and docstatus < 2 - and status in ("Open", "Approved") - and to_date >= %(from_date)s - and from_date <= %(to_date)s + where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved") + and to_date >= %(from_date)s and from_date <= %(to_date)s and name != %(name)s""", { "employee": self.employee, "from_date": self.from_date, @@ -131,9 +140,12 @@ class LeaveApplication(Document): "name": self.name }, as_dict = 1): - frappe.msgprint(_("Employee {0} has already applied for {1} between {2} and {3}").format(self.employee, - cstr(d['leave_type']), formatdate(d['from_date']), formatdate(d['to_date']))) - frappe.throw('{0}'.format(d["name"]), OverlapError) + frappe.msgprint(_("Employee {0} has already applied for {1} between {2} and {3}") + .format(self.employee, cstr(d['leave_type']), + formatdate(d['from_date']), formatdate(d['to_date']))) + + frappe.throw("""Exising Application: {0}""" + .format(d["name"]), OverlapError) def validate_max_days(self): max_days = frappe.db.get_value("Leave Type", self.leave_type, "max_days_allowed") @@ -145,7 +157,8 @@ class LeaveApplication(Document): leave_approvers = [l.leave_approver for l in employee.get("leave_approvers")] if len(leave_approvers) and self.leave_approver not in leave_approvers: - frappe.throw(_("Leave approver must be one of {0}").format(comma_or(leave_approvers)), InvalidLeaveApproverError) + frappe.throw(_("Leave approver must be one of {0}") + .format(comma_or(leave_approvers)), InvalidLeaveApproverError) elif self.leave_approver and not frappe.db.sql("""select name from `tabUserRole` where parent=%s and role='Leave Approver'""", self.leave_approver): @@ -153,8 +166,8 @@ class LeaveApplication(Document): .format(get_fullname(self.leave_approver), self.leave_approver), InvalidLeaveApproverError) elif self.docstatus==1 and len(leave_approvers) and self.leave_approver != frappe.session.user: - msgprint(_("Only the selected Leave Approver can submit this Leave Application"), - raise_exception=LeaveApproverIdentityError) + frappe.throw(_("Only the selected Leave Approver can submit this Leave Application"), + LeaveApproverIdentityError) def notify_employee(self, status): employee = frappe.get_doc("Employee", self.employee) @@ -213,60 +226,89 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approver.parent = %s and user.name like %s and approver.leave_approver=user.name""", (filters.get("employee"), "%" + txt + "%")) + +@frappe.whitelist() +def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day=None): + if half_day: + return 0.5 + number_of_days = date_diff(to_date, from_date) + 1 + if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"): + number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date)) + + return number_of_days -def get_holidays(leave_app): +@frappe.whitelist() +def get_leave_balance_on(employee, leave_type, date, allocation_records=None): + if allocation_records == None: + allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict()) + + allocation = allocation_records.get(leave_type, frappe._dict()) + + leaves_taken = get_approved_leaves_for_period(employee, leave_type, allocation.from_date, date) + + return flt(allocation.total_leaves_allocated) - flt(leaves_taken) + +def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): + leave_applications = frappe.db.sql(""" + select employee, leave_type, from_date, to_date, total_leave_days + from `tabLeave Application` + where employee=%(employee)s and leave_type=%(leave_type)s + and status="Approved" 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) + + leave_days = 0 + for leave_app in leave_applications: + if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date): + leave_days += leave_app.total_leave_days + else: + if leave_app.from_date < getdate(from_date): + leave_app.from_date = from_date + if leave_app.to_date > getdate(to_date): + leave_app.to_date = to_date + + leave_days += get_number_of_leave_days(employee, leave_type, + leave_app.from_date, leave_app.to_date) + + return leave_days + +def get_leave_allocation_records(date, employee=None): + conditions = (" and employee='%s'" % employee) if employee else "" + + leave_allocation_records = frappe.db.sql(""" + select employee, leave_type, total_leaves_allocated, from_date + from `tabLeave Allocation` + where %s between from_date and to_date and docstatus=1 {0}""".format(conditions), (date), as_dict=1) + + allocated_leaves = frappe._dict() + for d in leave_allocation_records: + allocated_leaves.setdefault(d.employee, frappe._dict()).setdefault(d.leave_type, frappe._dict({ + "from_date": d.from_date, + "total_leaves_allocated": d.total_leaves_allocated + })) + + return allocated_leaves + + +def get_holidays(employee, from_date, to_date): tot_hol = frappe.db.sql("""select count(*) from `tabHoliday` h1, `tabHoliday List` h2, `tabEmployee` e1 where e1.name = %s and h1.parent = h2.name and e1.holiday_list = h2.name - and h1.holiday_date between %s and %s""", (leave_app.employee, leave_app.from_date, - leave_app.to_date))[0][0] - # below line is needed. If an employee hasn't been assigned with any holiday list then above will return 0 rows. + and h1.holiday_date between %s and %s""", (employee, from_date, to_date))[0][0] + if not tot_hol: tot_hol = frappe.db.sql("""select count(*) from `tabHoliday` h1, `tabHoliday List` h2 where h1.parent = h2.name and h1.holiday_date between %s and %s - and ifnull(h2.is_default,0) = 1 and h2.fiscal_year = %s""", - (leave_app.from_date, leave_app.to_date, leave_app.fiscal_year))[0][0] + and ifnull(h2.is_default,0) = 1""", (from_date, to_date))[0][0] + return tot_hol -@frappe.whitelist() -def get_total_leave_days(leave_app): - # Parse Leave Application if neccessary - if isinstance(leave_app, str) or isinstance(leave_app, unicode): - leave_app = frappe.get_doc(json.loads(leave_app)) - - """Calculates total leave days based on input and holidays""" - ret = {'total_leave_days' : 0.5} - if not leave_app.half_day: - tot_days = date_diff(leave_app.to_date, leave_app.from_date) + 1 - if frappe.db.get_value("Leave Type", leave_app.leave_type, "include_holiday"): - ret = { - 'total_leave_days' : flt(tot_days) - } - else: - holidays = leave_app.get_holidays() - ret = { - 'total_leave_days' : flt(tot_days)-flt(holidays) - } - - return ret - -@frappe.whitelist() -def get_leave_balance(employee, leave_type, from_date, to_date): - leave_all = frappe.db.sql("""select total_leaves_allocated - from `tabLeave Allocation` where employee = %s and leave_type = %s - and from_date<=%s and to_date>=%s and docstatus = 1""", (employee, - leave_type, from_date, to_date)) - - leave_all = leave_all and flt(leave_all[0][0]) or 0 - - leave_app = frappe.db.sql("""select SUM(total_leave_days) - from `tabLeave Application` - where employee = %s and leave_type = %s and to_date>=%s and from_date<=%s - and status="Approved" and docstatus = 1""", (employee, leave_type, from_date, to_date)) - leave_app = leave_app and flt(leave_app[0][0]) or 0 - - ret = {'leave_balance': leave_all - leave_app} - return ret - def is_lwp(leave_type): lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type) return lwp and cint(lwp[0][0]) or 0 diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js index 41b1421b71..5df4554dec 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js @@ -7,12 +7,14 @@ frappe.query_reports["Employee Leave Balance"] = { "fieldname":"from_date", "label": __("From Date"), "fieldtype": "Date", + "reqd": 1, "default": frappe.datetime.year_start() }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", + "reqd": 1, "default": frappe.datetime.year_end() }, { @@ -20,6 +22,7 @@ frappe.query_reports["Employee Leave Balance"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", + "reqd": 1, "default": frappe.defaults.get_user_default("company") } ] diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 0aa88a82b0..7f1c442a10 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -4,67 +4,54 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.desk.reportview import execute as runreport +from erpnext.hr.doctype.leave_application.leave_application \ + import get_leave_allocation_records, get_leave_balance_on, get_approved_leaves_for_period + def execute(filters=None): - if not filters: filters = {} - - employee_filters = { - "status": "Active" - } + leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") - if filters.get("company"): - filters["company"] = filters.company - - employees = runreport(doctype="Employee", fields=["name", "employee_name", "department"], - filters=employee_filters, limit_page_length=None) - - if not employees: - frappe.throw(_("No employee found!")) - - leave_types = frappe.db.sql_list("select name from `tabLeave Type`") - - employee_names = [d.name for d in employees] - - allocations = frappe.db.sql("""select employee, leave_type, sum(new_leaves_allocated) as leaves_allocated - from `tabLeave Allocation` - where docstatus=1 and employee in (%s) and from_date >= '%s' and to_date <= '%s'""" % - (','.join(['%s']*len(employee_names)), filters.get("from_date"), - filters.get("to_date")), employee_names, as_dict=True) - - applications = frappe.db.sql("""select employee, leave_type, - SUM(total_leave_days) as leaves - from `tabLeave Application` - where status="Approved" and docstatus = 1 and employee in (%s) - and from_date >= '%s' and to_date <= '%s' - group by employee, leave_type""" % - (','.join(['%s']*len(employee_names)), filters.get("from_date"), - filters.get("to_date")), employee_names, as_dict=True) - + columns = get_columns(leave_types) + data = get_data(filters, leave_types) + + return columns, data + +def get_columns(leave_types): columns = [ - _("Employee") + ":Link/Employee:150", _("Employee Name") + "::200", _("Department") +"::150" + _("Employee") + ":Link/Employee:150", + _("Employee Name") + "::200", + _("Department") +"::150" ] for leave_type in leave_types: - columns.append(_(leave_type) + " " + _("Opening") + ":Float") - columns.append(_(leave_type) + " " + _("Taken") + ":Float") - columns.append(_(leave_type) + " " + _("Balance") + ":Float") + columns.append(_(leave_type) + " " + _("Taken") + ":Float:160") + columns.append(_(leave_type) + " " + _("Balance") + ":Float:160") + + return columns + +def get_data(filters, leave_types): - data = {} - for d in allocations: - data.setdefault((d.employee,d.leave_type), frappe._dict()).allocation = d.leaves_allocated + allocation_records_based_on_to_date = get_leave_allocation_records(filters.to_date) - for d in applications: - data.setdefault((d.employee, d.leave_type), frappe._dict()).leaves = d.leaves - - result = [] - for employee in employees: + active_employees = frappe.get_all("Employee", + filters = { "status": "Active", "company": filters.company}, + fields = ["name", "employee_name", "department"]) + + data = [] + for employee in active_employees: row = [employee.name, employee.employee_name, employee.department] - result.append(row) - for leave_type in leave_types: - tmp = data.get((employee.name, leave_type), frappe._dict()) - row.append(tmp.allocation or 0) - row.append(tmp.leaves or 0) - row.append((tmp.allocation or 0) - (tmp.leaves or 0)) - return columns, result + for leave_type in leave_types: + # leaves taken + leaves_taken = get_approved_leaves_for_period(employee.name, leave_type, + filters.from_date, filters.to_date) + + # closing balance + closing = get_leave_balance_on(employee.name, leave_type, filters.to_date, + allocation_records_based_on_to_date.get(employee.name, frappe._dict())) + + row += [leaves_taken, closing] + + data.append(row) + + return data \ No newline at end of file diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 5fce97009d..7b2faf9d2b 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -4,11 +4,13 @@ frappe.provide("erpnext.projects"); frappe.ui.form.on("Time Log", "onload", function(frm) { - if (frm.doc.for_manufacturing) { - frappe.ui.form.trigger("Time Log", "production_order"); - } - if (frm.doc.from_time && frm.doc.to_time) { - frappe.ui.form.trigger("Time Log", "to_time"); + if (frm.doc.__islocal) { + if (frm.doc.for_manufacturing) { + frappe.ui.form.trigger("Time Log", "production_order"); + } + if (frm.doc.from_time && frm.doc.to_time) { + frappe.ui.form.trigger("Time Log", "to_time"); + } } }); diff --git a/test_sites/apps.txt b/test_sites/apps.txt deleted file mode 100644 index 37967291f6..0000000000 --- a/test_sites/apps.txt +++ /dev/null @@ -1 +0,0 @@ -erpnext diff --git a/test_sites/languages.txt b/test_sites/languages.txt deleted file mode 100644 index 0163b22e4a..0000000000 --- a/test_sites/languages.txt +++ /dev/null @@ -1 +0,0 @@ -en english diff --git a/test_sites/test_site/site_config.json b/test_sites/test_site/site_config.json deleted file mode 100644 index 7d1194af75..0000000000 --- a/test_sites/test_site/site_config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "test_frappe", - "db_password": "test_frappe", - "auto_email_id": "test@example.com", - "mail_server": "smtp.example.com", - "mail_login": "test@example.com", - "mail_password": "test", - "admin_password": "admin", - "run_selenium_tests": 1, - "host_name": "http://localhost:8000", - "install_apps": ["erpnext"] -} From b6de519571a962f358851ef75422cc5205d58853 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 16 Nov 2015 15:35:09 +0530 Subject: [PATCH 2/2] [fix] Test case fixed --- .../leave_allocation/leave_allocation.py | 17 ++++++++++++----- .../leave_allocation/test_leave_allocation.py | 4 ++-- .../leave_application/leave_application.py | 4 +++- .../hr/doctype/salary_slip/test_salary_slip.py | 16 ++++++++++++++-- test_sites/apps.txt | 1 + test_sites/languages.txt | 1 + test_sites/test_site/site_config.json | 12 ++++++++++++ 7 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 test_sites/apps.txt create mode 100644 test_sites/languages.txt create mode 100644 test_sites/test_site/site_config.json diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 43facf6dcc..57eb146654 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -9,6 +9,12 @@ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period +class OverlapError(frappe.ValidationError): pass +class BackDatedAllocationError(frappe.ValidationError): pass +class OverAllocationError(frappe.ValidationError): pass +class LessAllocationError(frappe.ValidationError): pass +class ValueMultiplierError(frappe.ValidationError): pass + class LeaveAllocation(Document): def validate(self): self.validate_period() @@ -35,7 +41,7 @@ class LeaveAllocation(Document): def validate_new_leaves_allocated_value(self): """validate that leave allocation is in multiples of 0.5""" if flt(self.new_leaves_allocated) % 0.5: - frappe.throw(_("Leaves must be allocated in multiples of 0.5")) + frappe.throw(_("Leaves must be allocated in multiples of 0.5"), ValueMultiplierError) def validate_allocation_overlap(self): leave_allocation = frappe.db.sql(""" @@ -49,7 +55,7 @@ class LeaveAllocation(Document): .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date))) frappe.throw(_('Reference') + ': {0}' - .format(leave_allocation[0][0])) + .format(leave_allocation[0][0]), OverlapError) def validate_back_dated_allocation(self): future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` @@ -58,7 +64,8 @@ class LeaveAllocation(Document): if future_allocation: frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) + .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), + BackDatedAllocationError) def set_total_leaves_allocated(self): self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, @@ -71,14 +78,14 @@ class LeaveAllocation(Document): def validate_total_leaves_allocated(self): if date_diff(self.to_date, self.from_date) <= flt(self.total_leaves_allocated): - frappe.throw(_("Total allocated leaves are more than days in the period")) + frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) def validate_against_leave_applications(self): leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, self.from_date, self.to_date) if flt(leaves_taken) > flt(self.total_leaves_allocated): - frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + 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) @frappe.whitelist() def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index d36fb2cb69..b3eee31121 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -13,7 +13,7 @@ class TestLeaveAllocation(unittest.TestCase): "employee": employee.name, "employee_name": employee.employee_name, "leave_type": "_Test Leave Type", - "from_date": getdate("2015-10-1"), + "from_date": getdate("2015-10-01"), "to_date": getdate("2015-10-31"), "new_leaves_allocated": 5, "docstatus": 1 @@ -24,7 +24,7 @@ class TestLeaveAllocation(unittest.TestCase): "employee": employee.name, "employee_name": employee.employee_name, "leave_type": "_Test Leave Type", - "from_date": getdate("2015-09-1"), + "from_date": getdate("2015-09-01"), "to_date": getdate("2015-11-30"), "new_leaves_allocated": 5 } diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 40f2136b70..1d84a4013b 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -49,6 +49,8 @@ class LeaveApplication(Document): def on_submit(self): if self.status != "Approved": frappe.throw(_("Only Leave Applications with status 'Approved' can be submitted")) + + self.validate_back_dated_application() # notify leave applier about approval self.notify_employee(self.status) @@ -87,7 +89,7 @@ class LeaveApplication(Document): and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) if future_allocation: - frappe.throw(_("Leave cannot be applied before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") + frappe.throw(_("Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) def show_block_day_warning(self): diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 2bb7f52f63..fb69440cab 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -10,8 +10,20 @@ from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_sli class TestSalarySlip(unittest.TestCase): def setUp(self): - frappe.db.sql("""delete from `tabLeave Application`""") - frappe.db.sql("""delete from `tabSalary Slip`""") + for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: + frappe.db.sql("delete from `tab%s`" % dt) + + allocation = frappe.get_doc({ + "doctype": "Leave Allocation", + "employee": "_T-Employee-0001", + "leave_type": "_Test Leave Type LWP", + "from_date": "2013-01-01", + "to_date": "2015-12-31", + "new_leaves_allocated": 5 + }) + + allocation.insert() + allocation.submit() frappe.db.set_value("Holiday List", "_Test Holiday List", "is_default", 1) diff --git a/test_sites/apps.txt b/test_sites/apps.txt new file mode 100644 index 0000000000..ee6454915c --- /dev/null +++ b/test_sites/apps.txt @@ -0,0 +1 @@ +erpnext \ No newline at end of file diff --git a/test_sites/languages.txt b/test_sites/languages.txt new file mode 100644 index 0000000000..cf2b150830 --- /dev/null +++ b/test_sites/languages.txt @@ -0,0 +1 @@ +en english \ No newline at end of file diff --git a/test_sites/test_site/site_config.json b/test_sites/test_site/site_config.json new file mode 100644 index 0000000000..48b330bc72 --- /dev/null +++ b/test_sites/test_site/site_config.json @@ -0,0 +1,12 @@ +{ + "db_name": "test_frappe", + "db_password": "test_frappe", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "run_selenium_tests": 1, + "host_name": "http://localhost:8000", + "install_apps": ["erpnext"] +} \ No newline at end of file