From fc2cb3a85e4fd24f767b4c1e3bbd7ea321d0f4c5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 10 Aug 2020 11:52:34 +0530 Subject: [PATCH] Feat: gratuity --- erpnext/hr/doctype/employee/employee.json | 2 +- erpnext/payroll/doctype/gratuity/__init__.py | 0 erpnext/payroll/doctype/gratuity/gratuity.js | 46 +++++ .../payroll/doctype/gratuity/gratuity.json | 192 ++++++++++++++++++ erpnext/payroll/doctype/gratuity/gratuity.py | 113 +++++++++++ .../payroll/doctype/gratuity/test_gratuity.py | 10 + .../gratuity_applicable_component/__init__.py | 0 .../gratuity_applicable_component.json | 32 +++ .../gratuity_applicable_component.py | 10 + .../payroll/doctype/gratuity_rule/__init__.py | 0 .../doctype/gratuity_rule/gratuity_rule.js | 8 + .../doctype/gratuity_rule/gratuity_rule.json | 88 ++++++++ .../doctype/gratuity_rule/gratuity_rule.py | 10 + .../gratuity_rule/test_gratuity_rule.py | 10 + .../doctype/gratuity_rule_slab/__init__.py | 0 .../gratuity_rule_slab.json | 45 ++++ .../gratuity_rule_slab/gratuity_rule_slab.py | 10 + 17 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 erpnext/payroll/doctype/gratuity/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.js create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.json create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.py create mode 100644 erpnext/payroll/doctype/gratuity/test_gratuity.py create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4f1c04ff5d0..b60e39282b8 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 15:02:04.283657", + "modified": "2020-12-02 15:58:23.805489", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/payroll/doctype/gratuity/__init__.py b/erpnext/payroll/doctype/gratuity/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js new file mode 100644 index 00000000000..cbf5119061f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -0,0 +1,46 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Gratuity', { + refresh: function(frm){ + if(frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") { + frm.add_custom_button(__("Make Payment Entry"), function() { + frm.trigger('make_payment_entry'); + }); + } + }, + onload: function(frm){ + frm.set_query('salary_component', function() { + return { + filters: { + type: "Earning" + } + }; + }); + }, + employee: function(frm) { + frm.events.calculate_work_experience_and_amount(frm); + }, + gratuity_rule: function(frm){ + frm.events.calculate_work_experience_and_amount(frm); + }, + calculate_work_experience_and_amount: function(frm) { + + if(frm.doc.employee && frm.doc.gratuity_rule){ + frappe.call({ + method:"erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount", + args:{ + employee: frm.doc.employee, + gratuity_rule: frm.doc.gratuity_rule + } + }).then((r) => { + frm.set_value("current_work_experience", r.message['current_work_experience']); + frm.set_value("amount", r.message['amount']); + }); + } + }, + make_payment_entry: function(frm){ + console.log("Hello"); + } + +}); diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json new file mode 100644 index 00000000000..8e7bb8616b9 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -0,0 +1,192 @@ +{ + "actions": [], + "autoname": "HR-GRA-PAY-.#####", + "creation": "2020-08-05 20:52:13.024683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "designation", + "column_break_3", + "posting_date", + "status", + "company", + "gratuity_rule", + "section_break_5", + "pay_via_salary_slip", + "payroll_date", + "salary_component", + "expense_account", + "mode_of_payment", + "column_break_15", + "current_work_experience", + "amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1, + "reqd": 1 + }, + { + "default": "1", + "fieldname": "pay_via_salary_slip", + "fieldtype": "Check", + "label": "Pay via Salary Slip" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting date", + "reqd": 1 + }, + { + "depends_on": "eval: doc.pay_via_salary_slip == 1", + "fieldname": "salary_component", + "fieldtype": "Link", + "label": "Salary Component", + "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1", + "options": "Salary Component" + }, + { + "default": "0", + "fieldname": "current_work_experience", + "fieldtype": "Int", + "label": "Current Work Experience", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1, + "reqd": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Draft\nUnpaid\nPaid", + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "eval: doc.pay_via_salary_slip == 0", + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0", + "options": "Account" + }, + { + "depends_on": "eval: doc.pay_via_salary_slip == 0", + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0", + "options": "Mode of Payment" + }, + { + "fieldname": "gratuity_rule", + "fieldtype": "Link", + "label": "Gratuity Rule", + "options": "Gratuity Rule", + "reqd": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Payment Configuration" + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fetch_from": "employee.designation", + "fieldname": "designation", + "fieldtype": "Data", + "label": "Designation", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Gratuity", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.pay_via_salary_slip == 1", + "fieldname": "payroll_date", + "fieldtype": "Date", + "label": "Payroll Date", + "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-08-06 15:51:16.047698", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py new file mode 100644 index 00000000000..fe31f4d7a61 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, bold +from frappe.model.document import Document + +from dateutil.relativedelta import relativedelta + +class Gratuity(Document): + def validate(self): + calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + + def on_submit(self): + if self.pay_via_salary_slip: + additional_salary = frappe.new_doc('Additional Salary') + additional_salary.employee = self.employee + additional_salary.salary_component = self.salary_component + additional_salary.overwrite_salary_structure_amount = 0 + additional_salary.amount = self.amount + additional_salary.payroll_date = self.payroll_date + additional_salary.company = self.company + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name + additional_salary.submit() + self.status = "Paid" + else: + self.status = "Unpaid" + +@frappe.whitelist() +def calculate_work_experience_and_amount(employee, gratuity_rule): + current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0 + gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0 + + return {'current_work_experience': current_work_experience, "amount": gratuity_amount} + +def calculate_work_experience(employee, gratuity_rule): + date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) + if not relieving_date: + frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(employee))) + + time_difference = relativedelta(relieving_date, date_of_joining) + method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") + + current_work_experience = time_difference.years + + if method == "Round off Work Experience": + if time_difference.months >= 6 and time_difference.days > 0: + current_work_experience += 1 + + return current_work_experience + +def calculate_gratuity_amount(employee, gratuity_rule, experience): + applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) + applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] + + slabs = get_gratuity_rule_slabs(gratuity_rule) + + total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) + + + fraction_to_be_paid = 0 + + for slab in slabs: + if experience > slab.get("from", 0) and (slab.to == 0 or experience < slab.to): + fraction_to_be_paid = slab.fraction_of_applicable_earnings + if fraction_to_be_paid: + break + + gratuity_amount = total_applicable_components_amount * experience * fraction_to_be_paid + + return gratuity_amount + +def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule): + calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + if calculate_gratuity_amount_based_on == "Last Month Salary": + sal_slip = get_last_salary_slip(employee) + + if not sal_slip: + frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) + + component_and_amounts = frappe.get_list("Salary Detail", + filters={ + "docstatus": 1, + 'parent': sal_slip, + "parentfield": "earnings", + 'salary_component': ('in', applicable_earnings_component) + }, + fields=["amount"]) + total_applicable_components_amount = 0 + if not len(component_and_amounts): + frappe.throw("No Applicable Component is present in last month salary slip") + for data in component_and_amounts: + total_applicable_components_amount += data.amount + elif calculate_gratuity_amount_based_on == "Actual Salary": + pass + + return total_applicable_components_amount + +def get_gratuity_rule_slabs(gratuity_rule): + return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"]) + +def get_salary_structure(employee): + return frappe.get_list("Salary Structure Assignment", filters = {"employee": employee, 'docstatus': 1}, fields=["from_date", "salary_structure"], order_by = "from_date desc")[0].salary_structure + +def get_last_salary_slip(employee): + return frappe.get_list("Salary Slip", filters = {"employee": employee, 'docstatus': 1}, order_by = "start_date desc")[0].name + + + + diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py new file mode 100644 index 00000000000..92c1248b73f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestGratuity(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py b/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json new file mode 100644 index 00000000000..eea0e852b17 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-08-05 19:00:28.097265", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component" + ], + "fields": [ + { + "fieldname": "salary_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Salary Component ", + "options": "Salary Component", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-05 20:17:13.855035", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity Applicable Component", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py new file mode 100644 index 00000000000..23e4340b04f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityApplicableComponent(Document): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule/__init__.py b/erpnext/payroll/doctype/gratuity_rule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js new file mode 100644 index 00000000000..929370fb171 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Gratuity Rule', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json new file mode 100644 index 00000000000..b5de28173ab --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -0,0 +1,88 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-08-05 19:00:36.103500", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "applicable_earnings_component", + "work_experience_calculation_function", + "column_break_3", + "disable", + "calculate_gratuity_amount_based_on", + "gratuity_rules_section", + "gratuity_rule_slabs" + ], + "fields": [ + { + "default": "0", + "fieldname": "disable", + "fieldtype": "Check", + "label": "Disable" + }, + { + "fieldname": "calculate_gratuity_amount_based_on", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Calculate Gratuity Amount Based on", + "options": "Last Month Salary\nActual Salary", + "reqd": 1 + }, + { + "description": "Salary components should be part of the Salary Structure.", + "fieldname": "applicable_earnings_component", + "fieldtype": "Table MultiSelect", + "label": "Applicable Earnings Component", + "options": "Gratuity Applicable Component", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "gratuity_rules_section", + "fieldtype": "Section Break", + "label": "Gratuity Rules" + }, + { + "description": "Leave From and To blank for no upper and lower limit.", + "fieldname": "gratuity_rule_slabs", + "fieldtype": "Table", + "label": "Current Work Experience", + "options": "Gratuity Rule Slab", + "reqd": 1 + }, + { + "default": "Round off Work Experience", + "fieldname": "work_experience_calculation_function", + "fieldtype": "Select", + "label": "Work Experience Calculation method", + "options": "Round off Work Experience\nTake Exact Completed Years" + } + ], + "links": [], + "modified": "2020-08-06 12:28:13.757792", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity Rule", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py new file mode 100644 index 00000000000..10b2a87b976 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityRule(Document): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py new file mode 100644 index 00000000000..1f5dc4e571e --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestGratuityRule(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py b/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json new file mode 100644 index 00000000000..615829f45ae --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "creation": "2020-08-05 19:12:49.423500", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from", + "to", + "fraction_of_applicable_earnings" + ], + "fields": [ + { + "fieldname": "from", + "fieldtype": "Int", + "in_list_view": 1, + "label": "From(Year)" + }, + { + "fieldname": "to", + "fieldtype": "Int", + "in_list_view": 1, + "label": "To(Year)" + }, + { + "fieldname": "fraction_of_applicable_earnings", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Fraction of Applicable Earnings ", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-05 20:03:25.955448", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity Rule Slab", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py new file mode 100644 index 00000000000..fa468e77beb --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityRuleSlab(Document): + pass