diff --git a/addons_extensions/l10n_in_hr_payroll/__init__.py b/addons_extensions/l10n_in_hr_payroll/__init__.py new file mode 100644 index 000000000..637b45b52 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/__init__.py @@ -0,0 +1,7 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import report +from . import wizard +from . import controller diff --git a/addons_extensions/l10n_in_hr_payroll/__manifest__.py b/addons_extensions/l10n_in_hr_payroll/__manifest__.py new file mode 100644 index 000000000..af506ce52 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/__manifest__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Indian Payroll', + 'countries': ['in'], + 'category': 'Human Resources/Payroll', + 'depends': ['hr_payroll'], + 'auto_install': ['hr_payroll'], + 'description': """ +Indian Payroll Salary Rules. +============================ + + -Configuration of hr_payroll for India localization + -All main contributions rules for India payslip. + * New payslip report + * Employee Contracts + * Allow to configure Basic / Gross / Net Salary + * Employee PaySlip + * Allowance / Deduction + * Integrated with Leaves Management + * Medical Allowance, Travel Allowance, Child Allowance, ... + - Payroll Advice and Report + - Yearly Salary by Employee Report + """, + 'data': [ + 'data/report_paperformat.xml', + 'views/l10n_in_hr_payroll_report.xml', + 'data/res_partner_data.xml', + 'data/hr_salary_rule_category_data.xml', + 'data/hr_payroll_structure_type_data.xml', + 'data/hr_payroll_structure_data.xml', + 'data/salary_rules/hr_salary_rule_stipend_data.xml', + 'data/salary_rules/hr_salary_rule_ind_emp_data.xml', + 'data/salary_rules/hr_salary_rule_regular_pay_data.xml', + 'data/salary_rules/hr_salary_rule_worker_data.xml', + 'data/hr_contract_type_data.xml', + 'data/hr_rule_parameters_data.xml', + 'data/ir_sequence_data.xml', + 'data/hr_payroll_dashboard_warning_data.xml', + 'wizard/hr_tds_calculation.xml', + 'views/hr_contract_views.xml', + 'views/res_users_views.xml', + 'views/hr_employee_views.xml', + 'views/res_config_settings_views.xml', + 'security/ir.model.access.csv', + 'views/report_payslip_details_template.xml', + 'wizard/hr_salary_register.xml', + 'views/report_hr_epf_views.xml', + 'wizard/hr_yearly_salary_detail_view.xml', + 'wizard/hr_payroll_payment_report.xml', + 'views/report_hr_yearly_salary_detail_template.xml', + 'views/report_payroll_advice_template.xml', + 'views/l10n_in_salary_statement.xml', + 'views/report_l10n_in_salary_statement.xml', + ], + 'demo': [ + 'data/l10n_in_hr_payroll_demo.xml', + ], + 'license': 'OEEL-1', +} diff --git a/addons_extensions/l10n_in_hr_payroll/controller/__init__.py b/addons_extensions/l10n_in_hr_payroll/controller/__init__.py new file mode 100644 index 000000000..80ee4da1c --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/controller/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons_extensions/l10n_in_hr_payroll/controller/main.py b/addons_extensions/l10n_in_hr_payroll/controller/main.py new file mode 100644 index 000000000..5bac0be80 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/controller/main.py @@ -0,0 +1,136 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from io import BytesIO +from copy import deepcopy + +from odoo import http, _ +from odoo.http import request +from odoo.tools.misc import xlsxwriter + + +class L10nInSalaryRegisterController(http.Controller): + + def _get_payslip_rules(self, employee_ids, payslips, struct_id=None): + rule_by_name = [] + if struct_id and struct_id.rule_ids: + rule_by_name = [(i.code, i.name) for i in struct_id.rule_ids] + else: + rule_by_name = [ + (rule.code, rule.name) + for payslip in payslips + for rule in payslip.struct_id.rule_ids + ] + child_dict = {code: [name, 0] for code, name in rule_by_name} + rules_per_employee = {employee_id: deepcopy(child_dict) for employee_id in employee_ids} + rules_by_name = dict(rule_by_name) + return rules_by_name, rules_per_employee + + @http.route(['/export/salary-register/'], type='http', auth='user') + def export_salary_register(self, wizard_id): + wizard = request.env['salary.register.wizard'].browse(wizard_id) + if not wizard.exists() or not request.env.user.has_group('hr_payroll.group_hr_payroll_user'): + return request.render( + 'http_routing.http_error', + { + 'status_code': 'Oops', + 'status_message': _('It seems that you either not have the rights to access the Salary Register ' + 'or that you try to access it outside normal circumstances. ' + 'If you think there is a problem, please contact an administrator.') + } + ) + output = BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + worksheet = workbook.add_worksheet('salary_register_report') + style_highlight = workbook.add_format({'bold': True, 'pattern': 1, 'bg_color': '#E0E0E0', 'align': 'center'}) + style_normal = workbook.add_format({'align': 'center'}) + column_width = 30 + + date_from = str(wizard.date_from) + date_to = str(wizard.date_to) + employee_ids = wizard.employee_ids + struct_id = wizard.struct_id + + # VERTICAL HEADERS + vertical_headers = [ + _('EMPLOYER ID'), + _('EMPLOYER NAME'), + _('FROM DATE'), + _('TO DATE'), + ] + # VERTICAL DATA + vertical_data = [ + wizard.company_id.company_registry or '', + wizard.company_id.name, + date_from, + date_to, + ] + blank_lines = 1 + + # HORIZONTAL HEADERS + horizontal_headers = [ + _('EMPLOYEE CODE'), + _('EMPLOYEE NAME'), + ] + + # HORIZONTAL DATA + domain = [('employee_id', 'in', employee_ids.ids), ('date_from', '>=', date_from), ('date_to', '<=', date_to), ('state', '=', 'paid')] + if struct_id: + domain.append(('struct_id', '=', struct_id.id)) + payslips_per_employee = dict(request.env['hr.payslip']._read_group( + domain=domain, + groupby=['employee_id'], + aggregates=['id:recordset'], + )) + rules_by_name, rules_per_employee = self._get_payslip_rules(employee_ids, payslips_per_employee.values(), struct_id=struct_id) + rules_per_employee = { + employee: rules + for employee, rules in rules_per_employee.items() + if employee in payslips_per_employee + } + # Dynamically calculated headers + horizontal_headers = [*horizontal_headers, *rules_by_name.values()] + for employee_id, payslips in payslips_per_employee.items(): + rule_codes = payslips.struct_id.rule_ids.mapped('code') + payslip_rules = payslips._get_line_values(rule_codes, compute_sum=True) + for code, rule in payslip_rules.items(): + rules_per_employee[employee_id][code][1] += rule['sum']['total'] + + horizontal_data = [] + for employee_id in rules_per_employee: + dynamic_horizontal_data = [data[1] for data in rules_per_employee[employee_id].values()] + horizontal_data.append(( + employee_id.registration_number or "", + employee_id.name, + *dynamic_horizontal_data, + )) + + # WRITE IN WORKSHEET + row = 0 + for (vertical_header, vertical_point) in zip(vertical_headers, vertical_data): + worksheet.write(row, 0, vertical_header, style_highlight) + worksheet.write(row, 1, vertical_point, style_normal) + row += 1 + + row += blank_lines + for col, horizontal_header in enumerate(horizontal_headers): + worksheet.write(row, col, horizontal_header, style_highlight) + worksheet.set_column(col, col, column_width) + + for payroll_line in horizontal_data: + row += 1 + for col, payroll_point in enumerate(payroll_line): + worksheet.write(row, col, payroll_point, style_normal) + + row += 1 + workbook.close() + xlsx_data = output.getvalue() + date_obj = datetime.strptime(date_from, '%Y-%m-%d').date() + filename = _("salary_register_report_%(year)s_%(month)s", year=date_obj.year, month=date_obj.month) + response = request.make_response( + xlsx_data, + headers=[ + ('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'), + ('Content-Disposition', f'attachment; filename={filename}.xlsx')], + ) + return response diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_contract_type_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_contract_type_data.xml new file mode 100644 index 000000000..216cd829f --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_contract_type_data.xml @@ -0,0 +1,13 @@ + + + + Probation + 4 + + + + Intern + 5 + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_dashboard_warning_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_dashboard_warning_data.xml new file mode 100644 index 000000000..8780895b9 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_dashboard_warning_data.xml @@ -0,0 +1,77 @@ + + + + + + Employees Without PAN Number + + +indian_companies = self.env.companies.filtered(lambda c: c.country_id.code == 'IN') +if indian_companies: + # Employees Without PAN Number + employees_wo_pan = self.env['hr.employee'].search([ + ('l10n_in_pan', '=', False), + ('company_id', 'in', indian_companies.ids), + ]) + if employees_wo_pan: + warning_count = len(employees_wo_pan) + warning_records = employees_wo_pan + + + + + Employees Without UAN Number + + +indian_companies = self.env.companies.filtered(lambda c: c.country_id.code == 'IN') +if indian_companies: + # Employees Without PAN Number + employees_wo_uan = self.env['hr.employee'].search([ + ('l10n_in_uan', '=', False), + ('company_id', 'in', indian_companies.ids), + ]) + if employees_wo_uan: + warning_count = len(employees_wo_uan) + warning_records = employees_wo_uan + + + + + Employees Without ESIC Number + + +indian_companies = self.env.companies.filtered(lambda c: c.country_id.code == 'IN') +if indian_companies: + # Employees Without PAN Number + employees_wo_esic = self.env['hr.employee'].search([ + ('l10n_in_esic_number', '=', False), + ('company_id', 'in', indian_companies.ids), + ]) + if employees_wo_esic: + warning_count = len(employees_wo_esic) + warning_records = employees_wo_esic + + + + + Employees Probation ends within a week + + +indian_companies = self.env.companies.filtered(lambda c: c.country_id.code == 'IN') +if indian_companies: + # Employees who are on the probation and their contracts expire within a week + probation_contract_type = self.env.ref('l10n_in_hr_payroll.l10n_in_contract_type_probation', raise_if_not_found=False) + if probation_contract_type: + nearly_expired_contracts = self.env['hr.contract'].search([ + ('contract_type_id', '=', probation_contract_type.id), + ('state', '=', 'open'), ('kanban_state', '!=', 'blocked'), + ('date_end', '<=', date.today() + relativedelta(days=7)), + ('date_end', '>=', date.today() + relativedelta(days=1)), + ]) + if nearly_expired_contracts: + warning_count = len(nearly_expired_contracts.employee_id) + warning_records = nearly_expired_contracts.employee_id + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_data.xml new file mode 100644 index 000000000..679e3d105 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_data.xml @@ -0,0 +1,46 @@ + + + + + India: Regular Pay + + + + + + + + Worker Pay + + + + + + + Stipend + + + + Stipend + + + + Non-Executive Employee + + + + + + + + + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_type_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_type_data.xml new file mode 100644 index 000000000..039859e1c --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_payroll_structure_type_data.xml @@ -0,0 +1,23 @@ + + + + hourly + + + + India: Employee Pay + + + + India: Intern + + + + India: Worker + + + + India: Non-Executives + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_rule_parameters_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_rule_parameters_data.xml new file mode 100644 index 000000000..4b155effd --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_rule_parameters_data.xml @@ -0,0 +1,304 @@ + + + + + India: Basic Salary Value + l10n_in_basic_value + + + + 7000 + + + + + + India: Basic Salary Percentage + l10n_in_basic_percent + + + + 0.60 + + + + + + India: Basic Salary Days + l10n_in_basic_days + + + + 0.31 + + + + + + India: House Rent Allowance Value + l10n_in_hra_value + + + + 0.40 + + + + + + India: Standard Allowance + l10n_in_std_alw + + + + 4167 + + + + + + India: Bonus Value and Percentage + l10n_in_bonus_percent + + + + [ + (450000.00, (12, 0.30)), + (550000.00, (3, 0.20)), + (0.00, (1, 0.10)), + ] + + + + + + India: TDS Rate Chart + l10n_in_tds_rate_chart + + + + [ + (0.0, (0, 300000)), + (0.05, (300000, 600000)), + (0.1, (600000, 900000)), + (0.15, (900000, 1200000)), + (0.2, (1200000, 1500000)), + (0.3, (1500000, 'inf')) + ] + + + + + + India: Surcharge Rate + l10n_in_surcharge_rate + + + + [ + (0.0, (0, 5000000)), + (0.1, (5000000, 10000000)), + (0.15, (10000000, 20000000)), + (0.25, (20000000, 'inf')), + ] + + + + + + India: Standard Deduction + l10n_in_standard_deduction + + + + 50000 + + + + + + India: Minimun Income for Tax Rebate + l10n_in_min_income_tax_rebate + + + + 700000 + + + + + + India: Miminum Income for Surcharge + l10n_in_min_income_surcharge + + + + 5000000 + + + + + + India: Maximum Tax Rate for Surcharge + l10n_in_max_surcharge_tax_rate + + + + [ + (5000000, 1200000, 0), + (10000000, 2700000, 270000), + (20000000, 5700000, 855000), + (50000000, 14700000, 3675000), + ] + + + + + + India: Leave Travel Allowance Value and Percentage + l10n_in_lta_percent + + + + [ + (450000.00, (12, 0.30)), + (550000.00, (3, 0.20)), + (0.00, (1, 0.10)), + ] + + + + + + India: Professional Tax + l10n_in_professional_tax + + + + [-200, -150, -80] + + + + + + India: Provident Fund Percentage + l10n_in_pf_percent + + + + 0.12 + + + + + + India: Provident Fund Value + l10n_in_pf_amount + + + + 15000.00 + + + + + + India: EPS Contri Percentage + l10n_in_eps_contri_percent + + + + 0.0833 + + + + + + India: Employer's State Insurance Corporation Percentage + l10n_in_esicf_percent + + + + 0.0325 + + + + + + India: City Compensatory Allowance Percentage + l10n_in_city_alw_percent + + + + 0.10 + + + + + + India: Child Education Allowance + l10n_in_chea_value + + + + [100, 200] + + + + + + India: Child Hostel Allowance Value + l10n_in_child_hostel_allowance + + + + [300, 600] + + + + + + India: Employer's State Insurance Corporation Value + l10n_in_esicf_value + + + + 21000 + + + + + + India: Leave Days + l10n_in_leave_days + + + + 22 + + + + + + India: India:Employee's NPS Contribution + l10n_in_cbda_percent + + + + 0.10 + + + + + + India: Performace Bonus Percentage + l10n_in_regular_bonus_percent + + + + 0.40 + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/hr_salary_rule_category_data.xml b/addons_extensions/l10n_in_hr_payroll/data/hr_salary_rule_category_data.xml new file mode 100644 index 000000000..285139ffb --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/hr_salary_rule_category_data.xml @@ -0,0 +1,20 @@ + + + + + Special Allowance + SPA + + + + Leave Allowance + LEAVE + + + + Performance Bonus + PBS + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/ir_sequence_data.xml b/addons_extensions/l10n_in_hr_payroll/data/ir_sequence_data.xml new file mode 100644 index 000000000..42fa6fd70 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/ir_sequence_data.xml @@ -0,0 +1,13 @@ + + + + + + Payment Advice + payment.advice + 3 + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/l10n_in_hr_payroll_demo.xml b/addons_extensions/l10n_in_hr_payroll/data/l10n_in_hr_payroll_demo.xml new file mode 100644 index 000000000..08865e478 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/l10n_in_hr_payroll_demo.xml @@ -0,0 +1,430 @@ + + + + + IN Company + Block no. 401 + Street 2 + Hyderabad + + + 500001 + +91 81234 56789 + + + + IN Company + + + 24DUMMY1234AAZA + + + + HDFC Bank + HDFC0000123 + + + + State Bank + SBIS0000321 + + + + + + + + + Vihaan Sengupta + A-4 Gunj Society + near AK Hospital + Hyderabad + 385426 + + + +91 98765 43021 + vihaan.sengupta@example.com + + + + Alisha Sharma + + + + + Shaurya Khurana + + + + + 1245670000123 + + + + + + + + + 0112340000998 + + + + + + + + + 0222340000789 + + + + + + + + + + vihaan@example.com + vihaan@123 + --
+V. Sengupta
+ + + + Asia/Kolkata + +
+ + + + Standard 24 hours/week + + 8 + + + + + Research & Development IN + + + + Technical Support & Investigation + + + + Marketing + + + + + Experienced Developer + + 5 + + + + Technical Support & Investigation Intern + + 10 + + + + + + Vihaan Sengupta + male + single + +91 8765432109 + + A-4 Gunj Society + near AK Hospital + Hyderabad + 385426 + + + +91 7654321098 + vihaan.sengupta123@example.com + + Software Developer + + Harshiv Sengupta + +91 9348762345 + India + + bachelor + Computer Engineering + TechInnova University + + + + 82735682375623 + + + + Father + 124567334654 + 93874947361284657 + HDIUE8765M + + + Shaurya Khurana + + male + + + single + +91 7890123456 + 503, highsky residency + VR Road + Ahmedabad + 385876 + + + +91 9870165432 + shaurya.khurana@example.com + India + + Asia/Kolkata + + + 82735682375623 + + + + + 387438790384 + 93487475300284657 + KUPYH9876I + + + Alisha Sharma + + + + female + Asia/Kolkata + +91 9887756789 + alisha.sharma@example.com + India + + + + + + + 398175628304 + 4658302649025064783 + GUNI5723P + + + + + Alisha Sharma + + + + + open + + + + + + Shaurya Contract + + + + hourly + 250 + + + + + open + + + + + + Vihaan Sengupta Contract + + + + 50000 + open + + 5000 + 1442.50 + 6550 + 12 + 12 + 6850 + 2750 + 1560 + + + + + + + Vihaan Sengupta Contract + + + + 40000 + close + + + + + 4000 + 1142 + 5100 + 12 + 16 + 5560 + 2000 + 1160 + + + + + Paid Time Off for Indian Employee 1 + + + + 24 + confirm + + + + + Paid Time Off for Indian Employee 2 + + + + 24 + confirm + + + + + Paid Time Off for Indian Employee 3 + + + + 24 + confirm + + + + + + + + 1 + + + confirm + + + + + + 0.5 + + + + confirm + + + + + + + + + + 5500 + 5500 + + + Health Insurance + + + + + + + + + + + + + + + + Vihaan Payslip September + + + + + + + + Shaurya Payslip September + + + + + + + + Vihaan Payslip October + + + + + + + + + Alisha Sharma November + + + + + + + + + + + + + +
+
diff --git a/addons_extensions/l10n_in_hr_payroll/data/report_paperformat.xml b/addons_extensions/l10n_in_hr_payroll/data/report_paperformat.xml new file mode 100644 index 000000000..b24941486 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/report_paperformat.xml @@ -0,0 +1,17 @@ + + + + Yealy Salary Head Report + + A4 + 0 + 0 + Landscape + 15 + 15 + 5 + 5 + + 96 + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/res_partner_data.xml b/addons_extensions/l10n_in_hr_payroll/data/res_partner_data.xml new file mode 100644 index 000000000..5eef7fdfe --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/res_partner_data.xml @@ -0,0 +1,54 @@ + + + + Register for House Rent Allowance + + + + Register for Provident Fund + + + + Register for Professional Tax + + + + Register for Food Coupen + + + + Register for TDS + + + + Register for NPS Contribution + + + + Register for Voluntary Provident Fund + + + + Register for Company Provided Transport Deduction + + + + Register for State Labour Welfare Fund Deduction + + + + Register for Company Provided Group Term Insurance Deduction + + + + Register for Leave Availed Deduction + + + + Register for Company Provided Medical Insurance Deduction + + + + Register for Other Deduction from Employer + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_ind_emp_data.xml b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_ind_emp_data.xml new file mode 100644 index 000000000..ad61926dd --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_ind_emp_data.xml @@ -0,0 +1,511 @@ + + + + HRAMN + code + +nonmetro_alw = contract.l10n_in_house_rent_allowance_metro_nonmetro / 100 +result = contract.wage * nonmetro_alw + + + House Rent Allowance + + + HRA is an allowance given by the employer to the employee for taking care of his rental or accommodation expenses. + + + + + SA + Grade/Special/Management/Supplementary Allowance + + python + result = bool(contract.l10n_in_supplementary_allowance) + code + result = contract.l10n_in_supplementary_allowance + + This allowance is normally given as an additional benefit to employees and is fully taxable. + + + + + CHEA + Child Education Allowance + + python + result = bool(employee.children) + code + +amounts = payslip._rule_parameter('l10n_in_chea_value') +if employee.children == 1: + result = amounts[0] +else: + result = amounts[1] + + + Per school going child 1200 per annum is non-taxable.Maximum for 2 children, so max 2400 per annum becomes non-taxable. + + + + + CHEAH + Child Hostel Allowance + + python + result = bool(employee.l10n_in_residing_child_hostel) + code + +amounts = payslip._rule_parameter('l10n_in_child_hostel_allowance') +if employee.l10n_in_residing_child_hostel == 1: + result = amounts[0] +else: + result = amounts[1] + + In case the children are in hostel, the exemption available for child. + + + + + + CBDA + City Compensatory Allowance + + none + + code + +city_alw_percent = payslip._rule_parameter('l10n_in_city_alw_percent') +result = contract.wage * city_alw_percent + + This allowance is paid to Employees who are posted in big cities. The purpose is to compensate the high cost of living in cities like Mumbai, Delhi, etc. However it is Fully Taxable. + + + + + CMETRO + City Allowance for Metro city + + none + + fix + 850.0 + + + + + CNMETRO + City Allowance for Non Metro city + + none + + fix + 800.0 + + + + + ARRE + Arrears + + code + +result = 'ARS' in inputs and inputs['ARS'].amount +result_name = 'ARS' in inputs and inputs['ARS'].name + + + + Generally arrears are fully taxable, but employee may claim exemption u/s 89(1). +One would need to compute income tax on the arrears if it would have been received in actual year. +Now difference of income tax between payment year and actual year would be allowed for deduction. + + + + + MEDA + Medical Reimbursement + + code + +result = 'MR' in inputs and inputs['MR'].amount +result_name = 'MR' in inputs and inputs['MR'].name + + + This component is on-taxable up to 15000 per year (or Rs 1500 per month) on producing medical bills. + + + + + fix + + 'WORK100' in worked_days and worked_days['WORK100'].number_of_days + FC + + Food Allowance + + + + + + + ESICF + Employer's State Insurance Corporation + + + python + result = result_rules['GROSS']['total'] <= payslip._rule_parameter('l10n_in_esicf_value') + code + +esicf_percent = payslip._rule_parameter('l10n_in_esicf_percent') +gross = categories['GROSS'] +result = gross * esicf_percent + + + + + + + + ERPF + Employer's PF Contribution + + code + +pf_ctb_percent = payslip._rule_parameter('l10n_in_pf_percent') +result = -contract.wage * pf_ctb_percent + + + + + Both the employees and employer contribute to the fund at the rate of 12% of the basic wages, dearness allowance and retaining allowance, if any, payable to employees per month. + + + + + PERJ + Book and Periodicals Allowance (BNP) + + code + +result = 'PJ' in inputs and inputs['PJ'].amount +result_name = 'PJ' in inputs and inputs['PJ'].name + + + Some employers may provide component for buying magazines, journals and books as a part of knowledge enhancement for business growth.This part would become non taxable on providing original bills. + + + + + UNIFS + Uniform/Dress Allowance for Senior Executive + + fix + + + Some sections of employees mat get allowance for purchase of office dress/uniform.In such case, the component would become non-taxable. + + + + + TELR + Telephone Reimbursement + + code + +result = 'TR' in inputs and inputs['TR'].amount +result_name = 'TR' in inputs and inputs['TR'].name + + + In some of the cases, companies may provide a component for telephone bills.Employees may provide actual phone usage bills to reimburse this component and make it non-taxable. + + + + + + PDA + Professional Development Allowance + + fix + + + + + + + CAR + Car Expenses Reimbursement + + code + +result = 'CEI' in inputs and inputs['CEI'].amount +result_name = 'CEI' in inputs and inputs['CEI'].name + + + In case company provides component for this and employee use self owned car for official and personal purposes, Rs 1800 per month would be non-taxable on showing bills for fuel or can maintenance. This amount would be Rs 2400 in case car is more capacity than 1600cc. + + + + + INT + Mobile and Internet Expense + + code + +result = 'IE' in inputs and inputs['IE'].amount +result_name = 'IE' in inputs and inputs['IE'].name + + + Employer may also provide reimbursement of Mobile and Internet Expense and thus this would become non taxable. + + + + + DRI + Driver Salary + + python + result = bool(contract.l10n_in_driver_salay) + fix + + + Rs. 900 per month (non taxable) + + + + + + + + EPMF + Employee's PF Contribution + + code + +pf_ctb_percent = payslip._rule_parameter('l10n_in_pf_percent') +result = -contract.wage * pf_ctb_percent + + Employer contribution does not become part of employee’s income and hence income tax is not payable on this part. + + + + + + + ENPFC + Employee's NPS Contribution + + code + +pf_ctb_percent = payslip._rule_parameter('l10n_in_cbda_percent') +result = -contract.wage * pf_ctb_percent + + + + Employee can claim deduction even of employer's contribution to NPS. + + + + + VPF + Voluntary Provident Fund Contribution + + python + result = bool(contract.l10n_in_voluntary_provident_fund) + code + +VPF = contract.l10n_in_voluntary_provident_fund +result = -contract.wage * VPF / 100 + + + + VPF is a safe option wherein you can contribute more than the PF ceiling of 12% that has been mandated by the government.This additional amount enjoys all the benefits of PF except that the employer is not liable to contribute any extra amount apart from 12%.An added advantage is that the interest rate is equal to the interest rate of PF and he withdrawal is tax free. Please note that the maximum contribution towards VPF is 100% of your Basic.The highest rate of interest (close to 9%) makes it a very attractive saving scheme. Because of these advantages many employees chose not to close their PF account even after getting employment else where other than India.Employees also get a major tax break on their entire contribution to the fund up to a ceiling of Rs. 70,000/- + + + + + CPT + Deduction for Company Provided Transport + + none + fix + + + + Company provided transport amount is based on company. + + + + + fix + + 'WORK100' in worked_days and worked_days['WORK100'].number_of_days + FD + + Deduction Towards Food Coupons + + + + + + + LWFE + Employee's Deduction Towards State Labour Welfare Fund + + fix + + + + The LWF is applicable to all the members of the organisation except the Management staff (Staffs having authority to sign on the cheque/official documents on behalf of the organisation). for e.x. Employee Contribution is Rs. 3.00 and Employer contribution Rs. 6.00 Total Rs 9.00 and deposited to the LWF office.It is half yearly contribution (June and December). + + + + + LWF + Employer's Deduction Towards State Labour Welfare Fund + + fix + + + + The LWF is applicable to all the members of the organisation except the Management staff (Staffs having authority to sign on the cheque/official documents on behalf of the organisation). for e.x. Employee Contribution is Rs. 3.00 and Employer contribution Rs. 6.00 Total Rs 9.00 and deposited to the LWF office.It is half yearly contribution (June and December). + + + + + CGTI + Deduction Towards Company Provided Group Term Insurance + + fix + + + + Group term insurance provides a solid foundation to a comprehensive employee benifit program,backed up by government asistance in the form of valuable tax incentives to both employees and employers. + + + + + DLA + Deduction Towards Leave Availed + + python + result = 'LAI' in inputs + code + +result = -inputs['LAI'].amount +result_name = inputs['LAI'].name + + + + + + CMT + Deduction Towards Company Provided Medical Insurance + + python + result = bool(contract.l10n_in_medical_insurance) + code + +result = -contract.l10n_in_medical_insurance + + + + + + + + + ODE + Other Deduction from Employer + + fix + + + + + + + + ENPC + Employer's NPS Contribution + + code + +pf_ctb_percent = payslip._rule_parameter('l10n_in_cbda_percent') +result = -contract.wage * pf_ctb_percent + + + + Any amount contributed by your employer to your NPS account is treated as part of your salary and is included in your income but you can claim deduction under Section 80C for this too.thus, effectively making it exempt from tax within the limit of 10% of your basic salary. This is very useful and tax efficient for you particularly if you fall in the maximum tax. + + + + + EPF + Employer's PF Contribution + + code + +pf_ctb_percent = payslip._rule_parameter('l10n_in_pf_percent') +result = -contract.wage * pf_ctb_percent + + + + + Both the employees and employer contribute to the fund at the rate of 12% of the basic wages, dearness allowance and retaining allowance, if any, payable to employees per month. + + + + + ESICS + Employer's State Insurance Corporation + + + python + result = bool(contract.l10n_in_esic_amount) + code + result = -contract.l10n_in_esic_amount + + + + + + EXPENSE + Refund Expenses + + python + result = 'EXPENSES' in inputs + code + +result = inputs['EXPENSES'].amount +result_name = inputs['EXPENSES'].name + + + + + + GRATUITY + Gratuity + + + python + result = bool(contract.l10n_in_gratuity) + code + result = -contract.l10n_in_gratuity + + + + + PF + Provident fund + + python + result = bool(contract.l10n_in_provident_fund) + code + +PF = payslip._rule_parameter('l10n_in_pf_percent') +result = -contract.wage * PF + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_regular_pay_data.xml b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_regular_pay_data.xml new file mode 100644 index 000000000..e67fcffda --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_regular_pay_data.xml @@ -0,0 +1,248 @@ + + + + + + + + + + TDS + Tax Deducted at Source + + python + result = bool(contract.l10n_in_tds) + code + result = -(contract.l10n_in_tds) + + + As per income tax rules, all payment which are taxable in nature should be done after deduction of taxes at the source itself. Hence employer compute income tax on salary payment and deduct it every month. This TDS is based on employee’s saving/investment declaration at the start of year. If investments for tax saving is not done, large amount may be deducted in last few months. + + + + + HRA + House Rent Allowance + + + code + +hra_value = payslip._rule_parameter('l10n_in_hra_value') +result = categories['BASIC'] * hra_value + + + + + + STD + Standard Allowance + + + 1 + code + +std_awl = payslip._rule_parameter('l10n_in_std_alw') +work_rate = 1 +if payslip.worked_days_line_ids: + total_days = sum(payslip.worked_days_line_ids.mapped('number_of_days')) + total_unpaid_days = worked_days['LEAVE90'].number_of_days if 'LEAVE90' in worked_days else 0 + work_rate = (total_days - total_unpaid_days) / total_days +result = std_awl * work_rate + + + + + + BONUS + Bonus + + + code + +bonus_percent = payslip._rule_parameter('l10n_in_bonus_percent') +result = 0 +for treshhold, coef in bonus_percent: + annual_wage = contract.wage * coef[0] + if annual_wage < treshhold: + result = categories['BASIC'] * coef[1] + break + + + + + + LTA + Leave Travel Allowance + + + code + +lta_percent = payslip._rule_parameter('l10n_in_lta_percent') +result = 0 +for treshhold, coef in lta_percent: + annual_wage = contract.wage * coef[0] + if annual_wage < treshhold: + result = categories['BASIC'] * coef[1] + break + + + + + + SPL + Supplementary Allowance + + + code + +basic_and_alw = categories['BASIC'] + categories['ALW'] +basic_in_per = payslip._rule_parameter('l10n_in_basic_percent') +result = payslip.paid_amount * basic_in_per - basic_and_alw + + + + + + P_BONUS + Performance Bonus + + + code + +basic_percent = payslip._rule_parameter('l10n_in_basic_percent') +regular_bonus_percent = payslip._rule_parameter('l10n_in_regular_bonus_percent') +worked_days_ratio = 1 +if payslip.worked_days_line_ids: + total_days = sum(payslip.worked_days_line_ids.mapped('number_of_days')) + total_unpaid_days = worked_days['LEAVE90'].number_of_days if 'LEAVE90' in worked_days else 0 + worked_days_ratio = (total_days - total_unpaid_days) / total_days +bonus_base_ded = contract.wage * basic_percent * 2 / payslip._rule_parameter('l10n_in_leave_days') + contract.l10n_in_gratuity +bonus_base = contract.wage * regular_bonus_percent - bonus_base_ded +result = bonus_base * worked_days_ratio + + + + + + + + + + + LEAVE + Leave Allowance + + + python + result = bool(contract.l10n_in_leave_allowance) + code + result = contract.l10n_in_leave_allowance + + False + + + + PT + Professional Tax + + + code + +amounts = payslip._rule_parameter('l10n_in_professional_tax') +if categories['GROSS'] >= 12000: + result = amounts[0] +elif categories['GROSS'] >= 9000 and categories['GROSS'] < 12000: + result = amounts[1] +elif categories['GROSS'] >= 6000 and categories['GROSS'] < 9000: + result = amounts[2] +else: + result = 0 + + + + + + PF + Provident fund - Employee + + + python + result = bool(contract.l10n_in_provident_fund) + code + +total_days = sum(payslip.worked_days_line_ids.mapped('number_of_days')) +total_worked_days = total_days - ('LEAVE90' in worked_days and worked_days['LEAVE90'].number_of_days) +result = - categories['BASIC'] * payslip._rule_parameter('l10n_in_pf_percent') if ((categories['BASIC']) < 15000) else -(1800/total_days) * total_worked_days + + + + + + PFE + Provident fund - Employer + + + python + result = bool(contract.l10n_in_provident_fund) + code + +total_days = sum(payslip.worked_days_line_ids.mapped('number_of_days')) +total_worked_days = total_days - ('LEAVE90' in worked_days and worked_days['LEAVE90'].number_of_days) +result = - categories['BASIC'] * payslip._rule_parameter('l10n_in_pf_percent') if ((categories['BASIC']) < 15000) else -(1800/total_days) * total_worked_days + + + + + + ATTACH_SALARY + Attachment of Salary + + + python + result = 'ATTACH_SALARY' in inputs + code + +result = -inputs['ATTACH_SALARY'].amount +result_name = inputs['ATTACH_SALARY'].name + + + + + + ASSIG_SALARY + Assignment of Salary + + + code + python + result = 'ASSIG_SALARY' in inputs + +result = -inputs['ASSIG_SALARY'].amount +result_name = inputs['ASSIG_SALARY'].name + + + + + Expenses Reimbursement + EXPENSES + + + python + result = inputs['EXPENSES'].amount > 0.0 if 'EXPENSES' in inputs else False + code + +result = inputs['EXPENSES'].amount +result_name = inputs['EXPENSES'].name + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_stipend_data.xml b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_stipend_data.xml new file mode 100644 index 000000000..460431122 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_stipend_data.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_worker_data.xml b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_worker_data.xml new file mode 100644 index 000000000..7709ab313 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/data/salary_rules/hr_salary_rule_worker_data.xml @@ -0,0 +1,50 @@ + + + + TCA + Transport/Conveyance Allownace + + fix + + + A conveyance allowance refers to an amount of money reimbursed to someone for the operation of a vehicle or the riding of a vehicle. The allowance is typically a designated amount or percentage of total transportation expenses that is referenced in a country's tax laws or code. Organizations and private or public businesses may also offer a conveyance allowance in addition to reimbursing employees or members for transportation expenses. In this instance, the conveyance allowance may identify an unusual transport occurrence that may not be covered by a designated travel expense report such as travel to a specific job site that requires a daily bus or taxi ride. + + + + + SA + Special + + python + result = bool(contract.l10n_in_supplementary_allowance) + code + result = contract.l10n_in_supplementary_allowance + + This allowance is normally given as an additional benefit to worker and is fully taxable. + + + + + PTD + Professional Tax + + python + result = categories['GROSS'] >= 3000 + code + +amounts = payslip._rule_parameter('l10n_in_professional_tax') +if categories['GROSS'] >= 12000: + result = amounts[0] +elif categories['GROSS'] >= 9000 and categories < 11999: + result = amounts[1] +elif categories['GROSS'] >= 6000 and categories['GROSS'] <= 8999: + result = amounts[2] +else: + result = 0 + + + + Workers living in states that impose the professional tax must submit a payment each half-year for the right to practice a profession or trade. It applies equally to employees who work for the national or state government, and those employed by private corporations. The professional tax uses a six-month accounting system, which divides the year into two periods, beginning on April 1 and October 1. + + + diff --git a/addons_extensions/l10n_in_hr_payroll/i18n/l10n_in_hr_payroll.pot b/addons_extensions/l10n_in_hr_payroll/i18n/l10n_in_hr_payroll.pot new file mode 100644 index 000000000..149c5d733 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/i18n/l10n_in_hr_payroll.pot @@ -0,0 +1,26 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_in_hr_payroll +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~16.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-06-05 06:08+0000\n" +"PO-Revision-Date: 2023-06-05 06:08+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_in_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_in_hr_payroll.report_payslip_details_template +msgid "Employee Code" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model_terms:ir.ui.view,arch_db:l10n_in_hr_payroll.report_payslip_details_template +msgid "Joining Date" +msgstr "" diff --git a/addons_extensions/l10n_in_hr_payroll/models/__init__.py b/addons_extensions/l10n_in_hr_payroll/models/__init__.py new file mode 100644 index 000000000..fe3977d69 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/__init__.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import hr_employee +from . import res_users +from . import hr_contract +from . import hr_payslip +from . import hr_payslip_run +from . import res_company +from . import res_config_settings +from . import l10n_in_salary_statement diff --git a/addons_extensions/l10n_in_hr_payroll/models/hr_contract.py b/addons_extensions/l10n_in_hr_payroll/models/hr_contract.py new file mode 100644 index 000000000..7fea3230b --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/hr_contract.py @@ -0,0 +1,47 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from odoo import api, fields, models, _ + + +class HrContract(models.Model): + """ + Employee contract allows to add different values in fields. + Fields are used in salary rule computation. + """ + _inherit = 'hr.contract' + + l10n_in_tds = fields.Float(string='TDS', digits='Payroll', + help='Amount for Tax Deduction at Source') + l10n_in_driver_salay = fields.Boolean(string='Driver Salary', help='Check this box if you provide allowance for driver') + l10n_in_medical_insurance = fields.Float(string='Medical Insurance', digits='Payroll', + help='Deduction towards company provided medical insurance') + l10n_in_provident_fund = fields.Boolean(string='Provident Fund', default=False, + help='Check this box if you contribute for PF') + l10n_in_voluntary_provident_fund = fields.Float(string='Voluntary Provident Fund (%)', digits='Payroll', + help='VPF is a safe option wherein you can contribute more than the PF ceiling of 12% that has been mandated by the government and VPF computed as percentage(%)') + l10n_in_house_rent_allowance_metro_nonmetro = fields.Float(string='House Rent Allowance (%)', digits='Payroll', + help='HRA is an allowance given by the employer to the employee for taking care of his rental or accommodation expenses for metro city it is 50% and for non metro 40%. \nHRA computed as percentage(%)') + l10n_in_supplementary_allowance = fields.Float(string='Supplementary Allowance', digits='Payroll') + l10n_in_gratuity = fields.Float(string='Gratuity') + l10n_in_esic_amount = fields.Float(string='ESIC Amount', digits='Payroll', + help='Deduction towards company provided ESIC Amount') + l10n_in_leave_allowance = fields.Float(string='Leave Allowance', digits='Payroll', + help='Deduction towards company provided Leave Allowance') + + @api.model + def update_state(self): + contract_type_id = self.env.ref('l10n_in_hr_payroll.l10n_in_contract_type_probation', raise_if_not_found=False) + if contract_type_id: + one_week_ago = fields.Date.today() - timedelta(weeks=1) + contracts = self.env['hr.contract'].search([ + ('date_end', '=', one_week_ago), ('state', '=', 'open'), ('contract_type_id', '=', contract_type_id.id) + ]) + for contract in contracts: + contract.activity_schedule( + 'note.mail_activity_data_reminder', + user_id=contract.hr_responsible_id.id, + note=_("End date of %(name)s's contract is today.", name=contract.employee_id.name), + ) + return super().update_state() diff --git a/addons_extensions/l10n_in_hr_payroll/models/hr_employee.py b/addons_extensions/l10n_in_hr_payroll/models/hr_employee.py new file mode 100644 index 000000000..4d2f352e3 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/hr_employee.py @@ -0,0 +1,24 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re + +from odoo import fields, models + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + l10n_in_uan = fields.Char(string='UAN', groups="hr.group_hr_user") + l10n_in_pan = fields.Char(string='PAN', groups="hr.group_hr_user") + l10n_in_esic_number = fields.Char(string='ESIC Number', groups="hr.group_hr_user") + l10n_in_relationship = fields.Char("Relationship", groups="hr.group_hr_user", tracking=True) + l10n_in_residing_child_hostel = fields.Integer("Child Residing in hostel", groups="hr.group_hr_user", tracking=True) + + _sql_constraints = [ + ('unique_l10n_in_uan', 'unique (l10n_in_uan)', 'This UAN already exists'), + ('unique_l10n_in_pan', 'unique (l10n_in_pan)', 'This PAN already exists'), + ('unique_l10n_in_esic_number', 'unique (l10n_in_esic_number)', 'This ESIC Number already exists'), + ] + + def _get_employees_with_invalid_ifsc(self): + return self.filtered(lambda emp: not bool(re.match("^[A-Z]{4}0[A-Z0-9]{6}$", emp.bank_account_id.bank_bic))) diff --git a/addons_extensions/l10n_in_hr_payroll/models/hr_payslip.py b/addons_extensions/l10n_in_hr_payroll/models/hr_payslip.py new file mode 100644 index 000000000..8a59108e2 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/hr_payslip.py @@ -0,0 +1,57 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, time + +from odoo import api, models, _ +from odoo.tools import format_date, date_utils + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + def _get_l10n_in_company_working_time(self, return_hours=False): + self.ensure_one() + slip_date_time = datetime.combine(self.date_from, time(12, 0, 0)) + company_work_data = self.company_id.resource_calendar_id.get_work_duration_data( + date_utils.start_of(slip_date_time, 'month'), + date_utils.end_of(slip_date_time, 'month')) + if return_hours: + return company_work_data['hours'] + return company_work_data['days'] + + @api.depends('employee_id', 'struct_id', 'date_from') + def _compute_name(self): + super()._compute_name() + for slip in self.filtered(lambda s: s.country_code == 'IN'): + lang = slip.employee_id.lang or self.env.user.lang + payslip_name = slip.struct_id.payslip_name or _('Salary Slip') + date = format_date(self.env, slip.date_from, date_format="MMMM y", lang_code=lang) + if slip.number: + slip.name = '%(payslip_name)s - %(slip_ref)s - %(dates)s' % { + 'slip_ref': slip.number, + 'payslip_name': payslip_name, + 'dates': date + } + else: + slip.name = '%(payslip_name)s - %(dates)s' % { + 'payslip_name': payslip_name, + 'dates': date + } + + def _get_data_files_to_update(self): + # Note: file order should be maintained + return super()._get_data_files_to_update() + [( + 'l10n_in_hr_payroll', [ + 'data/hr_salary_rule_category_data.xml', + 'data/hr_payroll_structure_type_data.xml', + 'data/hr_rule_parameters_data.xml', + 'data/salary_rules/hr_salary_rule_ind_emp_data.xml', + 'data/salary_rules/hr_salary_rule_regular_pay_data.xml', + 'data/salary_rules/hr_salary_rule_worker_data.xml', + ])] + + def _get_base_local_dict(self): + return {**super()._get_base_local_dict(), '_': self.env._} + + def _get_employee_timeoff_data(self): + return self.env['hr.leave.type'].with_company(self.company_id).with_context(employee_id=self.employee_id.id).get_allocation_data_request() diff --git a/addons_extensions/l10n_in_hr_payroll/models/hr_payslip_run.py b/addons_extensions/l10n_in_hr_payroll/models/hr_payslip_run.py new file mode 100644 index 000000000..28b5e5af7 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/hr_payslip_run.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, _ + + +class HrPayslipRun(models.Model): + _inherit = 'hr.payslip.run' + + def action_payment_report(self, export_format='advice'): + self.ensure_one() + return { + 'name': _('Create Payment'), + 'type': 'ir.actions.act_window', + 'res_model': 'hr.payroll.payment.report.wizard', + 'view_mode': 'form', + 'view_id': 'hr_payslip_payment_report_view_form', + 'views': [(False, 'form')], + 'target': 'new', + 'context': { + 'default_payslip_ids': self.slip_ids.ids, + 'default_payslip_run_id': self.id, + 'default_export_format': export_format, + 'default_date': self.date_end, + 'dialog_size': 'medium', + }, + } diff --git a/addons_extensions/l10n_in_hr_payroll/models/l10n_in_salary_statement.py b/addons_extensions/l10n_in_hr_payroll/models/l10n_in_salary_statement.py new file mode 100644 index 000000000..0fd72f265 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/l10n_in_salary_statement.py @@ -0,0 +1,107 @@ +import calendar + +from odoo import api, fields, models, Command, _ +from dateutil.relativedelta import relativedelta +from collections import defaultdict +from datetime import datetime + + +class L10nInSalaryStatement(models.Model): + _name = 'l10n_in_hr_payroll.salary.statement' + _inherit = 'hr.payroll.declaration.mixin' + _description = 'Salary Statement Report' + + name = fields.Char(string="Description", required=True, compute='_compute_name', readonly=False, store=True) + month = fields.Selection([ + ('1', 'January'), + ('2', 'February'), + ('3', 'March'), + ('4', 'April'), + ('5', 'May'), + ('6', 'June'), + ('7', 'July'), + ('8', 'August'), + ('9', 'September'), + ('10', 'October'), + ('11', 'November'), + ('12', 'December'), + ], required=True, default=lambda self: str((fields.Date.today() + relativedelta(months=-1)).month)) + + def default_get(self, fields_list): + res = super().default_get(fields_list) + res['year'] = str(datetime.now().year) + return res + + @api.depends('year', 'month') + def _compute_name(self): + for sheet in self: + month_name = calendar.month_name[int(sheet.month)] + sheet.name = _('Salary Statement - %(month)s, %(year)s', month=month_name, year=sheet.year) + + def action_generate_declarations(self): + for sheet in self: + date_from = datetime(int(sheet.year), int(sheet.month), 1) + date_to = date_from + relativedelta(months=1, days=-1) + employees = self.env['hr.payslip'].search([ + ('date_to', '<=', date_to), + ('date_from', '>=', date_from), + ('state', 'in', ['done', 'paid']), + ('company_id', '=', sheet.company_id.id), + ]).mapped('employee_id') + + sheet.write({ + 'line_ids': [Command.clear()] + [Command.create({ + 'employee_id': employee.id, + 'res_model': 'l10n_in_hr_payroll.salary.statement', + 'res_id': sheet.id, + }) for employee in employees] + }) + return super().action_generate_declarations() + + def _country_restriction(self): + return 'IN' + + def _get_pdf_report(self): + return self.env.ref('l10n_in_hr_payroll.action_report_salary_statement') + + def _get_rendering_data(self, employees): + self.ensure_one() + date_from = datetime(int(self.year), int(self.month), 1) + date_to = date_from + relativedelta(months=1) + payslips = self.env['hr.payslip'].search([ + ('employee_id', 'in', employees.ids), + ('state', 'in', ['done', 'paid']), + ('date_from', '>=', date_from), + ('date_to', '<=', date_to), + ]) + + result = defaultdict(lambda: { + 'month': calendar.month_name[int(self.month)], + 'year': self.year, + 'allow_rules': defaultdict(lambda: {'name': '', 'total': 0, 'total_annual': 0}), + 'deduct_rules': defaultdict(lambda: {'name': '', 'total': 0, 'total_annual': 0}), + 'ctc': 0, + 'ctc_annual': 0 + }) + + for line in payslips.line_ids.filtered(lambda l: l.salary_rule_id.appears_on_payslip): + employee_id = line.employee_id + rule_category = result[employee_id]['deduct_rules' if line.total < 0 else 'allow_rules'][line.salary_rule_id] + + rule_category['name'] = line.salary_rule_id.name + rule_category['total'] = line.total + rule_category['total_annual'] = line.total * 12 + + if line.code == 'GROSS' or line.total < 0: + total = abs(line.total) + result[employee_id]['ctc'] += total + result[employee_id]['ctc_annual'] += total * 12 + result[employee_id]['date'] = line.date_from.strftime('%d/%m/%Y') + + result = dict(result) + return result + + def _get_pdf_filename(self, employee): + self.ensure_one() + month_name = calendar.month_name[int(self.month)] + return _('%(employee_name)s-salary-statement-report-%(month)s-%(year)s', employee_name=employee.name, month=month_name, year=self.year) diff --git a/addons_extensions/l10n_in_hr_payroll/models/res_company.py b/addons_extensions/l10n_in_hr_payroll/models/res_company.py new file mode 100644 index 000000000..c39f14032 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/res_company.py @@ -0,0 +1,16 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + +class ResCompany(models.Model): + _inherit = 'res.company' + + l10n_in_dearness_allowance = fields.Boolean(string='Dearness Allowance', default=True, + help='Check this box if your company provide Dearness Allowance to employee') + l10n_in_epf_employer_id = fields.Char(string="EPF Employer ID", + help="Code of 10 numbers. The first seven numbers represent the establishment ID.\n Next three numbers represent the extension code.") + l10n_in_esic_ip_number = fields.Char(string="ESIC IP Number", + help="Code of 17 digits.\n The Identification number is assigned to the company if registered under the Indian provisions of the Employee\'s State Insurance (ESI) Act.") + l10n_in_pt_number = fields.Char(string="PT Number", + help="Code of 11 digit.\n The P TIN digit number with the first two digits indicating the State.") + l10n_in_is_statutory_compliance = fields.Boolean(string="Statutory Compliance") diff --git a/addons_extensions/l10n_in_hr_payroll/models/res_config_settings.py b/addons_extensions/l10n_in_hr_payroll/models/res_config_settings.py new file mode 100644 index 000000000..f799311f2 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + l10n_in_dearness_allowance = fields.Boolean( + string="Dearness Allowance", related='company_id.l10n_in_dearness_allowance', readonly=False) + l10n_in_epf_employer_id = fields.Char(related='company_id.l10n_in_epf_employer_id', readonly=False) + l10n_in_esic_ip_number = fields.Char(related='company_id.l10n_in_esic_ip_number', readonly=False) + l10n_in_pt_number = fields.Char(related='company_id.l10n_in_pt_number', readonly=False) + l10n_in_is_statutory_compliance = fields.Boolean(related='company_id.l10n_in_is_statutory_compliance', readonly=False) diff --git a/addons_extensions/l10n_in_hr_payroll/models/res_users.py b/addons_extensions/l10n_in_hr_payroll/models/res_users.py new file mode 100644 index 000000000..7cc8e695b --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/models/res_users.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + +class User(models.Model): + _inherit = 'res.users' + + l10n_in_relationship = fields.Char(related='employee_id.l10n_in_relationship', readonly=False, related_sudo=False) + + @property + def SELF_READABLE_FIELDS(self): + return super().SELF_READABLE_FIELDS + ['l10n_in_relationship'] + + @property + def SELF_WRITEABLE_FIELDS(self): + return super().SELF_WRITEABLE_FIELDS + ['l10n_in_relationship'] diff --git a/addons_extensions/l10n_in_hr_payroll/report/__init__.py b/addons_extensions/l10n_in_hr_payroll/report/__init__.py new file mode 100644 index 000000000..495a2479c --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/report/__init__.py @@ -0,0 +1,6 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import report_payroll_advice +from . import report_hr_yearly_salary_detail +from . import report_hr_epf diff --git a/addons_extensions/l10n_in_hr_payroll/report/report_hr_epf.py b/addons_extensions/l10n_in_hr_payroll/report/report_hr_epf.py new file mode 100644 index 000000000..e735a7c07 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/report/report_hr_epf.py @@ -0,0 +1,149 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import io +import calendar +from odoo import api, fields, models +from odoo.tools.misc import xlsxwriter + +MONTH_SELECTION = [ + ('1', 'January'), + ('2', 'February'), + ('3', 'March'), + ('4', 'April'), + ('5', 'May'), + ('6', 'June'), + ('7', 'July'), + ('8', 'August'), + ('9', 'September'), + ('10', 'October'), + ('11', 'November'), + ('12', 'December'), +] + + +class HrEPFReport(models.Model): + _name = 'l10n.in.hr.payroll.epf.report' + _description = 'Indian Payroll: Employee Provident Fund Report' + + month = fields.Selection(MONTH_SELECTION, default='1', required=True) + year = fields.Integer(required=True, default=lambda self: fields.Date.context_today(self).year) + xls_file = fields.Binary(string="XLS file") + xls_filename = fields.Char() + + @api.depends('month', 'year') + def _compute_display_name(self): + month_description = dict(self._fields['month']._description_selection(self.env)) + for report in self: + report.display_name = f"{month_description.get(report.month)}-{report.year}" + + @api.model + def _get_employee_pf_data(self, year, month): + # Get the relevant records based on the year and month + indian_employees = self.env['hr.employee'].search([('contract_id.l10n_in_provident_fund', '=', True)]).filtered(lambda e: e.company_country_code == 'IN') + + result = [] + end_date = calendar.monthrange(year, int(month))[1] + + payslips = self.env['hr.payslip'].search([ + ('employee_id', 'in', indian_employees.ids), + ('date_from', '>=', f'{year}-{month}-1'), + ('date_to', '<=', f'{year}-{month}-{end_date}'), + ('state', 'in', ('done', 'paid')) + ]) + + if not payslips: + return [] + + payslip_line_values = payslips._get_line_values(['GROSS', 'BASIC', 'PF']) + + for employee in indian_employees: + + wage = 0 + epf = 0 + eps = 0 + epf_contri = 0 + + payslip_ids = payslips.filtered(lambda p: p.employee_id == employee) + + if not payslip_ids: + continue + + for payslip in payslip_ids: + pf_value = payslip_line_values['PF'][payslip.id]['total'] + if pf_value == 0: + continue + + epf_contri -= pf_value + wage += payslip_line_values['GROSS'][payslip.id]['total'] + epf += payslip_line_values['BASIC'][payslip.id]['total'] + + # Skip the employee if there are no valid PF contributions + if epf_contri == 0: + continue + + # Calculate contributions and differences + eps = min(payslip_ids[0]._rule_parameter('l10n_in_pf_amount'), epf) + eps_contri = round(eps * payslip_ids[0]._rule_parameter('l10n_in_eps_contri_percent'), 2) + diff = round(epf_contri - eps_contri, 2) + + result.append(( + employee.l10n_in_uan, + employee.name, + wage, + epf, + eps, + eps, + epf_contri, + eps_contri, + diff, + 0, 0, + )) + + return result + + def action_export_xlsx(self): + self.ensure_one() + + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + worksheet = workbook.add_worksheet('Employee_provident_fund_report') + style_highlight = workbook.add_format({'bold': True, 'pattern': 1, 'bg_color': '#E0E0E0', 'align': 'center'}) + style_normal = workbook.add_format({'align': 'center', 'font_size': 12}) + row = 0 + worksheet.set_row(row, 20) + + headers = [ + "UAN", + "MEMBER NAME", + "GROSS WAGES", + "EPF WAGES", + "EPS WAGES", + "EDLI WAGES", + "EPF CONTRIBUTION REMITTED", + "EPS CONTRIBUTION REMITTED", + "EPF EPS DIFFERENCE REMITTED", + "NCP DAYS", + "REFUNDED OF ADVANCES" + ] + + rows = self._get_employee_pf_data(self.year, self.month) + + for col, header in enumerate(headers): + worksheet.write(row, col, header, style_highlight) + worksheet.set_column(col, col, 30) + + row = 1 + for data_row in rows: + col = 0 + worksheet.set_row(row, 20) + for data in data_row: + worksheet.write(row, col, data, style_normal) + col += 1 + row += 1 + + workbook.close() + xlsx_data = output.getvalue() + + self.xls_file = base64.encodebytes(xlsx_data) + self.xls_filename = f"{self.display_name} EPF Report.xlsx" diff --git a/addons_extensions/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py b/addons_extensions/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py new file mode 100644 index 000000000..a12f3f8b6 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/report/report_hr_yearly_salary_detail.py @@ -0,0 +1,149 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date +from dateutil.relativedelta import relativedelta + + +from odoo import api, fields, models, _ +from odoo.tools import SQL +from odoo.exceptions import UserError + +class EmployeesYearlySalaryReport(models.AbstractModel): + _name = 'report.l10n_in_hr_payroll.report_hryearlysalary' + _description = "Indian Yearly Salary Report" + + # YTI: This mess deserves a good cleaning + + def _get_periods_new(self, form): + months = [] + # Get start year-month-date and end year-month-date + first_year = int(form['year']) + first_month = 1 + + # Get name of the months from integer + month_name = [] + for count in range(0, 12): + m = date(first_year, first_month, 1).strftime('%b') + month_name.append(m) + months.append(f"{first_month:02d}-{first_year}") + if first_month == 12: + first_month = 0 + first_year += 1 + first_month += 1 + return [month_name], months + + def _get_employee(self, form): + return self.env['hr.employee'].browse(form.get('employee_ids', [])) + + def _get_employee_detail_new(self, form, employee_id, months, date_from, date_to): + structures_data = {} + payslip_lines = self._cal_monthly_amt(form, employee_id, months, date_from, date_to) + for structure_name, payslip_data in payslip_lines.items(): + allow_list = [] + deduct_list = [] + total = 0.0 + gross = False + net = False + for line in payslip_data: + code = line[0] + subline = line[1:] + if code == "GROSS": + gross = [code, subline] + elif code == "NET": + net = [code, subline] + elif subline[-1] > 0.0 and code != "NET": + total += subline[-1] + allow_list.append([code, subline]) + elif subline[-1] < 0.0: + total += subline[-1] + deduct_list.append([code, subline]) + + if gross: + allow_list.append(gross) + if net: + deduct_list.append(net) + + structures_data[structure_name] = { + 'allow_list': allow_list, + 'deduct_list': deduct_list, + 'total': total, + } + + return structures_data + + def _cal_monthly_amt(self, form, emp_id, months, date_from, date_to): + result = {} + salaries = {} + + self.env.cr.execute(SQL( + """ + SELECT src.code, pl.name, sum(pl.total), to_char(p.date_to,'mm-yyyy') as to_date, ps.name + FROM hr_payslip_line as pl + LEFT JOIN hr_salary_rule AS sr on sr.id = pl.salary_rule_id + LEFT JOIN hr_salary_rule_category AS src on (sr.category_id = src.id) + LEFT JOIN hr_payslip as p on pl.slip_id = p.id + LEFT JOIN hr_employee as e on e.id = p.employee_id + LEFT JOIN hr_payroll_structure as ps on ps.id = p.struct_id + WHERE p.employee_id = %(employee_id)s + AND p.state = 'paid' + AND p.date_from >= %(date_from)s AND p.date_to <= %(date_to)s + GROUP BY src.parent_id, pl.sequence, pl.id, sr.category_id, pl.name, p.date_to, src.code, ps.name + ORDER BY pl.sequence, src.parent_id + """, employee_id=emp_id, date_from=date_from, date_to=date_to + )) + + for category_code, item_name, amount, payslip_date, structure_name in self.env.cr.fetchall(): + salaries.setdefault(structure_name, {}).setdefault(category_code, {}).setdefault(item_name, {}).setdefault(payslip_date, 0.0) + salaries[structure_name][category_code][item_name][payslip_date] += amount + + result = {key: self.salary_list(value, months) for key, value in salaries.items()} + return result + + def salary_list(self, salaries, months): + cat_salary_all = [] + for code, category_amount in salaries.items(): + for category_name, amount in category_amount.items(): + cat_salary = [code, category_name] + total = 0.0 + for month in months: + if month != 'None': + if len(month) != 7: + month = '0' + str(month) + if amount.get(month): + cat_salary.append(amount[month]) + total += amount[month] + else: + cat_salary.append(0.00) + else: + cat_salary.append('') + cat_salary.append(total) + cat_salary_all.append(cat_salary) + return cat_salary_all + + @api.model + def _get_report_values(self, docids, data=None): + if not self.env.context.get('active_model') or not self.env.context.get('active_id'): + raise UserError(_("Form content is missing, this report cannot be printed.")) + + model = self.env.context.get('active_model') + docs = self.env[model].browse(self.env.context.get('active_id')) + employees = self._get_employee(data['form']) + month_name, months = self._get_periods_new(data['form']) + date_from = fields.Date.today() + relativedelta(day=1, month=1, year=int(data['form']['year'])) + date_to = fields.Date.today() + relativedelta(day=31, month=12, year=int(data['form']['year'])) + + employee_data = {} + for employee in employees: + structures_data = self._get_employee_detail_new(data['form'], employee.id, months, date_from, date_to) + employee_data[employee.id] = structures_data + return { + 'doc_ids': docids, + 'doc_model': model, + 'data': data, + 'docs': docs, + 'date_from': date_from, + 'date_to': date_to, + 'get_employee': self._get_employee, + 'get_periods': lambda form: month_name, + 'get_employee_detail_new': lambda emp_id: employee_data.get(emp_id, {}), + } diff --git a/addons_extensions/l10n_in_hr_payroll/report/report_payroll_advice.py b/addons_extensions/l10n_in_hr_payroll/report/report_payroll_advice.py new file mode 100644 index 000000000..164384c4f --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/report/report_payroll_advice.py @@ -0,0 +1,34 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import time + +from odoo import api, models + + +class payroll_advice_report(models.AbstractModel): + _name = 'report.l10n_in_hr_payroll.report_payrolladvice' + _description = "Indian Payroll Advice Report" + + def get_month(self, input_date): + res = { + 'from_name': '', 'to_name': '' + } + slip = self.env['hr.payslip'].search([('date_from', '<=', input_date), ('date_to', '>=', input_date)], limit=1) + if slip: + from_date = slip.date_from + to_date = slip.date_to + res['from_name'] = from_date.strftime('%d') + '-' + from_date.strftime('%B') + '-' + from_date.strftime('%Y') + res['to_name'] = to_date.strftime('%d') + '-' + to_date.strftime('%B') + '-' + to_date.strftime('%Y') + return res + + @api.model + def _get_report_values(self, docids, data=None): + payment_report = self.env['hr.payroll.payment.report.wizard'].browse(docids) + return { + 'doc_ids': docids, + 'doc_model': 'hr.payroll.payment.report.wizard', + 'data': data, + 'docs': payment_report, + 'time': time, + 'get_month': self.get_month, + } diff --git a/addons_extensions/l10n_in_hr_payroll/security/ir.model.access.csv b/addons_extensions/l10n_in_hr_payroll/security/ir.model.access.csv new file mode 100644 index 000000000..9a1946dc0 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/security/ir.model.access.csv @@ -0,0 +1,6 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_yearly_salary_detail_user","yearly.salary.detail","model_yearly_salary_detail","hr_payroll.group_hr_payroll_user",1,1,1,0 +l10n_in_hr_payroll.access_salary_register_wizard,access_salary_register_wizard,l10n_in_hr_payroll.model_salary_register_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 +l10n_in_hr_payroll.access_l10n_in_tds_computation_wizard,access_l10n_in_tds_computation_wizard,l10n_in_hr_payroll.model_l10n_in_tds_computation_wizard,base.group_user,1,1,1,0 +access_report_l10n_in_hr_payroll_report_epf,report.l10n_in_hr_payroll.report_epf,model_l10n_in_hr_payroll_epf_report,hr_payroll.group_hr_payroll_user,1,1,1,1 +l10n_in_hr_payroll.access_l10n_in_hr_payroll_salary_statement,access_l10n_in_hr_payroll_salary_statement,l10n_in_hr_payroll.model_l10n_in_hr_payroll_salary_statement,"hr_payroll.group_hr_payroll_user",1,1,1,1 diff --git a/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_alisha_sharma.jpg b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_alisha_sharma.jpg new file mode 100644 index 000000000..5f3908976 Binary files /dev/null and b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_alisha_sharma.jpg differ diff --git a/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_shaurya.jpg b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_shaurya.jpg new file mode 100644 index 000000000..0ee7f13ea Binary files /dev/null and b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_shaurya.jpg differ diff --git a/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_vihaan.jpg b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_vihaan.jpg new file mode 100644 index 000000000..c5d7d9c73 Binary files /dev/null and b/addons_extensions/l10n_in_hr_payroll/static/img/hr_employee_vihaan.jpg differ diff --git a/addons_extensions/l10n_in_hr_payroll/tests/__init__.py b/addons_extensions/l10n_in_hr_payroll/tests/__init__.py new file mode 100644 index 000000000..e275ab870 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/tests/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_payment_advice_batch +from . import test_hr_contract diff --git a/addons_extensions/l10n_in_hr_payroll/tests/common.py b/addons_extensions/l10n_in_hr_payroll/tests/common.py new file mode 100644 index 000000000..eda93ec02 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/tests/common.py @@ -0,0 +1,88 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date + +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestPayrollCommon(TransactionCase): + + def setUp(self): + super(TestPayrollCommon, self).setUp() + + self.Bank = self.env['res.partner.bank'] + self.Employee = self.env['hr.employee'] + self.PayslipRun = self.env['hr.payslip.run'] + self.PayslipEmployee = self.env['hr.payslip.employees'] + self.Company = self.env['res.company'] + self.partner = self.env.ref('base.partner_admin') + self.bank_1 = self.env.ref('base.res_bank_1') + self.in_country = self.env.ref('base.in') + self.rd_dept = self.env.ref('hr.dep_rd') + self.employee_fp = self.env.ref('hr.employee_admin') + self.employee_al = self.env.ref('hr.employee_al') + + self.company_in = self.Company.create({ + 'name': 'Company IN', + 'country_code': 'IN', + }) + + self.in_bank = self.env['res.bank'].create({ + 'name': 'Bank IN', + 'bic': 'ABCD0123456' + }) + + # I create a new employee “Rahul” + self.rahul_emp = self.Employee.create({ + 'name': 'Rahul', + 'country_id': self.in_country.id, + 'department_id': self.rd_dept.id, + }) + + # I create a new employee “Rahul” + self.jethalal_emp = self.Employee.create({ + 'name': 'Jethalal', + 'country_id': self.in_country.id, + 'department_id': self.rd_dept.id, + }) + + self.res_bank = self.Bank.create({ + 'acc_number': '3025632343043', + 'partner_id': self.rahul_emp.work_contact_id.id, + 'acc_type': 'bank', + 'bank_id': self.in_bank.id, + 'allow_out_payment': True, + }) + self.rahul_emp.bank_account_id = self.res_bank + + self.res_bank_1 = self.Bank.create({ + 'acc_number': '3025632343044', + 'partner_id': self.jethalal_emp.work_contact_id.id, + 'acc_type': 'bank', + 'bank_id': self.in_bank.id, + 'allow_out_payment': True, + }) + self.jethalal_emp.bank_account_id = self.res_bank_1 + + self.contract_rahul = self.env['hr.contract'].create({ + 'date_start': date(2023, 1, 1), + 'date_end': date(2023, 1, 31), + 'name': 'Rahul Probation contract', + 'wage': 5000.0, + 'employee_id': self.rahul_emp.id, + 'state': 'open', + 'hr_responsible_id': self.employee_fp.id, + }) + + self.contract_jethalal = self.env['hr.contract'].create({ + 'date_start': date(2023, 1, 1), + 'date_end': date(2023, 1, 31), + 'name': 'Jethalal Probation contract', + 'wage': 5000.0, + 'employee_id': self.jethalal_emp.id, + 'state': 'open', + 'hr_responsible_id': self.employee_fp.id, + }) diff --git a/addons_extensions/l10n_in_hr_payroll/tests/test_hr_contract.py b/addons_extensions/l10n_in_hr_payroll/tests/test_hr_contract.py new file mode 100644 index 000000000..dae2eb72c --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/tests/test_hr_contract.py @@ -0,0 +1,43 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date + +from freezegun import freeze_time +from odoo.addons.l10n_in_hr_payroll.tests.common import TestPayrollCommon +from odoo.tests import tagged + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestHrContract(TestPayrollCommon): + + def test_contract_end_reminder_to_hr(self): + """ Check reminder activity is set the for probation contract + Test Case + --------- + 1) Create contract + 2) Check the activity is activity schedule or not + 3) Now run cron + 4) Check the activity is activity schedule or not + """ + user_admin_id = self.env.ref('base.user_admin').id + + contract = self.env['hr.contract'].create({ + 'date_start': date(2020, 1, 1), + 'date_end': date(2020, 4, 30), + 'name': 'Rahul Probation contract', + 'resource_calendar_id': self.env.company.resource_calendar_id.id, + 'wage': 5000.0, + 'employee_id': self.rahul_emp.id, + 'state': 'open', + 'contract_type_id': self.env.ref('l10n_in_hr_payroll.l10n_in_contract_type_probation').id, + 'kanban_state': 'done', + 'hr_responsible_id': user_admin_id, + }) + with freeze_time("2020-04-24"): + mail_activity = self.env['mail.activity'].search([('res_id', '=', contract.id), ('res_model', '=', 'hr.contract')]) + self.assertFalse(mail_activity.exists(), "There should be no mail activity as contract is not ends on 2020-04-10") + # run the cron + self.env['hr.contract'].update_state() + mail_activity = self.env['mail.activity'].search([('res_id', '=', contract.id), ('res_model', '=', 'hr.contract')]) + self.assertTrue(mail_activity.exists(), "There should be reminder activity as employee rahul's contract end today") diff --git a/addons_extensions/l10n_in_hr_payroll/tests/test_payment_advice_batch.py b/addons_extensions/l10n_in_hr_payroll/tests/test_payment_advice_batch.py new file mode 100644 index 000000000..a2236c129 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/tests/test_payment_advice_batch.py @@ -0,0 +1,62 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import Command +from odoo.addons.l10n_in_hr_payroll.tests.common import TestPayrollCommon +from odoo.tests import tagged + + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestPaymentAdviceBatch(TestPayrollCommon): + def _prepare_payslip_run(self): + payslip_run = self.env['hr.payslip.run'].create({ + 'date_start': '2023-01-01', + 'date_end': '2023-01-31', + 'name': 'January Batch', + 'company_id': self.company_in.id, + }) + + payslip_employee = self.env['hr.payslip.employees'].with_company(self.company_in).create({ + 'employee_ids': [ + Command.set([self.rahul_emp.id, self.jethalal_emp.id]) + ] + }) + + payslip_employee.with_context(active_id=payslip_run.id).compute_sheet() + payslip_run.action_validate() + return payslip_run + + def test_payment_report_advice_xlsx_creation(self): + payslip_run = self._prepare_payslip_run() + self.assertEqual(payslip_run.state, "close", "Payslip run should be in Done state") + + # Generating the XLSX report for the batch + payment_report_dict = self.env["hr.payroll.payment.report.wizard"].create({ + 'payslip_ids': payslip_run.slip_ids.ids, + 'payslip_run_id': payslip_run.id, + 'export_format': 'advice', + }).generate_payment_report_xls() + + payment_report = self.env['hr.payroll.payment.report.wizard'].browse(payment_report_dict['res_id']) + + self.assertTrue(payslip_run.payment_report, "XLSX File should be generated!") + self.assertTrue(payment_report.l10n_in_payment_advice_xlsx, "XLSX File should be generated!") + self.assertEqual(payment_report.l10n_in_payment_advice_filename_xlsx, payment_report.l10n_in_reference + '.xlsx') + self.assertTrue(payslip_run.payment_report_filename) + + def test_payment_report_advice_pdf_creation(self): + payslip_run = self._prepare_payslip_run() + self.assertEqual(payslip_run.state, "close", "Payslip run should be in Done state") + + # Generating the PDF report for the batch + payment_report_dict = self.env["hr.payroll.payment.report.wizard"].create({ + 'payslip_ids': payslip_run.slip_ids.ids, + 'payslip_run_id': payslip_run.id, + 'export_format': 'advice', + }).generate_payment_report_pdf() + + payment_report = self.env['hr.payroll.payment.report.wizard'].browse(payment_report_dict['res_id']) + + self.assertTrue(payslip_run.payment_report, "PDF File should be generated!") + self.assertTrue(payment_report.l10n_in_payment_advice_pdf, "PDF File should be generated!") + self.assertEqual(payment_report.l10n_in_payment_advice_filename_pdf, payment_report.l10n_in_reference + '.pdf') + self.assertTrue(payslip_run.payment_report_filename) diff --git a/addons_extensions/l10n_in_hr_payroll/views/hr_contract_views.xml b/addons_extensions/l10n_in_hr_payroll/views/hr_contract_views.xml new file mode 100644 index 000000000..61525ffca --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/hr_contract_views.xml @@ -0,0 +1,30 @@ + + + + hr.contract.form.in.inherit + hr.contract + + + + + + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/hr_employee_views.xml b/addons_extensions/l10n_in_hr_payroll/views/hr_employee_views.xml new file mode 100644 index 000000000..60beb61ec --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/hr_employee_views.xml @@ -0,0 +1,25 @@ + + + + + hr.employee.form.in.inherit + hr.employee + + + + + + + + + + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/l10n_in_hr_payroll_report.xml b/addons_extensions/l10n_in_hr_payroll/views/l10n_in_hr_payroll_report.xml new file mode 100644 index 000000000..abb5873ff --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/l10n_in_hr_payroll_report.xml @@ -0,0 +1,56 @@ + + + + PaySlip Details + hr.payslip + qweb-pdf + l10n_in_hr_payroll.report_payslip + l10n_in_hr_payroll.report_payslip + 'Payslip - %s' % (object.employee_id.name) + + report + + + + PaySlip Details (Light) + hr.payslip + qweb-pdf + l10n_in_hr_payroll.report_light_payslip + l10n_in_hr_payroll.report_light_payslip + 'Payslip - %s' % (object.employee_id.name) + + report + + + + + Advice Report + hr.payroll.payment.report.wizard + qweb-pdf + l10n_in_hr_payroll.report_payrolladvice + l10n_in_hr_payroll.report_payrolladvice + report + + + + Yearly Salary by Employee + yearly.salary.detail + qweb-pdf + l10n_in_hr_payroll.report_hryearlysalary + l10n_in_hr_payroll.report_hryearlysalary + + + report + + + + Salary Statement + hr.employee + qweb-pdf + l10n_in_hr_payroll.report_salary_statement + l10n_in_hr_payroll.report_salary_statement + (object._get_report_base_filename()) + + report + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/l10n_in_salary_statement.xml b/addons_extensions/l10n_in_hr_payroll/views/l10n_in_salary_statement.xml new file mode 100644 index 000000000..e20b244dc --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/l10n_in_salary_statement.xml @@ -0,0 +1,45 @@ + + + + l10n_in_hr_payroll.salary.statement.form + l10n_in_hr_payroll.salary.statement + +
+
+
+ +
+ +
+ + + + + +
+
+
+
+ + + Salary Statement Report + ir.actions.act_window + l10n_in_hr_payroll.salary.statement + list,form + + + +
diff --git a/addons_extensions/l10n_in_hr_payroll/views/report_hr_epf_views.xml b/addons_extensions/l10n_in_hr_payroll/views/report_hr_epf_views.xml new file mode 100644 index 000000000..4ae4ba964 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/report_hr_epf_views.xml @@ -0,0 +1,54 @@ + + + + + l10n.in.hr.payroll.epf.report + + + + + + + + + + l10n.in.hr.payroll.epf.report + +
+
+
+ + + + + + + + +
+

Generation Complete

+

+ Download the XLSX details file:
+ +

+
+
+
+
+
+ + + EPF Report + l10n.in.hr.payroll.epf.report + list,form + + + + +
diff --git a/addons_extensions/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml b/addons_extensions/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml new file mode 100644 index 000000000..5d0f6604a --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml @@ -0,0 +1,106 @@ + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/report_l10n_in_salary_statement.xml b/addons_extensions/l10n_in_hr_payroll/views/report_l10n_in_salary_statement.xml new file mode 100644 index 000000000..e80c36790 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/report_l10n_in_salary_statement.xml @@ -0,0 +1,69 @@ + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/report_payroll_advice_template.xml b/addons_extensions/l10n_in_hr_payroll/views/report_payroll_advice_template.xml new file mode 100644 index 000000000..15d7d2a8e --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/report_payroll_advice_template.xml @@ -0,0 +1,68 @@ + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/report_payslip_details_template.xml b/addons_extensions/l10n_in_hr_payroll/views/report_payslip_details_template.xml new file mode 100644 index 000000000..738013749 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/report_payslip_details_template.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/res_config_settings_views.xml b/addons_extensions/l10n_in_hr_payroll/views/res_config_settings_views.xml new file mode 100644 index 000000000..4f32bd9ac --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/res_config_settings_views.xml @@ -0,0 +1,40 @@ + + + + res.config.settings.view.form.inherit.l10n_in_hr_payroll + res.config.settings + + + +

Indian Localization

+
+
+
+
+ Company Information +
+ Offical Company Information +
+ + + +
+
+
+
+ +
+
+
+
+
+ + + + diff --git a/addons_extensions/l10n_in_hr_payroll/views/res_users_views.xml b/addons_extensions/l10n_in_hr_payroll/views/res_users_views.xml new file mode 100644 index 000000000..e82fef0f8 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + + res.user.preferences.view.form.l10n.in.payroll.inherit + res.users + + + + + + + + + diff --git a/addons_extensions/l10n_in_hr_payroll/wizard/__init__.py b/addons_extensions/l10n_in_hr_payroll/wizard/__init__.py new file mode 100644 index 000000000..6bdea8998 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/wizard/__init__.py @@ -0,0 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import hr_yearly_salary_detail +from . import hr_salary_register +from . import hr_tds_calculation +from . import hr_payroll_payment_report_wizard diff --git a/addons_extensions/l10n_in_hr_payroll/wizard/hr_payroll_payment_report.xml b/addons_extensions/l10n_in_hr_payroll/wizard/hr_payroll_payment_report.xml new file mode 100644 index 000000000..d77e14d16 --- /dev/null +++ b/addons_extensions/l10n_in_hr_payroll/wizard/hr_payroll_payment_report.xml @@ -0,0 +1,36 @@ + + + + hr.payroll.payment.advice.report.wizard.form + hr.payroll.payment.report.wizard + + + + + + + + + + + + + + + export_format == 'advice' + + +