From 49b326fc08dbf44b87e8c97fe6b9582ab6e34449 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 12:53:06 +0530 Subject: [PATCH] feat(HR): Gratuity Payment --- .../doctype/payment_entry/payment_entry.py | 19 ++- erpnext/payroll/doctype/gratuity/gratuity.js | 24 +++- .../payroll/doctype/gratuity/gratuity.json | 13 +- erpnext/payroll/doctype/gratuity/gratuity.py | 116 +++++++++++++----- .../doctype/gratuity_rule/gratuity_rule.json | 10 +- 5 files changed, 135 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 31a4c8a387..df49667ed2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -242,7 +242,7 @@ class PaymentEntry(AccountsController): elif self.party_type == "Supplier": valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Employee": - valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") + valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") @@ -604,7 +604,7 @@ class PaymentEntry(AccountsController): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): if d.allocated_amount \ - and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance"): + and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"): frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() def update_expense_claim(self): @@ -932,6 +932,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre exchange_rate = ref_doc.get("exchange_rate") if party_account_currency != ref_doc.currency: total_amount = flt(total_amount) * flt(exchange_rate) + elif ref_doc.doctype == "Gratuity": + total_amount = ref_doc.amount if not total_amount: if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total @@ -955,6 +957,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) if party_account_currency == company_currency: exchange_rate = 1 + elif reference_doctype == "Gratuity": + outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount) else: outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) else: @@ -996,7 +1000,7 @@ def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_curre total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) elif ref_doc.doctype == "Employee Advance": total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) - + if not total_amount: total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( party_account_currency, company_currency, ref_doc) @@ -1032,7 +1036,7 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): outstanding_amount, bill_no = None - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): +if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") elif reference_doctype == "Expense Claim": @@ -1160,7 +1164,7 @@ def set_party_type(dt): party_type = "Customer" elif dt in ("Purchase Invoice", "Purchase Order"): party_type = "Supplier" - elif dt in ("Expense Claim", "Employee Advance"): + elif dt in ("Expense Claim", "Employee Advance", "Gratuity"): party_type = "Employee" elif dt in ("Fees"): party_type = "Student" @@ -1177,6 +1181,8 @@ def set_party_account(dt, dn, doc, party_type): party_account = doc.advance_account elif dt == "Expense Claim": party_account = doc.payable_account + elif dt == "Gratuity": + party_account = doc.expense_account else: party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account @@ -1222,6 +1228,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total + elif dt == "Gratuity": + grand_total = doc.amount + outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index cbf5119061..d6e93af524 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -5,7 +5,17 @@ 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'); + return frappe.call({ + method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', + args: { + "dt": cur_frm.doc.doctype, + "dn": cur_frm.doc.name + }, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); }); } }, @@ -17,6 +27,15 @@ frappe.ui.form.on('Gratuity', { } }; }); + frm.set_query("expense_account", function() { + return { + filters: { + "root_type": "Asset", + "is_group": 0, + "company": frm.doc.company + } + }; + }); }, employee: function(frm) { frm.events.calculate_work_experience_and_amount(frm); @@ -38,9 +57,6 @@ frappe.ui.form.on('Gratuity', { 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 index 8e7bb8616b..b8122dfb89 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -24,6 +24,7 @@ "column_break_15", "current_work_experience", "amount", + "paid_amount", "amended_from" ], "fields": [ @@ -77,7 +78,7 @@ "default": "0", "fieldname": "amount", "fieldtype": "Currency", - "label": "Amount", + "label": "Total Amount", "read_only": 1, "reqd": 1 }, @@ -164,11 +165,19 @@ "fieldtype": "Date", "label": "Payroll Date", "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1" + }, + { + "default": "0", + "depends_on": "eval:doc.pay_via_salary_slip == 0", + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-08-06 15:51:16.047698", + "modified": "2020-08-14 11:59:15.499548", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index fe31f4d7a6..23cc16b994 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -6,13 +6,19 @@ from __future__ import unicode_literals import frappe from frappe import _, bold from frappe.model.document import Document +from frappe.utils import flt +from math import floor -from dateutil.relativedelta import relativedelta - +from frappe.utils import get_datetime class Gratuity(Document): def validate(self): calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + def before_submit(self): + self.status = "Unpaid" + if self.pay_via_salary_slip: + self.status = "Paid" + def on_submit(self): if self.pay_via_salary_slip: additional_salary = frappe.new_doc('Additional Salary') @@ -25,9 +31,27 @@ class Gratuity(Document): additional_salary.ref_doctype = self.doctype additional_salary.ref_docname = self.name additional_salary.submit() - self.status = "Paid" - else: - self.status = "Unpaid" + + + def set_total_advance_paid(self): + paid_amount = frappe.db.sql(""" + select ifnull(sum(debit_in_account_currency), 0) as paid_amount + from `tabGL Entry` + where against_voucher_type = 'Gratuity' + and against_voucher = %s + and party_type = 'Employee' + and party = %s + """, (self.name, self.employee), as_dict=1)[0].paid_amount + + if flt(paid_amount) > self.amount: + frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount"), + EmployeeAdvanceOverPayment) + + + self.db_set("paid_amount", paid_amount) + if self.amount == self.paid_amount: + self.db_set("status", "Paid") + @frappe.whitelist() def calculate_work_experience_and_amount(employee, gratuity_rule): @@ -37,18 +61,29 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): return {'current_work_experience': current_work_experience, "amount": gratuity_amount} def calculate_work_experience(employee, gratuity_rule): + + total_working_days_per_year = frappe.db.get_value("Gratuity Rule", gratuity_rule, "total_working_days_per_year") + 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) + # 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 + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + + # current_work_experience = time_difference.years + + current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 + + print("--->", current_work_experience) if method == "Round off Work Experience": - if time_difference.months >= 6 and time_difference.days > 0: - current_work_experience += 1 + current_work_experience = round(current_work_experience) + else: + current_work_experience = floor(current_work_experience) + return current_work_experience @@ -61,41 +96,54 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) - fraction_to_be_paid = 0 + calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + + gratuity_amount = 0 + fraction_to_be_paid = 0 + year_left = experience 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: + if calculate_gratuity_amount_based_on == "Single Slab": + if experience >= slab.get("from", 0) and (slab.to == 0 or experience <= slab.to): + gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings + if slab.fraction_of_applicable_earnings: + break + elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": + if slab.get("to") == 0 and slab.get("from") == 0: + gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings break - gratuity_amount = total_applicable_components_amount * experience * fraction_to_be_paid + if experience > slab.get("to") and experience > slab.get("from"): + gratuity_amount += (slab.get("to") - slab.get("from")) * total_applicable_components_amount * slab.fraction_of_applicable_earnings + year_left -= (slab.get("to") - slab.get("from")) + print(experience, year_left) + elif slab.get("from") < experience < slab.get("to"): + print(year_left) + gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + + 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) + sal_slip = get_last_salary_slip(employee) - if not sal_slip: - frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(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 + 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 return total_applicable_components_amount diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index b5de28173a..40906fa6e4 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -8,6 +8,7 @@ "field_order": [ "applicable_earnings_component", "work_experience_calculation_function", + "total_working_days_per_year", "column_break_3", "disable", "calculate_gratuity_amount_based_on", @@ -26,7 +27,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Calculate Gratuity Amount Based on", - "options": "Last Month Salary\nActual Salary", + "options": "Single Slab\nSum of all previous slabs", "reqd": 1 }, { @@ -60,10 +61,15 @@ "fieldtype": "Select", "label": "Work Experience Calculation method", "options": "Round off Work Experience\nTake Exact Completed Years" + }, + { + "fieldname": "total_working_days_per_year", + "fieldtype": "Int", + "label": "Total Working Days per year" } ], "links": [], - "modified": "2020-08-06 12:28:13.757792", + "modified": "2020-08-13 16:21:10.466739", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule",