diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 33c63268054..27c6ebaf2b6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -506,6 +506,10 @@ accounting_dimension_doctypes = [ ] regional_overrides = { + "India": { + "erpnext.hr.utils.calculate_annual_eligible_hra_exemption": "erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption", + "erpnext.hr.utils.calculate_hra_exemption_for_period": "erpnext.regional.india.utils.calculate_hra_exemption_for_period", + }, "France": { "erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method" }, diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py index c133ab4b8a7..ccf16565c1f 100644 --- a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py @@ -5,6 +5,8 @@ import frappe from frappe import _ +import erpnext + def execute(filters=None): data = get_data(filters) @@ -14,7 +16,7 @@ def execute(filters=None): def get_columns(filters): - return [ + columns = [ { "label": _("Employee"), "options": "Employee", @@ -29,6 +31,14 @@ def get_columns(filters): "fieldtype": "Link", "width": 160, }, + ] + + if erpnext.get_region() == "India": + columns.append( + {"label": _("PAN Number"), "fieldname": "pan_number", "fieldtype": "Data", "width": 140} + ) + + columns += [ {"label": _("Income Tax Component"), "fieldname": "it_comp", "fieldtype": "Data", "width": 170}, { "label": _("Income Tax Amount"), @@ -47,6 +57,8 @@ def get_columns(filters): {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, ] + return columns + def get_conditions(filters): conditions = [""] @@ -73,6 +85,11 @@ def get_data(filters): data = [] + if erpnext.get_region() == "India": + employee_pan_dict = frappe._dict( + frappe.db.sql(""" select employee, pan_number from `tabEmployee`""") + ) + component_types = frappe.db.sql( """ select name from `tabSalary Component` where is_income_tax_component = 1 """ @@ -100,15 +117,20 @@ def get_data(filters): ) for d in entry: - data.append( - { - "employee": d.employee, - "employee_name": d.employee_name, - "it_comp": d.salary_component, - "posting_date": d.posting_date, - "it_amount": d.amount, - "gross_pay": d.gross_pay, - } - ) + + employee = { + "employee": d.employee, + "employee_name": d.employee_name, + "it_comp": d.salary_component, + "posting_date": d.posting_date, + # "pan_number": employee_pan_dict.get(d.employee), + "it_amount": d.amount, + "gross_pay": d.gross_pay, + } + + if erpnext.get_region() == "India": + employee["pan_number"] = employee_pan_dict.get(d.employee) + + data.append(employee) return data diff --git a/erpnext/regional/doctype/lower_deduction_certificate/__init__.py b/erpnext/regional/doctype/lower_deduction_certificate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js new file mode 100644 index 00000000000..8257bf8a969 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.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('Lower Deduction Certificate', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json new file mode 100644 index 00000000000..c32ab6bec24 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -0,0 +1,140 @@ +{ + "actions": [], + "autoname": "field:certificate_no", + "creation": "2020-03-10 23:12:10.072631", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "certificate_details_section", + "tax_withholding_category", + "fiscal_year", + "column_break_3", + "certificate_no", + "section_break_3", + "supplier", + "column_break_7", + "pan_no", + "validity_details_section", + "valid_from", + "column_break_10", + "valid_upto", + "section_break_9", + "rate", + "column_break_14", + "certificate_limit" + ], + "fields": [ + { + "fieldname": "certificate_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Certificate No", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Deductee Details" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, + { + "fetch_from": "supplier.pan", + "fetch_if_empty": 1, + "fieldname": "pan_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "PAN No", + "reqd": 1 + }, + { + "fieldname": "validity_details_section", + "fieldtype": "Section Break", + "label": "Validity Details" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "rate", + "fieldtype": "Percent", + "label": "Rate Of TDS As Per Certificate", + "reqd": 1 + }, + { + "fieldname": "certificate_limit", + "fieldtype": "Currency", + "label": "Certificate Limit", + "reqd": 1 + }, + { + "fieldname": "certificate_details_section", + "fieldtype": "Section Break", + "label": "Certificate Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "fieldtype": "Link", + "label": "Fiscal Year", + "options": "Fiscal Year", + "reqd": 1 + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "label": "Tax Withholding Category", + "options": "Tax Withholding Category", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-23 18:33:38.962622", + "modified_by": "Administrator", + "module": "Regional", + "name": "Lower Deduction Certificate", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py new file mode 100644 index 00000000000..cc223e91bc8 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -0,0 +1,60 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_link_to_form, getdate + +from erpnext.accounts.utils import get_fiscal_year + + +class LowerDeductionCertificate(Document): + def validate(self): + self.validate_dates() + self.validate_supplier_against_tax_category() + + def validate_dates(self): + if getdate(self.valid_upto) < getdate(self.valid_from): + frappe.throw(_("Valid Upto date cannot be before Valid From date")) + + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + if not (fiscal_year.year_start_date <= getdate(self.valid_from) <= fiscal_year.year_end_date): + frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date): + frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + + def validate_supplier_against_tax_category(self): + duplicate_certificate = frappe.db.get_value( + "Lower Deduction Certificate", + { + "supplier": self.supplier, + "tax_withholding_category": self.tax_withholding_category, + "name": ("!=", self.name), + }, + ["name", "valid_from", "valid_upto"], + as_dict=True, + ) + if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate): + certificate_link = get_link_to_form("Lower Deduction Certificate", duplicate_certificate.name) + frappe.throw( + _( + "There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period." + ).format( + certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category) + ) + ) + + def are_dates_overlapping(self, duplicate_certificate): + valid_from = duplicate_certificate.valid_from + valid_upto = duplicate_certificate.valid_upto + if valid_from <= getdate(self.valid_from) <= valid_upto: + return True + elif valid_from <= getdate(self.valid_upto) <= valid_upto: + return True + elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto): + return True + return False diff --git a/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py new file mode 100644 index 00000000000..d8e78019311 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py @@ -0,0 +1,9 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestLowerDeductionCertificate(unittest.TestCase): + pass diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py new file mode 100644 index 00000000000..33f8cb2895b --- /dev/null +++ b/erpnext/regional/india/setup.py @@ -0,0 +1,321 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def setup(company=None, patch=True): + # Company independent fixtures should be called only once at the first company setup + if patch or frappe.db.count("Company", {"country": "India"}) <= 1: + make_custom_fields() + create_gratuity_rule() + create_salary_components() + + +def make_custom_fields(): + create_custom_fields( + { + "Salary Component": [ + { + "fieldname": "component_type", + "label": "Component Type", + "fieldtype": "Select", + "insert_after": "description", + "options": ( + "\nProvident Fund\nAdditional Provident Fund\nProvident Fund" " Loan\nProfessional Tax" + ), + "depends_on": 'eval:doc.type == "Deduction"', + "translatable": 0, + }, + ], + "Employee": [ + { + "fieldname": "ifsc_code", + "label": "IFSC Code", + "fieldtype": "Data", + "insert_after": "bank_ac_no", + "print_hide": 1, + "depends_on": 'eval:doc.salary_mode == "Bank"', + "translatable": 0, + }, + { + "fieldname": "pan_number", + "label": "PAN Number", + "fieldtype": "Data", + "insert_after": "payroll_cost_center", + "print_hide": 1, + "translatable": 0, + }, + { + "fieldname": "micr_code", + "label": "MICR Code", + "fieldtype": "Data", + "insert_after": "ifsc_code", + "print_hide": 1, + "depends_on": 'eval:doc.salary_mode == "Bank"', + "translatable": 0, + }, + { + "fieldname": "provident_fund_account", + "label": "Provident Fund Account", + "fieldtype": "Data", + "insert_after": "pan_number", + "translatable": 0, + }, + ], + "Company": [ + { + "fieldname": "hra_section", + "label": "HRA Settings", + "fieldtype": "Section Break", + "insert_after": "asset_received_but_not_billed", + "collapsible": 1, + }, + { + "fieldname": "basic_component", + "label": "Basic Component", + "fieldtype": "Link", + "options": "Salary Component", + "insert_after": "hra_section", + }, + { + "fieldname": "hra_component", + "label": "HRA Component", + "fieldtype": "Link", + "options": "Salary Component", + "insert_after": "basic_component", + }, + { + "fieldname": "hra_column_break", + "fieldtype": "Column Break", + "insert_after": "hra_component", + }, + { + "fieldname": "arrear_component", + "label": "Arrear Component", + "fieldtype": "Link", + "options": "Salary Component", + "insert_after": "hra_column_break", + }, + { + "fieldname": "non_profit_section", + "label": "Non Profit Settings", + "fieldtype": "Section Break", + "insert_after": "arrear_component", + "collapsible": 1, + }, + { + "fieldname": "company_80g_number", + "label": "80G Number", + "fieldtype": "Data", + "insert_after": "non_profit_section", + "translatable": 0, + }, + { + "fieldname": "with_effect_from", + "label": "80G With Effect From", + "fieldtype": "Date", + "insert_after": "company_80g_number", + }, + ], + "Employee Tax Exemption Declaration": [ + { + "fieldname": "hra_section", + "label": "HRA Exemption", + "fieldtype": "Section Break", + "insert_after": "declarations", + }, + { + "fieldname": "monthly_house_rent", + "label": "Monthly House Rent", + "fieldtype": "Currency", + "insert_after": "hra_section", + }, + { + "fieldname": "rented_in_metro_city", + "label": "Rented in Metro City", + "fieldtype": "Check", + "insert_after": "monthly_house_rent", + "depends_on": "monthly_house_rent", + }, + { + "fieldname": "salary_structure_hra", + "label": "HRA as per Salary Structure", + "fieldtype": "Currency", + "insert_after": "rented_in_metro_city", + "read_only": 1, + "depends_on": "monthly_house_rent", + }, + { + "fieldname": "hra_column_break", + "fieldtype": "Column Break", + "insert_after": "salary_structure_hra", + "depends_on": "monthly_house_rent", + }, + { + "fieldname": "annual_hra_exemption", + "label": "Annual HRA Exemption", + "fieldtype": "Currency", + "insert_after": "hra_column_break", + "read_only": 1, + "depends_on": "monthly_house_rent", + }, + { + "fieldname": "monthly_hra_exemption", + "label": "Monthly HRA Exemption", + "fieldtype": "Currency", + "insert_after": "annual_hra_exemption", + "read_only": 1, + "depends_on": "monthly_house_rent", + }, + ], + "Employee Tax Exemption Proof Submission": [ + { + "fieldname": "hra_section", + "label": "HRA Exemption", + "fieldtype": "Section Break", + "insert_after": "tax_exemption_proofs", + }, + { + "fieldname": "house_rent_payment_amount", + "label": "House Rent Payment Amount", + "fieldtype": "Currency", + "insert_after": "hra_section", + }, + { + "fieldname": "rented_in_metro_city", + "label": "Rented in Metro City", + "fieldtype": "Check", + "insert_after": "house_rent_payment_amount", + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "rented_from_date", + "label": "Rented From Date", + "fieldtype": "Date", + "insert_after": "rented_in_metro_city", + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "rented_to_date", + "label": "Rented To Date", + "fieldtype": "Date", + "insert_after": "rented_from_date", + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "hra_column_break", + "fieldtype": "Column Break", + "insert_after": "rented_to_date", + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "monthly_house_rent", + "label": "Monthly House Rent", + "fieldtype": "Currency", + "insert_after": "hra_column_break", + "read_only": 1, + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "monthly_hra_exemption", + "label": "Monthly Eligible Amount", + "fieldtype": "Currency", + "insert_after": "monthly_house_rent", + "read_only": 1, + "depends_on": "house_rent_payment_amount", + }, + { + "fieldname": "total_eligible_hra_exemption", + "label": "Total Eligible HRA Exemption", + "fieldtype": "Currency", + "insert_after": "monthly_hra_exemption", + "read_only": 1, + "depends_on": "house_rent_payment_amount", + }, + ], + } + ) + + +def create_gratuity_rule(): + # Standard Indain Gratuity Rule + if frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): + return + + rule = frappe.new_doc("Gratuity Rule") + rule.update( + { + "name": "Indian Standard Gratuity Rule", + "calculate_gratuity_amount_based_on": "Current Slab", + "work_experience_calculation_method": "Round Off Work Experience", + "minimum_year_for_gratuity": 5, + "gratuity_rule_slabs": [ + { + "from_year": 0, + "to_year": 0, + "fraction_of_applicable_earnings": 15 / 26, + } + ], + } + ) + rule.flags.ignore_mandatory = True + rule.save() + + +def create_salary_components(): + docs = [ + { + "doctype": "Salary Component", + "salary_component": "Professional Tax", + "description": "Professional Tax", + "type": "Deduction", + "exempted_from_income_tax": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Provident Fund", + "description": "Provident fund", + "type": "Deduction", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "House Rent Allowance", + "description": "House Rent Allowance", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Basic", + "description": "Basic", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Arrear", + "description": "Arrear", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Leave Encashment", + "description": "Leave Encashment", + "type": "Earning", + "is_tax_applicable": 1, + }, + ] + + for d in docs: + try: + doc = frappe.get_doc(d) + doc.flags.ignore_permissions = True + doc.insert(ignore_if_duplicate=True) + + except frappe.DuplicateEntryError: + frappe.clear_messages() diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py new file mode 100644 index 00000000000..23de0753d90 --- /dev/null +++ b/erpnext/regional/india/utils.py @@ -0,0 +1,200 @@ +import math + +import frappe +from frappe import _ +from frappe.utils import add_days, date_diff, flt, get_link_to_form, month_diff + +from erpnext.hr.utils import get_salary_assignments +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip + + +def calculate_annual_eligible_hra_exemption(doc): + basic_component, hra_component = frappe.db.get_value( + "Company", doc.company, ["basic_component", "hra_component"] + ) + + if not (basic_component and hra_component): + frappe.throw( + _("Please set Basic and HRA component in Company {0}").format( + get_link_to_form("Company", doc.company) + ) + ) + + annual_exemption = monthly_exemption = hra_amount = basic_amount = 0 + + if hra_component and basic_component: + assignments = get_salary_assignments(doc.employee, doc.payroll_period) + + if not assignments and doc.docstatus == 1: + frappe.throw( + _("Salary Structure must be submitted before submission of {0}").format(doc.doctype) + ) + + assignment_dates = [assignment.from_date for assignment in assignments] + + for idx, assignment in enumerate(assignments): + if has_hra_component(assignment.salary_structure, hra_component): + basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip( + doc.employee, + assignment.salary_structure, + basic_component, + hra_component, + assignment.from_date, + ) + to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period) + + frequency = frappe.get_value( + "Salary Structure", assignment.salary_structure, "payroll_frequency" + ) + basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date) + hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date) + + if hra_amount: + if doc.monthly_house_rent: + annual_exemption = calculate_hra_exemption( + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) + if annual_exemption > 0: + monthly_exemption = annual_exemption / 12 + else: + annual_exemption = 0 + + return frappe._dict( + { + "hra_amount": hra_amount, + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, + } + ) + + +def has_hra_component(salary_structure, hra_component): + return frappe.db.exists( + "Salary Detail", + { + "parent": salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) + + +def get_end_date_for_assignment(assignment_dates, idx, payroll_period): + end_date = None + + try: + end_date = assignment_dates[idx + 1] + end_date = add_days(end_date, -1) + except IndexError: + pass + + if not end_date: + end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date") + + return end_date + + +def get_component_amt_from_salary_slip( + employee, salary_structure, basic_component, hra_component, from_date +): + salary_slip = make_salary_slip( + salary_structure, + employee=employee, + for_preview=1, + ignore_permissions=True, + posting_date=from_date, + ) + + basic_amt, hra_amt = 0, 0 + for earning in salary_slip.earnings: + if earning.salary_component == basic_component: + basic_amt = earning.amount + elif earning.salary_component == hra_component: + hra_amt = earning.amount + if basic_amt and hra_amt: + return basic_amt, hra_amt + return basic_amt, hra_amt + + +def calculate_hra_exemption(annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city): + # TODO make this configurable + exemptions = [] + # case 1: The actual amount allotted by the employer as the HRA. + exemptions.append(annual_hra) + + # case 2: Actual rent paid less 10% of the basic salary. + actual_annual_rent = monthly_house_rent * 12 + exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1)) + + # case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city). + exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4) + + # return minimum of 3 cases + return min(exemptions) + + +def get_component_pay(frequency, amount, from_date, to_date): + days = date_diff(to_date, from_date) + 1 + + if frequency == "Daily": + return amount * days + elif frequency == "Weekly": + return amount * math.floor(days / 7) + elif frequency == "Fortnightly": + return amount * math.floor(days / 14) + elif frequency == "Monthly": + return amount * month_diff(to_date, from_date) + elif frequency == "Bimonthly": + return amount * (month_diff(to_date, from_date) / 2) + + +def calculate_hra_exemption_for_period(doc): + monthly_rent, eligible_hra = 0, 0 + if doc.house_rent_payment_amount: + validate_house_rent_dates(doc) + # TODO receive rented months or validate dates are start and end of months? + # Calc monthly rent, round to nearest .5 + factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1) / 30 + factor = round(factor * 2) / 2 + monthly_rent = doc.house_rent_payment_amount / factor + # update field used by calculate_annual_eligible_hra_exemption + doc.monthly_house_rent = monthly_rent + exemptions = calculate_annual_eligible_hra_exemption(doc) + + if exemptions["monthly_exemption"]: + # calc total exemption amount + eligible_hra = exemptions["monthly_exemption"] * factor + exemptions["monthly_house_rent"] = monthly_rent + exemptions["total_eligible_hra_exemption"] = eligible_hra + return exemptions + + +def validate_house_rent_dates(doc): + if not doc.rented_to_date or not doc.rented_from_date: + frappe.throw(_("House rented dates required for exemption calculation")) + + if date_diff(doc.rented_to_date, doc.rented_from_date) < 14: + frappe.throw(_("House rented dates should be atleast 15 days apart")) + + proofs = frappe.db.sql( + """ + select name + from `tabEmployee Tax Exemption Proof Submission` + where + docstatus=1 and employee=%(employee)s and payroll_period=%(payroll_period)s + and (rented_from_date between %(from_date)s and %(to_date)s or rented_to_date between %(from_date)s and %(to_date)s) + """, + { + "employee": doc.employee, + "payroll_period": doc.payroll_period, + "from_date": doc.rented_from_date, + "to_date": doc.rented_to_date, + }, + ) + + if proofs: + frappe.throw(_("House rent paid days overlapping with {0}").format(proofs[0][0])) diff --git a/erpnext/regional/report/professional_tax_deductions/__init__.py b/erpnext/regional/report/professional_tax_deductions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js new file mode 100644 index 00000000000..bb75238b8c0 --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Professional Tax Deductions"] = erpnext.salary_slip_deductions_report_filters; +}); diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json new file mode 100644 index 00000000000..9938e9db524 --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.json @@ -0,0 +1,20 @@ +{ + "add_total_row": 0, + "creation": "2020-06-02 00:37:44.537355", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 19:02:26.306348", + "modified_by": "Administrator", + "module": "Regional", + "name": "Professional Tax Deductions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Professional Tax Deductions", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py new file mode 100644 index 00000000000..17a62d5e5da --- /dev/null +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe import _ + +from erpnext.regional.report.provident_fund_deductions.provident_fund_deductions import ( + get_conditions, +) + + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) if len(data) else [] + + return columns, data + + +def get_columns(filters): + columns = [ + { + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 200, + }, + { + "label": _("Employee Name"), + "options": "Employee", + "fieldname": "employee_name", + "fieldtype": "Link", + "width": 160, + }, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 140}, + ] + + return columns + + +def get_data(filters): + + data = [] + + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type = 'Professional Tax' """ + ) + ) + + if not len(component_type_dict): + return [] + + conditions = get_conditions(filters) + + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, ded.salary_component, ded.amount + from `tabSalary Slip` sal, `tabSalary Detail` ded + where sal.name = ded.parent + and ded.parentfield = 'deductions' + and ded.parenttype = 'Salary Slip' + and sal.docstatus = 1 %s + and ded.salary_component in (%s) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) + + for d in entry: + + employee = {"employee": d.employee, "employee_name": d.employee_name, "amount": d.amount} + + data.append(employee) + + return data diff --git a/erpnext/regional/report/provident_fund_deductions/__init__.py b/erpnext/regional/report/provident_fund_deductions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js new file mode 100644 index 00000000000..a91a30796bc --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.js @@ -0,0 +1,7 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.require("assets/erpnext/js/salary_slip_deductions_report_filters.js", function() { + frappe.query_reports["Provident Fund Deductions"] = erpnext.salary_slip_deductions_report_filters; +}); diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json new file mode 100644 index 00000000000..e25d335f9bd --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.json @@ -0,0 +1,20 @@ +{ + "add_total_row": 0, + "creation": "2020-06-01 23:44:07.919117", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-06-16 18:54:19.305763", + "modified_by": "Administrator", + "module": "Regional", + "name": "Provident Fund Deductions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Provident Fund Deductions", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py new file mode 100644 index 00000000000..ab4b6e73b83 --- /dev/null +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -0,0 +1,174 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe import _ +from frappe.utils import getdate + + +def execute(filters=None): + data = get_data(filters) + columns = get_columns(filters) if len(data) else [] + + return columns, data + + +def get_columns(filters): + columns = [ + { + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 200, + }, + { + "label": _("Employee Name"), + "options": "Employee", + "fieldname": "employee_name", + "fieldtype": "Link", + "width": 160, + }, + {"label": _("PF Account"), "fieldname": "pf_account", "fieldtype": "Data", "width": 140}, + {"label": _("PF Amount"), "fieldname": "pf_amount", "fieldtype": "Currency", "width": 140}, + { + "label": _("Additional PF"), + "fieldname": "additional_pf", + "fieldtype": "Currency", + "width": 140, + }, + {"label": _("PF Loan"), "fieldname": "pf_loan", "fieldtype": "Currency", "width": 140}, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}, + ] + + return columns + + +def get_conditions(filters): + conditions = [""] + + if filters.get("department"): + conditions.append("sal.department = '%s' " % (filters["department"])) + + if filters.get("branch"): + conditions.append("sal.branch = '%s' " % (filters["branch"])) + + if filters.get("company"): + conditions.append("sal.company = '%s' " % (filters["company"])) + + if filters.get("month"): + conditions.append("month(sal.start_date) = '%s' " % (filters["month"])) + + if filters.get("year"): + conditions.append("year(start_date) = '%s' " % (filters["year"])) + + if filters.get("mode_of_payment"): + conditions.append("sal.mode_of_payment = '%s' " % (filters["mode_of_payment"])) + + return " and ".join(conditions) + + +def prepare_data(entry, component_type_dict): + data_list = {} + + employee_account_dict = frappe._dict( + frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""") + ) + + for d in entry: + + component_type = component_type_dict.get(d.salary_component) + + if data_list.get(d.name): + data_list[d.name][component_type] = d.amount + else: + data_list.setdefault( + d.name, + { + "employee": d.employee, + "employee_name": d.employee_name, + "pf_account": employee_account_dict.get(d.employee), + component_type: d.amount, + }, + ) + + return data_list + + +def get_data(filters): + data = [] + + conditions = get_conditions(filters) + + salary_slips = frappe.db.sql( + """ select sal.name from `tabSalary Slip` sal + where docstatus = 1 %s + """ + % (conditions), + as_dict=1, + ) + + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""" + ) + ) + + if not len(component_type_dict): + return [] + + entry = frappe.db.sql( + """ select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount + from `tabSalary Slip` sal, `tabSalary Detail` ded + where sal.name = ded.parent + and ded.parentfield = 'deductions' + and ded.parenttype = 'Salary Slip' + and sal.docstatus = 1 %s + and ded.salary_component in (%s) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) + + data_list = prepare_data(entry, component_type_dict) + + for d in salary_slips: + total = 0 + if data_list.get(d.name): + employee = { + "employee": data_list.get(d.name).get("employee"), + "employee_name": data_list.get(d.name).get("employee_name"), + "pf_account": data_list.get(d.name).get("pf_account"), + } + + if data_list.get(d.name).get("Provident Fund"): + employee["pf_amount"] = data_list.get(d.name).get("Provident Fund") + total += data_list.get(d.name).get("Provident Fund") + + if data_list.get(d.name).get("Additional Provident Fund"): + employee["additional_pf"] = data_list.get(d.name).get("Additional Provident Fund") + total += data_list.get(d.name).get("Additional Provident Fund") + + if data_list.get(d.name).get("Provident Fund Loan"): + employee["pf_loan"] = data_list.get(d.name).get("Provident Fund Loan") + total += data_list.get(d.name).get("Provident Fund Loan") + + employee["total"] = total + + data.append(employee) + + return data + + +@frappe.whitelist() +def get_years(): + year_list = frappe.db.sql_list( + """select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""" + ) + if not year_list: + year_list = [getdate().year] + + return "\n".join(str(year) for year in year_list)