Sagar Vora | 12b7e14 | 2022-06-24 13:20:46 +0530 | [diff] [blame] | 1 | import math |
| 2 | |
| 3 | import frappe |
| 4 | from frappe import _ |
| 5 | from frappe.utils import add_days, date_diff, flt, get_link_to_form, month_diff |
| 6 | |
| 7 | from erpnext.hr.utils import get_salary_assignments |
| 8 | from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip |
| 9 | |
| 10 | |
| 11 | def calculate_annual_eligible_hra_exemption(doc): |
| 12 | basic_component, hra_component = frappe.db.get_value( |
| 13 | "Company", doc.company, ["basic_component", "hra_component"] |
| 14 | ) |
| 15 | |
| 16 | if not (basic_component and hra_component): |
| 17 | frappe.throw( |
| 18 | _("Please set Basic and HRA component in Company {0}").format( |
| 19 | get_link_to_form("Company", doc.company) |
| 20 | ) |
| 21 | ) |
| 22 | |
| 23 | annual_exemption = monthly_exemption = hra_amount = basic_amount = 0 |
| 24 | |
| 25 | if hra_component and basic_component: |
| 26 | assignments = get_salary_assignments(doc.employee, doc.payroll_period) |
| 27 | |
| 28 | if not assignments and doc.docstatus == 1: |
| 29 | frappe.throw( |
| 30 | _("Salary Structure must be submitted before submission of {0}").format(doc.doctype) |
| 31 | ) |
| 32 | |
| 33 | assignment_dates = [assignment.from_date for assignment in assignments] |
| 34 | |
| 35 | for idx, assignment in enumerate(assignments): |
| 36 | if has_hra_component(assignment.salary_structure, hra_component): |
| 37 | basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip( |
| 38 | doc.employee, |
| 39 | assignment.salary_structure, |
| 40 | basic_component, |
| 41 | hra_component, |
| 42 | assignment.from_date, |
| 43 | ) |
| 44 | to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period) |
| 45 | |
| 46 | frequency = frappe.get_value( |
| 47 | "Salary Structure", assignment.salary_structure, "payroll_frequency" |
| 48 | ) |
| 49 | basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date) |
| 50 | hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date) |
| 51 | |
| 52 | if hra_amount: |
| 53 | if doc.monthly_house_rent: |
| 54 | annual_exemption = calculate_hra_exemption( |
| 55 | basic_amount, |
| 56 | hra_amount, |
| 57 | doc.monthly_house_rent, |
| 58 | doc.rented_in_metro_city, |
| 59 | ) |
| 60 | if annual_exemption > 0: |
| 61 | monthly_exemption = annual_exemption / 12 |
| 62 | else: |
| 63 | annual_exemption = 0 |
| 64 | |
| 65 | return frappe._dict( |
| 66 | { |
| 67 | "hra_amount": hra_amount, |
| 68 | "annual_exemption": annual_exemption, |
| 69 | "monthly_exemption": monthly_exemption, |
| 70 | } |
| 71 | ) |
| 72 | |
| 73 | |
| 74 | def has_hra_component(salary_structure, hra_component): |
| 75 | return frappe.db.exists( |
| 76 | "Salary Detail", |
| 77 | { |
| 78 | "parent": salary_structure, |
| 79 | "salary_component": hra_component, |
| 80 | "parentfield": "earnings", |
| 81 | "parenttype": "Salary Structure", |
| 82 | }, |
| 83 | ) |
| 84 | |
| 85 | |
| 86 | def get_end_date_for_assignment(assignment_dates, idx, payroll_period): |
| 87 | end_date = None |
| 88 | |
| 89 | try: |
| 90 | end_date = assignment_dates[idx + 1] |
| 91 | end_date = add_days(end_date, -1) |
| 92 | except IndexError: |
| 93 | pass |
| 94 | |
| 95 | if not end_date: |
| 96 | end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date") |
| 97 | |
| 98 | return end_date |
| 99 | |
| 100 | |
| 101 | def get_component_amt_from_salary_slip( |
| 102 | employee, salary_structure, basic_component, hra_component, from_date |
| 103 | ): |
| 104 | salary_slip = make_salary_slip( |
| 105 | salary_structure, |
| 106 | employee=employee, |
| 107 | for_preview=1, |
| 108 | ignore_permissions=True, |
| 109 | posting_date=from_date, |
| 110 | ) |
| 111 | |
| 112 | basic_amt, hra_amt = 0, 0 |
| 113 | for earning in salary_slip.earnings: |
| 114 | if earning.salary_component == basic_component: |
| 115 | basic_amt = earning.amount |
| 116 | elif earning.salary_component == hra_component: |
| 117 | hra_amt = earning.amount |
| 118 | if basic_amt and hra_amt: |
| 119 | return basic_amt, hra_amt |
| 120 | return basic_amt, hra_amt |
| 121 | |
| 122 | |
| 123 | def calculate_hra_exemption(annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city): |
| 124 | # TODO make this configurable |
| 125 | exemptions = [] |
| 126 | # case 1: The actual amount allotted by the employer as the HRA. |
| 127 | exemptions.append(annual_hra) |
| 128 | |
| 129 | # case 2: Actual rent paid less 10% of the basic salary. |
| 130 | actual_annual_rent = monthly_house_rent * 12 |
| 131 | exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1)) |
| 132 | |
| 133 | # case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city). |
| 134 | exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4) |
| 135 | |
| 136 | # return minimum of 3 cases |
| 137 | return min(exemptions) |
| 138 | |
| 139 | |
| 140 | def get_component_pay(frequency, amount, from_date, to_date): |
| 141 | days = date_diff(to_date, from_date) + 1 |
| 142 | |
| 143 | if frequency == "Daily": |
| 144 | return amount * days |
| 145 | elif frequency == "Weekly": |
| 146 | return amount * math.floor(days / 7) |
| 147 | elif frequency == "Fortnightly": |
| 148 | return amount * math.floor(days / 14) |
| 149 | elif frequency == "Monthly": |
| 150 | return amount * month_diff(to_date, from_date) |
| 151 | elif frequency == "Bimonthly": |
| 152 | return amount * (month_diff(to_date, from_date) / 2) |
| 153 | |
| 154 | |
| 155 | def calculate_hra_exemption_for_period(doc): |
| 156 | monthly_rent, eligible_hra = 0, 0 |
| 157 | if doc.house_rent_payment_amount: |
| 158 | validate_house_rent_dates(doc) |
| 159 | # TODO receive rented months or validate dates are start and end of months? |
| 160 | # Calc monthly rent, round to nearest .5 |
| 161 | factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1) / 30 |
| 162 | factor = round(factor * 2) / 2 |
| 163 | monthly_rent = doc.house_rent_payment_amount / factor |
| 164 | # update field used by calculate_annual_eligible_hra_exemption |
| 165 | doc.monthly_house_rent = monthly_rent |
| 166 | exemptions = calculate_annual_eligible_hra_exemption(doc) |
| 167 | |
| 168 | if exemptions["monthly_exemption"]: |
| 169 | # calc total exemption amount |
| 170 | eligible_hra = exemptions["monthly_exemption"] * factor |
| 171 | exemptions["monthly_house_rent"] = monthly_rent |
| 172 | exemptions["total_eligible_hra_exemption"] = eligible_hra |
| 173 | return exemptions |
| 174 | |
| 175 | |
| 176 | def validate_house_rent_dates(doc): |
| 177 | if not doc.rented_to_date or not doc.rented_from_date: |
| 178 | frappe.throw(_("House rented dates required for exemption calculation")) |
| 179 | |
| 180 | if date_diff(doc.rented_to_date, doc.rented_from_date) < 14: |
| 181 | frappe.throw(_("House rented dates should be atleast 15 days apart")) |
| 182 | |
| 183 | proofs = frappe.db.sql( |
| 184 | """ |
| 185 | select name |
| 186 | from `tabEmployee Tax Exemption Proof Submission` |
| 187 | where |
| 188 | docstatus=1 and employee=%(employee)s and payroll_period=%(payroll_period)s |
| 189 | and (rented_from_date between %(from_date)s and %(to_date)s or rented_to_date between %(from_date)s and %(to_date)s) |
| 190 | """, |
| 191 | { |
| 192 | "employee": doc.employee, |
| 193 | "payroll_period": doc.payroll_period, |
| 194 | "from_date": doc.rented_from_date, |
| 195 | "to_date": doc.rented_to_date, |
| 196 | }, |
| 197 | ) |
| 198 | |
| 199 | if proofs: |
| 200 | frappe.throw(_("House rent paid days overlapping with {0}").format(proofs[0][0])) |