mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-15 01:43:10 +00:00
Merge branch 'develop' into fix-salary-slip-bg-job
This commit is contained in:
@@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
self.total_declared_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.total_exemption_amount = get_total_exemption_amount(self.declarations)
|
||||
self.total_exemption_amount = flt(
|
||||
get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
|
||||
@@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
hra_exemption = calculate_annual_eligible_hra_exemption(self)
|
||||
if hra_exemption:
|
||||
self.total_exemption_amount += hra_exemption["annual_exemption"]
|
||||
self.salary_structure_hra = hra_exemption["hra_amount"]
|
||||
self.annual_hra_exemption = hra_exemption["annual_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.total_exemption_amount = flt(
|
||||
self.total_exemption_amount, self.precision("total_exemption_amount")
|
||||
)
|
||||
self.salary_structure_hra = flt(
|
||||
hra_exemption["hra_amount"], self.precision("salary_structure_hra")
|
||||
)
|
||||
self.annual_hra_exemption = flt(
|
||||
hra_exemption["annual_exemption"], self.precision("annual_hra_exemption")
|
||||
)
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -4,25 +4,28 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.utils import DuplicateDeclarationError
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@taxexepmtion.com")
|
||||
make_employee("employee1@taxexepmtion.com")
|
||||
create_payroll_period()
|
||||
make_employee("employee@taxexemption.com", company="_Test Company")
|
||||
make_employee("employee1@taxexemption.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
|
||||
frappe.db.delete("Employee Tax Exemption Declaration")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_duplicate_category_in_declaration(self):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@@ -46,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@@ -68,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
duplicate_declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@@ -83,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
|
||||
duplicate_declaration.employee = frappe.get_value(
|
||||
"Employee", {"user_id": "employee1@taxexepmtion.com"}, "name"
|
||||
"Employee", {"user_id": "employee1@taxexemption.com"}, "name"
|
||||
)
|
||||
self.assertTrue(duplicate_declaration.insert)
|
||||
|
||||
@@ -91,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@@ -112,6 +115,298 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
|
||||
self.assertEqual(declaration.total_exemption_amount, 100000)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Monthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 36000)
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_daily_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Daily")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Daily HRA received = 3000
|
||||
# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 215000)
|
||||
# 50000 Standard Exemption + 215000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 265000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_weekly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Weekly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Weekly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 13000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 156000)
|
||||
# 50000 Standard Exemption + 156000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 206000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Fortnightly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Fortnightly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 6500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 78000)
|
||||
# 50000 Standard Exemption + 78000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 128000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Bimonthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Bimonthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 1500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 18000)
|
||||
# 100000 Standard Exemption + 18000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 118000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
make_salary_structure,
|
||||
)
|
||||
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = make_employee("employee@taxexemption2.com", company="_Test Company")
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
||||
# salary structure with base 50000, HRA 3000
|
||||
make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 1",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
)
|
||||
|
||||
# salary structure with base 70000, HRA = base * 0.2 = 14000
|
||||
salary_structure = make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 2",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
dont_submit=True,
|
||||
)
|
||||
for component_row in salary_structure.earnings:
|
||||
if component_row.salary_component == "HRA":
|
||||
component_row.amount = 0
|
||||
component_row.amount_based_on_formula = 1
|
||||
component_row.formula = "base * 0.2"
|
||||
break
|
||||
|
||||
salary_structure.submit()
|
||||
|
||||
create_salary_structure_assignment(
|
||||
employee,
|
||||
salary_structure.name,
|
||||
from_date=add_months(payroll_period.start_date, 6),
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
base=70000,
|
||||
allow_duplicate=True,
|
||||
)
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": payroll_period.name,
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 8500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 102000)
|
||||
# 50000 Standard Exemption + 102000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 152000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
|
||||
def create_payroll_period(**args):
|
||||
args = frappe._dict(args)
|
||||
@@ -163,3 +458,33 @@ def create_exemption_category():
|
||||
"is_active": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def setup_hra_exemption_prerequisites(frequency, employee=None):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
if not employee:
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
make_salary_structure(
|
||||
f"{frequency} Structure for HRA Exemption",
|
||||
frequency,
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
||||
@@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
self.total_actual_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs)
|
||||
self.exemption_amount = flt(
|
||||
get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
|
||||
@@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
hra_exemption = calculate_hra_exemption_for_period(self)
|
||||
if hra_exemption:
|
||||
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.monthly_house_rent = hra_exemption["monthly_house_rent"]
|
||||
self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
|
||||
self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount"))
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
self.monthly_house_rent = flt(
|
||||
hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent")
|
||||
)
|
||||
self.total_eligible_hra_exemption = flt(
|
||||
hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption")
|
||||
)
|
||||
|
||||
@@ -4,22 +4,26 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||
create_exemption_category,
|
||||
create_payroll_period,
|
||||
setup_hra_exemption_prerequisites,
|
||||
)
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
def setup(self):
|
||||
make_employee("employee@proofsubmission.com")
|
||||
create_payroll_period()
|
||||
class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@proofsubmission.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""")
|
||||
frappe.db.delete("Employee Tax Exemption Proof Submission")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_exemption_amount_lesser_than_category_max(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
declaration = frappe.get_doc(
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"payroll_period": "Test Payroll Period",
|
||||
@@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertTrue(declaration.save)
|
||||
self.assertTrue(declaration.submit)
|
||||
self.assertTrue(proof.save)
|
||||
self.assertTrue(proof.submit)
|
||||
|
||||
def test_duplicate_category_in_proof_submission(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name")
|
||||
setup_hra_exemption_prerequisites("Monthly", employee)
|
||||
payroll_period = frappe.db.get_value(
|
||||
"Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True
|
||||
)
|
||||
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"house_rent_payment_amount": 600000,
|
||||
"rented_in_metro_city": 1,
|
||||
"rented_from_date": payroll_period.start_date,
|
||||
"rented_to_date": payroll_period.end_date,
|
||||
"tax_exemption_proofs": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=100000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=50000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertEqual(proof.monthly_house_rent, 50000)
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(proof.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(proof.total_eligible_hra_exemption, 36000)
|
||||
|
||||
# total exemptions + house rent payment amount
|
||||
self.assertEqual(proof.total_actual_amount, 750000)
|
||||
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(proof.exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
@@ -76,9 +76,8 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nUnpaid\nPaid",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
@@ -194,7 +193,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-02 14:00:45.536152",
|
||||
"modified": "2022-05-27 13:56:14.349183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Gratuity",
|
||||
|
||||
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
@@ -0,0 +1,12 @@
|
||||
frappe.listview_settings["Gratuity"] = {
|
||||
get_indicator: function(doc) {
|
||||
let status_color = {
|
||||
"Draft": "red",
|
||||
"Submitted": "blue",
|
||||
"Cancelled": "red",
|
||||
"Paid": "green",
|
||||
"Unpaid": "orange",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
}
|
||||
};
|
||||
@@ -4,57 +4,69 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, get_datetime, getdate
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_deduction_salary_component,
|
||||
make_earning_salary_component,
|
||||
make_employee_salary_slip,
|
||||
make_holiday_list,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
||||
|
||||
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
||||
|
||||
|
||||
class TestGratuity(unittest.TestCase):
|
||||
class TestGratuity(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Gratuity")
|
||||
frappe.db.delete("Salary Slip")
|
||||
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||
|
||||
make_earning_salary_component(
|
||||
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
|
||||
)
|
||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||
make_holiday_list()
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
|
||||
salary_slip = get_last_salary_slip(new_employee)
|
||||
assert salary_slip is None
|
||||
self.assertIsNone(salary_slip)
|
||||
|
||||
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_current_slab_via_additional_salary(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
5-0 | 1
|
||||
"""
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
|
||||
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
||||
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
gratuity.reload()
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@@ -64,20 +76,44 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" 5 - 0 fraction is 1 """
|
||||
|
||||
gratuity_amount = component_amount[0].amount * experience
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
# additional salary creation (Pay via salary slip)
|
||||
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
||||
|
||||
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
# gratuity should be marked "Paid" on the next salary slip submission
|
||||
salary_slip = make_salary_slip("Test Gratuity", employee=employee)
|
||||
salary_slip.posting_date = getdate()
|
||||
salary_slip.insert()
|
||||
salary_slip.submit()
|
||||
|
||||
gratuity.reload()
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
||||
set_mode_of_payment_account()
|
||||
|
||||
@@ -86,22 +122,11 @@ class TestGratuity(unittest.TestCase):
|
||||
)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
|
||||
gratuity.reload()
|
||||
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@@ -111,35 +136,22 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
|
||||
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
self.assertEqual(gratuity.status, "Unpaid")
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
pe = get_payment_entry("Gratuity", gratuity.name)
|
||||
pe.reference_no = "123467"
|
||||
pe.reference_date = getdate()
|
||||
pe.submit()
|
||||
|
||||
pay_entry = get_payment_entry("Gratuity", gratuity.name)
|
||||
pay_entry.reference_no = "123467"
|
||||
pay_entry.reference_date = getdate()
|
||||
pay_entry.save()
|
||||
pay_entry.submit()
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_gratuity_rule(name):
|
||||
rule = frappe.db.exists("Gratuity Rule", name)
|
||||
@@ -149,7 +161,6 @@ def get_gratuity_rule(name):
|
||||
rule.applicable_earnings_component = []
|
||||
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
|
||||
rule.save()
|
||||
rule.reload()
|
||||
|
||||
return rule
|
||||
|
||||
@@ -204,23 +215,17 @@ def create_account():
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_employee_and_get_last_salary_slip():
|
||||
employee = make_employee("test_employee@salary.com", company="_Test Company")
|
||||
frappe.db.set_value("Employee", employee, "relieving_date", getdate())
|
||||
frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365)))
|
||||
def create_salary_slip(employee):
|
||||
if not frappe.db.exists("Salary Slip", {"employee": employee}):
|
||||
salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
|
||||
posting_date = get_first_day(add_months(getdate(), -1))
|
||||
salary_slip = make_employee_salary_slip(
|
||||
employee, "Monthly", "Test Gratuity", posting_date=posting_date
|
||||
)
|
||||
salary_slip.start_date = posting_date
|
||||
salary_slip.end_date = None
|
||||
salary_slip.submit()
|
||||
salary_slip = salary_slip.name
|
||||
else:
|
||||
salary_slip = get_last_salary_slip(employee)
|
||||
|
||||
if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
make_holiday_list()
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List"
|
||||
)
|
||||
|
||||
return employee, salary_slip
|
||||
return salary_slip
|
||||
|
||||
@@ -49,6 +49,7 @@ class PayrollEntry(Document):
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_employee_details()
|
||||
self.validate_payroll_payable_account()
|
||||
if self.validate_attendance:
|
||||
if self.validate_employee_attendance():
|
||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||
@@ -79,6 +80,14 @@ class PayrollEntry(Document):
|
||||
if len(emp_with_sal_slip):
|
||||
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
||||
|
||||
def validate_payroll_payable_account(self):
|
||||
if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account type cannot be set for payroll payable account {0}, please remove and try again"
|
||||
).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.delete_doc(
|
||||
"Salary Slip",
|
||||
|
||||
@@ -29,6 +29,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
calculate_amounts,
|
||||
create_repayment_entry,
|
||||
)
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||
get_benefit_component_amount,
|
||||
@@ -116,10 +119,10 @@ class SalarySlip(TransactionBase):
|
||||
self.update_payment_status_for_gratuity()
|
||||
|
||||
def update_payment_status_for_gratuity(self):
|
||||
add_salary = frappe.db.get_all(
|
||||
additional_salary = frappe.db.get_all(
|
||||
"Additional Salary",
|
||||
filters={
|
||||
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
|
||||
"payroll_date": ("between", [self.start_date, self.end_date]),
|
||||
"employee": self.employee,
|
||||
"ref_doctype": "Gratuity",
|
||||
"docstatus": 1,
|
||||
@@ -128,10 +131,10 @@ class SalarySlip(TransactionBase):
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if len(add_salary):
|
||||
if additional_salary:
|
||||
status = "Paid" if self.docstatus == 1 else "Unpaid"
|
||||
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
|
||||
if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status)
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status()
|
||||
@@ -1364,9 +1367,9 @@ class SalarySlip(TransactionBase):
|
||||
self.total_loan_repayment += payment.total_payment
|
||||
|
||||
def get_loan_details(self):
|
||||
return frappe.get_all(
|
||||
loan_details = frappe.get_all(
|
||||
"Loan",
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type"],
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
|
||||
filters={
|
||||
"applicant": self.employee,
|
||||
"docstatus": 1,
|
||||
@@ -1375,6 +1378,15 @@ class SalarySlip(TransactionBase):
|
||||
},
|
||||
)
|
||||
|
||||
if loan_details:
|
||||
for loan in loan_details:
|
||||
if loan.is_term_loan:
|
||||
process_loan_interest_accrual_for_term_loans(
|
||||
posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name
|
||||
)
|
||||
|
||||
return loan_details
|
||||
|
||||
def make_loan_repayment_entry(self):
|
||||
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||
for loan in self.loans:
|
||||
|
||||
@@ -997,7 +997,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
|
||||
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
if not salary_structure:
|
||||
@@ -1008,7 +1008,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
)
|
||||
|
||||
salary_structure_doc = make_salary_structure(
|
||||
salary_structure, payroll_frequency, employee=employee.name, company=employee.company
|
||||
salary_structure,
|
||||
payroll_frequency,
|
||||
employee=employee.name,
|
||||
company=employee.company,
|
||||
from_date=posting_date,
|
||||
)
|
||||
salary_slip_name = frappe.db.get_value(
|
||||
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
|
||||
@@ -1018,7 +1022,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
|
||||
salary_slip.employee_name = employee.employee_name
|
||||
salary_slip.payroll_frequency = payroll_frequency
|
||||
salary_slip.posting_date = nowdate()
|
||||
salary_slip.posting_date = posting_date or nowdate()
|
||||
salary_slip.insert()
|
||||
else:
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
||||
|
||||
@@ -253,6 +253,7 @@ def make_salary_slip(
|
||||
source_name,
|
||||
target_doc=None,
|
||||
employee=None,
|
||||
posting_date=None,
|
||||
as_print=False,
|
||||
print_format=None,
|
||||
for_preview=0,
|
||||
@@ -269,6 +270,9 @@ def make_salary_slip(
|
||||
target.designation = employee_details.designation
|
||||
target.department = employee_details.department
|
||||
|
||||
if posting_date:
|
||||
target.posting_date = posting_date
|
||||
|
||||
target.run_method("process_salary_structure", for_preview=for_preview)
|
||||
|
||||
doc = get_mapped_doc(
|
||||
|
||||
@@ -227,9 +227,12 @@ def create_salary_structure_assignment(
|
||||
company=None,
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
base=None,
|
||||
allow_duplicate=False,
|
||||
):
|
||||
|
||||
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
||||
if not allow_duplicate and frappe.db.exists(
|
||||
"Salary Structure Assignment", {"employee": employee}
|
||||
):
|
||||
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
|
||||
|
||||
if not payroll_period:
|
||||
@@ -242,7 +245,7 @@ def create_salary_structure_assignment(
|
||||
|
||||
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
||||
salary_structure_assignment.employee = employee
|
||||
salary_structure_assignment.base = 50000
|
||||
salary_structure_assignment.base = base or 50000
|
||||
salary_structure_assignment.variable = 5000
|
||||
|
||||
if not from_date:
|
||||
|
||||
Reference in New Issue
Block a user