diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json index 54377e94b30..f4db6f099a6 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json @@ -11,6 +11,7 @@ "max_working_hours_against_timesheet", "include_holidays_in_total_working_days", "disable_rounded_total", + "define_opening_balance_for_earning_and_deductions", "column_break_11", "daily_wages_fraction_for_half_day", "email_salary_slip_to_employee", @@ -91,13 +92,20 @@ "fieldname": "show_leave_balances_in_salary_slip", "fieldtype": "Check", "label": "Show Leave Balances in Salary Slip" + }, + { + "default": "0", + "description": "If checked, then the system will enable the provision to set the opening balance for earnings and deductions till date while creating a Salary Structure Assignment (if any)", + "fieldname": "define_opening_balance_for_earning_and_deductions", + "fieldtype": "Check", + "label": "Define Opening Balance for Earning and Deductions" } ], "icon": "fa fa-cog", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-03 17:49:59.579723", + "modified": "2022-12-21 17:30:08.704247", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Settings", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index b2243838202..232b5f39a7d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1063,7 +1063,26 @@ class SalarySlip(TransactionBase): ) exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 - return taxable_earnings - exempted_amount + opening_taxable_earning = self.get_opening_for( + "taxable_earnings_till_date", start_date, end_date + ) + + return (taxable_earnings + opening_taxable_earning) - exempted_amount + + def get_opening_for(self, field_to_select, start_date, end_date): + return ( + frappe.db.get_value( + "Salary Structure Assignment", + { + "employee": self.employee, + "salary_structure": self.salary_structure, + "from_date": ["between", [start_date, end_date]], + "docstatus": 1, + }, + field_to_select, + ) + or 0 + ) def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary @@ -1092,7 +1111,9 @@ class SalarySlip(TransactionBase): )[0][0] ) - return total_tax_paid + tax_deducted_till_date = self.get_opening_for("tax_deducted_till_date", start_date, end_date) + + return total_tax_paid + tax_deducted_till_date def get_taxable_earnings( self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 6e3b57239d4..32d0c7ed08f 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1030,6 +1030,104 @@ class TestSalarySlip(FrappeTestCase): activity_type.wage_rate = 25 activity_type.save() + def test_salary_slip_generation_against_opening_entries_in_ssa(self): + import math + + from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + payroll_period = frappe.db.get_value( + "Payroll Period", + { + "company": "_Test Company", + "start_date": ["<=", "2023-03-31"], + "end_date": [">=", "2022-04-01"], + }, + "name", + ) + + if not payroll_period: + payroll_period = create_payroll_period( + name="_Test Payroll Period for Tax", + company="_Test Company", + start_date="2022-04-01", + end_date="2023-03-31", + ) + else: + payroll_period = frappe.get_cached_doc("Payroll Period", payroll_period) + + emp = make_employee( + "test_employee_ss_with_opening_balance@salary.com", + company="_Test Company", + **{"date_of_joining": "2021-12-01"}, + ) + employee_doc = frappe.get_doc("Employee", emp) + + create_tax_slab(payroll_period, allow_tax_exemption=True) + + salary_structure_name = "Test Salary Structure for Opening Balance" + if not frappe.db.exists("Salary Structure", salary_structure_name): + salary_structure_doc = make_salary_structure( + salary_structure_name, + "Monthly", + company="_Test Company", + employee=emp, + from_date="2022-04-01", + payroll_period=payroll_period, + test_tax=True, + ) + + # validate no salary slip exists for the employee + self.assertTrue( + frappe.db.count( + "Salary Slip", + { + "employee": emp, + "salary_structure": salary_structure_doc.name, + "docstatus": 1, + "start_date": [">=", "2022-04-01"], + }, + ) + == 0 + ) + + remaining_sub_periods = get_period_factor( + emp, + get_first_day("2022-10-01"), + get_last_day("2022-10-01"), + "Monthly", + payroll_period, + depends_on_payment_days=0, + )[1] + + prev_period = math.ceil(remaining_sub_periods) + + annual_tax = 93036 # 89220 #data[0].get('applicable_tax') + monthly_tax_amount = 7732.40 # 7435 #annual_tax/12 + annual_earnings = 933600 # data[0].get('ctc') + monthly_earnings = 77800 # annual_earnings/12 + + # Get Salary Structure Assignment + ssa = frappe.get_value( + "Salary Structure Assignment", + {"employee": emp, "salary_structure": salary_structure_doc.name}, + "name", + ) + ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa) + + # Set opening balance for earning and tax deduction in Salary Structure Assignment + ssa_doc.taxable_earnings_till_date = monthly_earnings * prev_period + ssa_doc.tax_deducted_till_date = monthly_tax_amount * prev_period + ssa_doc.save() + + # Create Salary Slip + salary_slip = make_salary_slip( + salary_structure_doc.name, employee=employee_doc.name, posting_date=getdate("2022-10-01") + ) + for deduction in salary_slip.deductions: + if deduction.salary_component == "TDS": + self.assertEqual(deduction.amount, rounded(monthly_tax_amount)) + def get_no_of_days(): no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 6cd897e95d1..7cb573d6307 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -42,6 +42,13 @@ frappe.ui.form.on('Salary Structure Assignment', { }); }, + refresh: function(frm) { + if(frm.doc.__onload){ + frm.unhide_earnings_and_taxation_section = frm.doc.__onload.earning_and_deduction_entries_does_not_exists; + frm.trigger("set_earnings_and_taxation_section_visibility"); + } + }, + employee: function(frm) { if(frm.doc.employee){ frappe.call({ @@ -59,6 +66,8 @@ frappe.ui.form.on('Salary Structure Assignment', { } } }); + + frm.trigger("valiadte_joining_date_and_salary_slips"); } else{ frm.set_value("company", null); @@ -71,5 +80,33 @@ frappe.ui.form.on('Salary Structure Assignment', { frm.set_value("payroll_payable_account", r.default_payroll_payable_account); }); } - } + }, + + valiadte_joining_date_and_salary_slips: function(frm) { + frappe.call({ + method: "earning_and_deduction_entries_does_not_exists", + doc: frm.doc, + callback: function(data) { + let earning_and_deduction_entries_does_not_exists = data.message; + frm.unhide_earnings_and_taxation_section = earning_and_deduction_entries_does_not_exists; + frm.trigger("set_earnings_and_taxation_section_visibility"); + } + }); + }, + + set_earnings_and_taxation_section_visibility: function(frm) { + if(frm.unhide_earnings_and_taxation_section){ + frm.set_df_property('earnings_and_taxation_section', 'hidden', 0); + } + else{ + frm.set_df_property('earnings_and_taxation_section', 'hidden', 1); + } + }, + + from_date: function(frm) { + if (frm.doc.from_date) { + frm.trigger("valiadte_joining_date_and_salary_slips" ); + } + }, + }); diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index c8b98e5aafc..15bd2b3cc6f 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -22,6 +22,10 @@ "base", "column_break_9", "variable", + "earnings_and_taxation_section", + "tax_deducted_till_date", + "column_break_18", + "taxable_earnings_till_date", "amended_from" ], "fields": [ @@ -141,11 +145,31 @@ "fieldtype": "Link", "label": "Payroll Payable Account", "options": "Account" + }, + { + "allow_on_submit": 1, + "fieldname": "earnings_and_taxation_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "tax_deducted_till_date", + "fieldtype": "Currency", + "label": "Tax Deducted Till Date" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "taxable_earnings_till_date", + "fieldtype": "Currency", + "label": "Taxable Earnings Till Date" } ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:44:46.267974", + "modified": "2022-12-21 17:06:38.602361", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index e34e48e6c05..69fc5eb4917 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -13,10 +13,36 @@ class DuplicateAssignment(frappe.ValidationError): class SalaryStructureAssignment(Document): + def onload(self): + if self.employee: + self.set_onload( + "earning_and_deduction_entries_exists", self.earning_and_deduction_entries_does_not_exists() + ) + def validate(self): self.validate_dates() self.validate_income_tax_slab() self.set_payroll_payable_account() + self.valiadte_missing_taxable_earnings_and_deductions_till_date() + + def valiadte_missing_taxable_earnings_and_deductions_till_date(self): + if self.earning_and_deduction_entries_does_not_exists(): + if not self.taxable_earnings_till_date and not self.tax_deducted_till_date: + frappe.msgprint( + _( + """ + Not found any salary slip record(s) for the employee {0}.

+ Please specify {1} and {2} (if any), + for the correct tax calculation in future salary slips. + """ + ).format( + self.employee, + "" + _("Taxable Earnings Till Date") + "", + "" + _("Tax Deducted Till Date") + "", + ), + indicator="orange", + title=_("Warning"), + ) def validate_dates(self): joining_date, relieving_date = frappe.db.get_value( @@ -76,6 +102,56 @@ class SalaryStructureAssignment(Document): ) self.payroll_payable_account = payroll_payable_account + @frappe.whitelist() + def earning_and_deduction_entries_does_not_exists(self): + if self.enabled_settings_to_specify_earnings_and_deductions_till_date(): + if not self.joined_in_the_same_month() and not self.have_salary_slips(): + return True + else: + if self.docstatus in [1, 2] and ( + self.taxable_earnings_till_date or self.tax_deducted_till_date + ): + return True + return False + else: + return False + + def enabled_settings_to_specify_earnings_and_deductions_till_date(self): + """returns True if settings are enabled to specify earnings and deductions till date else False""" + + if frappe.db.get_single_value( + "Payroll Settings", "define_opening_balance_for_earning_and_deductions" + ): + return True + return False + + def have_salary_slips(self): + """returns True if salary structure assignment has salary slips else False""" + + salary_slip = frappe.db.get_value( + "Salary Slip", filters={"employee": self.employee, "docstatus": 1} + ) + + if salary_slip: + return True + + return False + + def joined_in_the_same_month(self): + """returns True if employee joined in same month as salary structure assignment from date else False""" + + date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") + from_date = getdate(self.from_date) + + if not self.from_date or not date_of_joining: + return False + + elif date_of_joining.month == from_date.month: + return True + + else: + return False + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: