From 44e5ee7e2f225df31c077ee0abf8056d983546fd Mon Sep 17 00:00:00 2001 From: pranay Date: Thu, 20 Nov 2025 10:11:59 +0530 Subject: [PATCH] ALL MODULE CHANGES --- .../employee_it_declaration/__init__.py | 2 + .../employee_it_declaration/__manifest__.py | 60 ++ .../employee_it_declaration/data/data.xml | 0 .../models/__init__.py | 7 + .../models/emp_it_declaration.py | 274 ++++++ .../models/investment_costings.py | 309 +++++++ .../models/investment_types.py | 220 +++++ .../models/it_tax_statement.py | 20 + .../models/it_tax_statement_wiz.py | 594 +++++++++++++ .../models/payroll_periods.py | 61 ++ .../models/slab_master.py | 72 ++ .../report/it_tax_template.xml | 726 ++++++++++++++++ .../report/report_action.xml | 12 + .../security/ir.model.access.csv | 65 ++ .../views/emp_it_declaration.xml | 355 ++++++++ .../views/investment_types.xml | 156 ++++ .../views/it_tax_menu_and_wizard_view.xml | 103 +++ .../views/payroll_periods.xml | 49 ++ .../views/report_it_tax_statement.xml | 28 + .../views/slab_master.xml | 70 ++ .../wizards/__init__.py | 6 + .../wizards/children_education_costing.py | 48 ++ .../wizards/children_education_costing.xml | 45 + .../wizards/employee_life_insurance.py | 52 ++ .../wizards/employee_life_insurance.xml | 57 ++ .../wizards/letout_house_property.py | 43 + .../wizards/letout_house_property.xml | 91 ++ .../wizards/nsc_declaration.py | 27 + .../wizards/nsc_declaration.xml | 52 ++ .../wizards/nsc_income_loss.py | 32 + .../wizards/nsc_income_loss.xml | 53 ++ .../wizards/self_occupied_property.py | 28 + .../wizards/self_occupied_property.xml | 80 ++ .../hr_employee_payroll_it/__init__.py | 1 + .../hr_employee_payroll_it/__manifest__.py | 47 ++ .../hr_employee_payroll_it/models/__init__.py | 4 + .../models/emp_it_declarations.py | 127 +++ .../models/it_investment_costing.py | 405 +++++++++ .../models/it_investment_type.py | 45 + .../models/payroll_periods.py | 61 ++ .../security/ir.model.access.csv | 22 + .../views/emp_it_declaration.xml | 210 +++++ .../views/it_investment_costing.xml | 36 + .../views/it_investment_type.xml | 59 ++ .../views/payroll_periods.xml | 49 ++ addons_extensions/hr_resignation/README.rst | 48 ++ addons_extensions/hr_resignation/__init__.py | 23 + .../hr_resignation/__manifest__.py | 51 ++ .../hr_resignation/data/data.xml | 11 + .../hr_resignation/data/ir_cron_data.xml | 15 + .../hr_resignation/data/ir_sequence_data.xml | 13 + .../hr_resignation/doc/RELEASE_NOTES.md | 5 + .../hr_resignation/i18n/ar_001.po | 460 ++++++++++ .../hr_resignation/models/__init__.py | 28 + .../hr_resignation/models/hr_contract.py | 44 + .../hr_resignation/models/hr_employee.py | 38 + .../hr_resignation/models/hr_resignation.py | 548 ++++++++++++ .../models/hr_resignation_warnings.py | 18 + ...re_resignation_requirements_proceedings.py | 33 + .../models/res_config_settings.py | 51 ++ .../security/hr_resignation_security.xml | 28 + .../security/ir.model.access.csv | 10 + .../assets/icons/arrows-repeat.svg | 10 + .../description/assets/icons/banner-bg.png | Bin 0 -> 111419 bytes .../description/assets/icons/banner-bg.svg | 9 + .../description/assets/icons/banner-call.svg | 5 + .../assets/icons/banner-logo-pattern.svg | 557 ++++++++++++ .../description/assets/icons/banner-mail.svg | 5 + .../description/assets/icons/banner-promo.svg | 147 ++++ .../blue-abstract-gradient-wave-wallpaper.jpg | Bin 0 -> 709089 bytes .../description/assets/icons/close-icon.svg | 5 + .../assets/icons/collabarate-icon.svg | 3 + .../description/assets/icons/cybro-logo.png | Bin 0 -> 17281 bytes .../description/assets/icons/down (1).svg | 1 + .../description/assets/icons/email (2).svg | 1 + .../description/assets/icons/feature-icon.svg | 10 + .../static/description/assets/icons/gear.svg | 10 + .../description/assets/icons/hire-odoo.svg | 12 + .../assets/icons/key-benefits-pattern.svg | 6 + .../description/assets/icons/key-benefits.svg | 3 + .../assets/icons/life-ring-icon.svg | 13 + .../assets/icons/logo-openhrms.png | Bin 0 -> 5845 bytes .../static/description/assets/icons/mail.svg | 3 + .../assets/icons/notification icon.svg | 10 + .../assets/icons/odoo-consultancy.svg | 4 + .../assets/icons/odoo-licencing.svg | 3 + .../description/assets/icons/odoo-logo.png | Bin 0 -> 9192 bytes .../description/assets/icons/patter.svg | 9 + .../description/assets/icons/pattern1.png | Bin 0 -> 7183 bytes .../assets/icons/puzzle-piece-icon.svg | 10 + .../description/assets/icons/replace-icon.svg | 10 + .../static/description/assets/icons/skype.svg | 3 + .../description/assets/icons/translate.svg | 10 + .../description/assets/icons/wrench-icon.svg | 10 + .../static/description/assets/modules/1.gif | Bin 0 -> 1336986 bytes .../static/description/assets/modules/2.png | Bin 0 -> 92765 bytes .../static/description/assets/modules/3.png | Bin 0 -> 99558 bytes .../static/description/assets/modules/4.png | Bin 0 -> 86062 bytes .../static/description/assets/modules/5.png | Bin 0 -> 92094 bytes .../static/description/assets/modules/6.png | Bin 0 -> 83144 bytes .../description/assets/screenshots/1.png | Bin 0 -> 124142 bytes .../description/assets/screenshots/2.png | Bin 0 -> 143213 bytes .../description/assets/screenshots/3.png | Bin 0 -> 79097 bytes .../description/assets/screenshots/hero.gif | Bin 0 -> 74592 bytes .../static/description/banner.jpg | Bin 0 -> 146101 bytes .../static/description/icon.png | Bin 0 -> 11901 bytes .../static/description/index.html | 790 ++++++++++++++++++ .../views/hr_contract_views.xml | 18 + .../views/hr_employee_views.xml | 28 + .../views/hr_resignation_views.xml | 394 +++++++++ ...e_resignation_requirements_proceedings.xml | 34 + .../views/res_config_settings_views.xml | 77 ++ .../menu_control_center/models/models.py | 48 ++ .../views/menu_access_control_views.xml | 11 +- .../tabbar/static/src/action_service.js | 11 - .../tabbar/static/src/akl_action_container.js | 1 - .../models/attendance_report.py | 11 +- .../static/src/js/quick_access_button.js | 16 +- .../ks_dashboard_ninja/__manifest__.py | 2 +- .../ks_dashboard_profile.js | 3 - .../static/src/js/dnNavBarExtend.js | 19 + .../static/src/scss/common.scss | 166 ++-- .../static/src/scss/header.scss | 216 ++--- .../static/src/scss/overview.scss | 6 +- .../src/widgets/ks_profile/ks_profile.js | 1 - .../src/xml/ks_dashboard_ninja_templates.xml | 2 +- 126 files changed, 8787 insertions(+), 223 deletions(-) create mode 100644 addons_extensions/employee_it_declaration/__init__.py create mode 100644 addons_extensions/employee_it_declaration/__manifest__.py create mode 100644 addons_extensions/employee_it_declaration/data/data.xml create mode 100644 addons_extensions/employee_it_declaration/models/__init__.py create mode 100644 addons_extensions/employee_it_declaration/models/emp_it_declaration.py create mode 100644 addons_extensions/employee_it_declaration/models/investment_costings.py create mode 100644 addons_extensions/employee_it_declaration/models/investment_types.py create mode 100644 addons_extensions/employee_it_declaration/models/it_tax_statement.py create mode 100644 addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py create mode 100644 addons_extensions/employee_it_declaration/models/payroll_periods.py create mode 100644 addons_extensions/employee_it_declaration/models/slab_master.py create mode 100644 addons_extensions/employee_it_declaration/report/it_tax_template.xml create mode 100644 addons_extensions/employee_it_declaration/report/report_action.xml create mode 100644 addons_extensions/employee_it_declaration/security/ir.model.access.csv create mode 100644 addons_extensions/employee_it_declaration/views/emp_it_declaration.xml create mode 100644 addons_extensions/employee_it_declaration/views/investment_types.xml create mode 100644 addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml create mode 100644 addons_extensions/employee_it_declaration/views/payroll_periods.xml create mode 100644 addons_extensions/employee_it_declaration/views/report_it_tax_statement.xml create mode 100644 addons_extensions/employee_it_declaration/views/slab_master.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/__init__.py create mode 100644 addons_extensions/employee_it_declaration/wizards/children_education_costing.py create mode 100644 addons_extensions/employee_it_declaration/wizards/children_education_costing.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/employee_life_insurance.py create mode 100644 addons_extensions/employee_it_declaration/wizards/employee_life_insurance.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/letout_house_property.py create mode 100644 addons_extensions/employee_it_declaration/wizards/letout_house_property.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/nsc_declaration.py create mode 100644 addons_extensions/employee_it_declaration/wizards/nsc_declaration.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/nsc_income_loss.py create mode 100644 addons_extensions/employee_it_declaration/wizards/nsc_income_loss.xml create mode 100644 addons_extensions/employee_it_declaration/wizards/self_occupied_property.py create mode 100644 addons_extensions/employee_it_declaration/wizards/self_occupied_property.xml create mode 100644 addons_extensions/hr_employee_payroll_it/__init__.py create mode 100644 addons_extensions/hr_employee_payroll_it/__manifest__.py create mode 100644 addons_extensions/hr_employee_payroll_it/models/__init__.py create mode 100644 addons_extensions/hr_employee_payroll_it/models/emp_it_declarations.py create mode 100644 addons_extensions/hr_employee_payroll_it/models/it_investment_costing.py create mode 100644 addons_extensions/hr_employee_payroll_it/models/it_investment_type.py create mode 100644 addons_extensions/hr_employee_payroll_it/models/payroll_periods.py create mode 100644 addons_extensions/hr_employee_payroll_it/security/ir.model.access.csv create mode 100644 addons_extensions/hr_employee_payroll_it/views/emp_it_declaration.xml create mode 100644 addons_extensions/hr_employee_payroll_it/views/it_investment_costing.xml create mode 100644 addons_extensions/hr_employee_payroll_it/views/it_investment_type.xml create mode 100644 addons_extensions/hr_employee_payroll_it/views/payroll_periods.xml create mode 100644 addons_extensions/hr_resignation/README.rst create mode 100644 addons_extensions/hr_resignation/__init__.py create mode 100644 addons_extensions/hr_resignation/__manifest__.py create mode 100644 addons_extensions/hr_resignation/data/data.xml create mode 100644 addons_extensions/hr_resignation/data/ir_cron_data.xml create mode 100644 addons_extensions/hr_resignation/data/ir_sequence_data.xml create mode 100644 addons_extensions/hr_resignation/doc/RELEASE_NOTES.md create mode 100644 addons_extensions/hr_resignation/i18n/ar_001.po create mode 100644 addons_extensions/hr_resignation/models/__init__.py create mode 100644 addons_extensions/hr_resignation/models/hr_contract.py create mode 100644 addons_extensions/hr_resignation/models/hr_employee.py create mode 100644 addons_extensions/hr_resignation/models/hr_resignation.py create mode 100644 addons_extensions/hr_resignation/models/hr_resignation_warnings.py create mode 100644 addons_extensions/hr_resignation/models/pre_resignation_requirements_proceedings.py create mode 100644 addons_extensions/hr_resignation/models/res_config_settings.py create mode 100644 addons_extensions/hr_resignation/security/hr_resignation_security.xml create mode 100644 addons_extensions/hr_resignation/security/ir.model.access.csv create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/arrows-repeat.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-bg.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-bg.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-call.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-logo-pattern.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-mail.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/banner-promo.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/blue-abstract-gradient-wave-wallpaper.jpg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/close-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/collabarate-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/cybro-logo.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/down (1).svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/email (2).svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/feature-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/gear.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/hire-odoo.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/key-benefits-pattern.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/key-benefits.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/life-ring-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/logo-openhrms.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/mail.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/notification icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/odoo-consultancy.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/odoo-licencing.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/odoo-logo.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/patter.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/pattern1.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/puzzle-piece-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/replace-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/skype.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/translate.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/icons/wrench-icon.svg create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/1.gif create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/2.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/3.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/4.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/5.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/modules/6.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/screenshots/1.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/screenshots/2.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/screenshots/3.png create mode 100644 addons_extensions/hr_resignation/static/description/assets/screenshots/hero.gif create mode 100644 addons_extensions/hr_resignation/static/description/banner.jpg create mode 100644 addons_extensions/hr_resignation/static/description/icon.png create mode 100644 addons_extensions/hr_resignation/static/description/index.html create mode 100644 addons_extensions/hr_resignation/views/hr_contract_views.xml create mode 100644 addons_extensions/hr_resignation/views/hr_employee_views.xml create mode 100644 addons_extensions/hr_resignation/views/hr_resignation_views.xml create mode 100644 addons_extensions/hr_resignation/views/pre_resignation_requirements_proceedings.xml create mode 100644 addons_extensions/hr_resignation/views/res_config_settings_views.xml diff --git a/addons_extensions/employee_it_declaration/__init__.py b/addons_extensions/employee_it_declaration/__init__.py new file mode 100644 index 000000000..6ed2c210b --- /dev/null +++ b/addons_extensions/employee_it_declaration/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/__manifest__.py b/addons_extensions/employee_it_declaration/__manifest__.py new file mode 100644 index 000000000..6f8d40a6f --- /dev/null +++ b/addons_extensions/employee_it_declaration/__manifest__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Payroll Tax Management", + + 'summary': "Manage Income Tax Declarations for Employees in Payroll", + 'description': """ + Payroll IT Declarations + ======================== + + This module allows HR and payroll departments to manage and track Income Tax (IT) declarations submitted by employees. + + Features: + --------- + - Employee-wise tax declaration submission + - HR approval workflow for declarations + - Category-wise declaration limits (e.g. 80C, HRA, LTA, etc.) + - Auto-calculation of eligible deductions + - Integration with Odoo Payroll for accurate tax computation + - Attach supporting documents (PDFs, images) + - Employee self-service through portal + + Built with usability and compliance in mind, this module streamlines the IT declaration process and ensures transparency and efficiency across the organization. + + Developed by: Pranay + """, + + 'author': "Pranay", + 'website': "https://www.ftprotech.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Human Resources', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base','hr','hr_payroll','hr_employee_extended'], + + # always loaded + 'data': [ + 'security/ir.model.access.csv', + 'views/payroll_periods.xml', + 'views/investment_types.xml', + # 'views/payroll_periods.xml', + 'views/slab_master.xml', + 'views/emp_it_declaration.xml', + 'views/report_it_tax_statement.xml', + 'report/report_action.xml', + 'report/it_tax_template.xml', + 'views/it_tax_menu_and_wizard_view.xml', + 'wizards/children_education_costing.xml', + 'wizards/employee_life_insurance.xml', + 'wizards/nsc_declaration.xml', + 'wizards/self_occupied_property.xml', + 'wizards/letout_house_property.xml', + 'wizards/nsc_income_loss.xml', + # 'views/it_investment_type.xml', + # 'views/it_investment_costing.xml' + ], +} \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/data/data.xml b/addons_extensions/employee_it_declaration/data/data.xml new file mode 100644 index 000000000..e69de29bb diff --git a/addons_extensions/employee_it_declaration/models/__init__.py b/addons_extensions/employee_it_declaration/models/__init__.py new file mode 100644 index 000000000..de9d70aab --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/__init__.py @@ -0,0 +1,7 @@ +from . import payroll_periods +from . import investment_types +from . import investment_costings +from . import emp_it_declaration +from . import slab_master +from . import it_tax_statement +from . import it_tax_statement_wiz \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py new file mode 100644 index 000000000..081cc1e6b --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py @@ -0,0 +1,274 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from datetime import datetime, timedelta +import calendar + + +class EmpITDeclaration(models.Model): + _name = 'emp.it.declaration' + _rec_name = 'employee_id' + _description = "IT Declaration" + + # @api.depends('period_id', 'employee_id') + # def _compute_name(self): + # for sheet in self: + # # sheet.name = _('%(period_id)s, %(emp_name)s', period_id=sheet.period_id.name, emp_name=sheet.employee_id.name) + # sheet.name='hello world' + + employee_id = fields.Many2one( + 'hr.employee', + string="Employee", + default=lambda self: self.env.user.employee_id, + required=True + ) + period_id = fields.Many2one( + 'payroll.period', + string="Payroll Period", + required=True + ) + + display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label') + + @api.depends('period_id.name') + def _compute_display_period_label(self): + for rec in self: + if rec.period_id: + rec.display_period_label = f"Financial Year {rec.period_id.name}" + else: + rec.display_period_label = "" + + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime') + ], string="Tax Regime", required=True, default='new') + + total_investment = fields.Float(string='Total Investment') + + costing_details_generated = fields.Boolean(default=False) + + investment_costing_ids = fields.One2many('investment.costings','it_declaration_id') + house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing") + is_section_open = fields.Boolean() + @api.depends('costing_details_generated','investment_costing_ids') + def _compute_investment_costing(self): + for rec in self: + if rec.investment_costing_ids and rec.costing_details_generated: + rec.house_rent_costing_id = rec.investment_costing_ids.filtered( + lambda e: e.investment_type_id.investment_type == 'house_rent' + )[:1] + else: + rec.house_rent_costing_id = False + past_employment_costings = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]) + past_employment_costings_new = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]) + us80c_costings = fields.One2many('us80c.costing.type','it_declaration_id') + us80d_selection_type = fields.Selection([('self_family','Self-family'),('self_family_parent','Self-family and parent'),('self_family_senior_parent','Self-family and senior parent')], default='self_family',required=True) + us80d_health_checkup = fields.Boolean(string='Preventive Health Checkup') + us80d_costings = fields.One2many('us80d.costing.type','it_declaration_id',domain=[('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',False),('investment_type_line_id.for_senior_parent','=',False)]) + us80d_costings_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',False)]) + us80d_costings_senior_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|','|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',True)]) + + us10_costings = fields.One2many('us10.costing.type','it_declaration_id') + us80g_costings = fields.One2many('us80g.costing.type','it_declaration_id') + chapter_via_costings = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]) + chapter_via_costings_new = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]) + us17_costings = fields.One2many('us17.costing.type','it_declaration_id') + + house_rent_costings = fields.One2many('house.rent.declaration','it_declaration_id') + + other_il_costings = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]) + other_il_costings_new = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]) + other_declaration_costings = fields.One2many('other.declaration.costing.type','it_declaration_id') + + + def toggle_section_visibility(self): + for rec in self: + rec.is_section_open = not rec.is_section_open + if rec.is_section_open: + for investment_type in rec.investment_costing_ids: + if investment_type.investment_type_id.investment_type == 'past_employment': + if rec.tax_regime == 'old': + investment_type.amount = sum( + cost.declaration_amount + for cost in rec.past_employment_costings + if not cost.investment_type_line_id.compute_method + ) + else: + investment_type.amount = sum( + cost.declaration_amount + for cost in rec.past_employment_costings_new + if not cost.investment_type_line_id.compute_method + ) + elif investment_type.investment_type_id.investment_type == 'us_80c': + investment_type.amount = sum( + cost.declaration_amount + for cost in rec.us80c_costings + if not cost.investment_type_line_id.compute_method + ) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'us_80d': + if rec.us80d_selection_type == 'self_family': + investment_type.amount = sum(rec.us80d_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + if rec.us80d_selection_type == 'self_family_parent': + investment_type.amount = sum(rec.us80d_costings_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + if rec.us80d_selection_type == 'self_family_senior_parent': + investment_type.amount = sum(rec.us80d_costings_senior_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'us_10': + investment_type.amount = sum(rec.us10_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'us_80g': + investment_type.amount = sum(rec.us80g_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'chapter_via': + if rec.tax_regime == 'old': + investment_type.amount = sum(rec.chapter_via_costings.mapped('declaration_amount') or [0]) + else: + investment_type.amount = sum(rec.chapter_via_costings_new.mapped('declaration_amount') or [0]) + elif investment_type.investment_type_id.investment_type == 'us_17': + investment_type.amount = sum(rec.us17_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'house_rent': + investment_type.amount = sum(rec.house_rent_costings.mapped('rent_amount') or [0]) if rec.tax_regime == 'old' else 0 + elif investment_type.investment_type_id.investment_type == 'other_i_or_l': + if rec.tax_regime == 'old': + investment_type.amount = sum(rec.other_il_costings.mapped('declaration_amount') or [0]) + else: + investment_type.amount = sum(rec.other_il_costings_new.mapped('declaration_amount') or [0]) + elif investment_type.investment_type_id.investment_type == 'other_declaration': + investment_type.amount = sum(rec.other_declaration_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0 + + @api.onchange('tax_regime') + def _onchange_tax_regime(self): + if self.tax_regime: + # res = super(empITDeclaration, self).fields_get(allfields, attributes) + # self.fields_get() + if self.tax_regime == 'new': + domain = [('investment_type_line_id.tax_regime', 'in', ['new', 'both'])] + elif self.tax_regime == 'old': + domain = [('investment_type_line_id.tax_regime', 'in', ['old', 'both'])] + else: + domain = [] # Default case, although 'tax_regime' is required + return {'domain': {'past_employment_costings': domain}} + else: + return {'domain': {'past_employment_costings': []}} # Handle potential empty state + + # def fields_get(self, allfields=None, attributes=None): + # import pdb + # pdb.set_trace() + # res = super(empITDeclaration, self).fields_get(allfields, attributes) + # print(res) + # + # # Example: Modify domain of field_1 based on field_2 + # if 'tax_regime' in res: + # if self.tax_regime == '': + # res['field_1']['domain'] = [('some_field', '=', 123)] + # else: + # res['field_1']['domain'] = [('some_field', '=', 456)] + # + # return res + # import pdb + # pdb.set_trace() + # if rec.tax_regime: + # return {'domain': {'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]}} + # return { + # 'domain': { + # 'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])] + # } + # } + + def generate_declarations(self): + for rec in self: + investment_types = self.env['it.investment.type'].sudo().search([]) + for inv_type in investment_types: + investment_costing = self.env['investment.costings'].sudo().create({ + 'investment_type_id': inv_type.id, + 'it_declaration_id': rec.id, + }) + + if inv_type.investment_type == 'past_employment': + past_emp_costing_ids = [ + self.env['past_employment.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.past_employment_ids + ] + if inv_type.investment_type == 'us_80c': + + us80c_costing_ids = [ + self.env['us80c.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.us80c_ids + ] + if inv_type.investment_type == 'us_80d': + us80d_costing_ids = [ + self.env['us80d.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.us80d_ids + ] + if inv_type.investment_type == 'us_10': + us10_costing_ids = [ + self.env['us10.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.us10_ids + ] + if inv_type.investment_type == 'us_80g': + us80g_costing_ids = [ + self.env['us80g.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.us80g_ids + ] + if inv_type.investment_type == 'chapter_via': + chapter_via_ids = [ + self.env['chapter.via.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.chapter_via_ids + ] + if inv_type.investment_type == 'us_17': + us17_costing_ids = [ + self.env['us17.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.us17_ids + ] + if inv_type.investment_type == 'other_i_or_l': + other_il_costing_ids = [ + self.env['other.il.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.other_il_ids + ] + if inv_type.investment_type == 'other_declaration': + other_declaration_costing_ids = [ + self.env['other.declaration.costing.type'].sudo().create({ + 'costing_type': investment_costing.id, + 'it_declaration_id': rec.id, + 'investment_type_line_id': investment_line.id, + 'limit': investment_line.limit + }).id + for investment_line in inv_type.other_declaration_ids + ] + rec.costing_details_generated = True \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/investment_costings.py b/addons_extensions/employee_it_declaration/models/investment_costings.py new file mode 100644 index 000000000..3d884a2c0 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/investment_costings.py @@ -0,0 +1,309 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError +from datetime import datetime, timedelta +import calendar +import re + + +class investmentCostings(models.Model): + _name = 'investment.costings' + _rec_name = 'investment_type_id' + + investment_type_id = fields.Many2one('it.investment.type') + amount = fields.Integer() + it_declaration_id = fields.Many2one('emp.it.declaration') + employee_id = fields.Many2one( + 'hr.employee', + string="Employee", + related='it_declaration_id.employee_id' + ) + period_id = fields.Many2one( + 'payroll.period', + string="Payroll Period", + related='it_declaration_id.period_id' + ) + +class pastEmpcostingType(models.Model): + _name = 'past_employment.costing.type' + _rec_name = 'investment_type_line_id' + + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + investment_type_line_id = fields.Many2one('past_employment.investment.type') + declaration_amount = fields.Integer(string='Declaration Amount',compute='_compute_declaration_amount',store=True,readonly=False) + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + @api.depends( + 'it_declaration_id.past_employment_costings.declaration_amount', + 'it_declaration_id.past_employment_costings_new.declaration_amount', + 'investment_type_line_id.compute_method', + 'investment_type_line_id.compute_code', + 'it_declaration_id.tax_regime', + 'declaration_amount' + ) + def _compute_declaration_amount(self): + for rec in self: + line = rec.investment_type_line_id + if not line or not rec.it_declaration_id: + rec.declaration_amount = 0 + continue + + if line.compute_method and line.compute_code: + siblings = ( + rec.it_declaration_id.past_employment_costings + if rec.it_declaration_id.tax_regime == 'old' + else rec.it_declaration_id.past_employment_costings_new + ) + + code_vars = {} + for sibling in siblings: + code = sibling.investment_type_line_id.investment_code + if code: + code_vars[code] = sibling.declaration_amount or 0 + + try: + # Extract variable names from compute_code + var_names = set(re.findall(r'\b[A-Z]+\b', line.compute_code)) + for var in var_names: + code_vars.setdefault(var, 0) # Ensure missing variables default to 0 + + rec.declaration_amount = int(eval(line.compute_code, {"__builtins__": {}}, code_vars)) + except Exception as e: + raise ValidationError(f"Error in compute_code for {line.name}: {e}") + else: + # Allow manual entry + pass + + + @api.onchange('investment_type_line_id', 'declaration_amount') + def _onchange_declaration_amount_live(self): + for rec in self: + line = rec.investment_type_line_id + if not line or not rec.it_declaration_id: + return + + siblings = rec.it_declaration_id.past_employment_costings | rec.it_declaration_id.past_employment_costings_new + code_vars = {} + for sibling in siblings: + code = sibling.investment_type_line_id.investment_code + if code: + code_vars[code] = sibling.declaration_amount or 0 + + if line.compute_method and line.compute_code: + try: + rec.declaration_amount = int(eval(line.compute_code, {"__builtins__": {}}, code_vars)) + except Exception as e: + rec.declaration_amount = 0 # fallback + + +class us80cCostingType(models.Model): + _name = 'us80c.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('us80c.investment.type') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + declaration_amount = fields.Integer(string='Declaration Amount') + action_id = fields.Many2one('ir.actions.act_window', related='investment_type_line_id.action_id') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + def open_action_wizard(self): + self.ensure_one() + model = self.env[self.action_id.res_model] + + action_model = model.sudo().search([('it_declaration_id','=',self.it_declaration_id.id),('us80c_id','=',self.id)],order='id desc',limit=1) + # it_declaration_id + if not action_model: + # Explicitly create record so children get added in create() + action_model = model.sudo().create({ + 'it_declaration_id': self.it_declaration_id.id, + 'us80c_id': self.id, + }) + return { + 'type': 'ir.actions.act_window', + 'name': self.action_id.name, + 'res_model': self.action_id.res_model, + 'res_id': action_model.id, + 'view_mode': self.action_id.view_mode, + 'target': 'new', + } +class us80dCostingType(models.Model): + _name = 'us80d.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('us80d.investment.type') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + + +class us10CostingType(models.Model): + _name = 'us10.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('us10.investment.type') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + +class us80gCostingType(models.Model): + _name = 'us80g.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('us80g.investment.type') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + +class chapterViaCostingType(models.Model): + _name = 'chapter.via.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('chapter.via.investment.type') + investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + +class us17CostingType(models.Model): + _name = 'us17.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('us17.investment.type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + +class OtherILCostingType(models.Model): + _name = 'other.il.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('other.il.investment.type') + declaration_amount = fields.Integer(string='Declaration Amount') + action_id = fields.Many2one('ir.actions.act_window', related='investment_type_line_id.action_id') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + def open_action_wizard(self): + self.ensure_one() + model = self.env[self.action_id.res_model] + + action_model = model.sudo().search([('it_declaration_id','=',self.it_declaration_id.id),('other_il_id','=',self.id)],order='id desc',limit=1) + # it_declaration_id + if not action_model: + # Explicitly create record so children get added in create() + action_model = model.sudo().create({ + 'it_declaration_id': self.it_declaration_id.id, + 'other_il_id': self.id, + }) + return { + 'type': 'ir.actions.act_window', + 'name': self.action_id.name, + 'res_model': self.action_id.res_model, + 'res_id': action_model.id, + 'view_mode': self.action_id.view_mode, + 'target': 'new', + } + + +class OtherDeclarationCostingType(models.Model): + _name = 'other.declaration.costing.type' + _rec_name = 'investment_type_line_id' + + costing_type = fields.Many2one('investment.costings') + it_declaration_id = fields.Many2one('emp.it.declaration') + investment_type_line_id = fields.Many2one('other.declaration.investment.type') + declaration_amount = fields.Integer(string='Declaration Amount') + proof_amount = fields.Integer(string="Proof Amount") + remarks = fields.Text(string="Remarks") + proof = fields.Binary(string="PROOF") + proof_name = fields.Char() + limit = fields.Integer() + + +class HouseRentDeclaration(models.Model): + _name = 'house.rent.declaration' + _description = 'House Rent Declaration' + + + it_declaration_id = fields.Many2one('emp.it.declaration') + costing_type = fields.Many2one('investment.costings') + + hra_exemption_type = fields.Selection([ + ('u_s_10', 'U/S 10 - HRA Exemption'), + ('u_s_80gg', 'U/S 80GG - HRA Exemption') + ], string="HRA Exemption Type", required=True, default='u_s_10') + + rent_amount = fields.Float(string="Total Rent for the Period", required=True) + from_date = fields.Date(string="From Date", required=True) + to_date = fields.Date(string="To Date", required=True) + remarks = fields.Text(string="Remarks") + + landlord_pan_no = fields.Char(string="Landlord PAN No") + landlord_name_address = fields.Text(string="Landlord Name & Address") + + landlord_pan_status = fields.Selection([ + ('has_pan', 'Landlord has PAN CARD'), + ('declaration', 'Declaration By Landlord') + ], string="Landlord PAN Status", required=True, default='has_pan') + + attachment = fields.Binary(string="Proof Attachment") + attachment_filename = fields.Char(string="Attachment Filename") + + @api.model + def create(self, vals): + # Auto-link applicant_id if context is passed correctly + if self.env.context.get('default_it_declaration_id'): + import pdb + pdb.set_trace() + costing_id = self.env['investment.costings'].sudo().search([('id','=',self.env.context.get('it_declaration_id')),('investment_type_id.investment_type','=','house_rent')],limit=1) + vals['costing_type'] = costing_id.id + return super().create(vals) \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/investment_types.py b/addons_extensions/employee_it_declaration/models/investment_types.py new file mode 100644 index 000000000..07d4deba6 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/investment_types.py @@ -0,0 +1,220 @@ +from odoo import models, fields + + +class ItInvestmentType(models.Model): + _name = 'it.investment.type' + _rec_name = 'investment_type' + + sequence = fields.Integer() + investment_type = fields.Selection( + [('past_employment', 'PAST EMPLOYMENT'), ('us_80c', 'US 80C'), ('us_80d', 'US 80D'), ('us_10', 'US 10'), + ('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'), + ('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type", + required=True) + + active = fields.Boolean(default=True) + past_employment_ids = fields.One2many('past_employment.investment.type','investment_type') + us80c_ids = fields.One2many('us80c.investment.type', 'investment_type') + us80d_ids = fields.One2many('us80d.investment.type', 'investment_type') + us10_ids = fields.One2many('us10.investment.type', 'investment_type') + us80g_ids = fields.One2many('us80g.investment.type', 'investment_type') + chapter_via_ids = fields.One2many('chapter.via.investment.type', 'investment_type') + us17_ids = fields.One2many('us17.investment.type', 'investment_type') + other_il_ids = fields.One2many('other.il.investment.type', 'investment_type') + other_declaration_ids = fields.One2many('other.declaration.investment.type', 'investment_type') + +class pastEmpInvestmentType(models.Model): + _name = 'past_employment.investment.type' + _rec_name = 'name' + + _sql_constraints = [ + ('investment_code_unique', 'unique(investment_code)', 'Code must be unique'), + ] + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + investment_code = fields.Char() + compute_method = fields.Boolean() + compute_code = fields.Text('Python Code') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + +class us80cInvestmentType(models.Model): + _name = 'us80c.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + investment_code = fields.Char() + compute_method = fields.Boolean() + compute_code = fields.Text('Python Code') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + require_action = fields.Boolean() + action_id = fields.Many2one('ir.actions.act_window') + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + +class us80dInvestmentType(models.Model): + _name = 'us80d.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + for_family = fields.Boolean() + for_parents = fields.Boolean() + for_senior_parent = fields.Boolean() + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + + +class us10InvestmentType(models.Model): + _name = 'us10.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + + +class us80gInvestmentType(models.Model): + _name = 'us80g.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", required=True) + + +class chapterViaInvestmentType(models.Model): + _name = 'chapter.via.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + + +class us17InvestmentType(models.Model): + _name = 'us17.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + +class OtherILInvestmentType(models.Model): + _name = 'other.il.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + require_action = fields.Boolean() + action_id = fields.Many2one('ir.actions.act_window') + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + + +class OtherDeclarationInvestmentType(models.Model): + _name = 'other.declaration.investment.type' + _rec_name = 'name' + + name = fields.Char(string='GENERAL', required=True) + investment_type = fields.Many2one('it.investment.type') + limit = fields.Integer(string='LIMIT') + sequence = fields.Integer() + active = fields.Boolean(default=True) + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime'), + ('both', 'Both') + ], string="Tax Regime", store=True, required=True) + + +# +# class ItInvestmentTypeLine(models.Model): +# _name = 'it.investment.type.line' +# _description = 'IT Investment Type Line' +# _rec_name = 'name' +# +# investment_type_id = fields.Many2one( +# 'it.investment.type', +# string="Investment Type", +# required=True, +# ondelete='cascade' +# ) +# investment_type = fields.Selection( +# [('past_employment', 'PAST EMPLOYMENT'), ('us_80c', 'US 80C'), ('us_80d', 'US 80D'), ('us_10', 'US 10'), +# ('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'), +# ('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type", +# related='investment_type_id.name') +# name = fields.Char(string="Line Name", required=True) +# need_action = fields.Boolean(string="Needs Action") +# action_id = fields.Many2one( +# 'ir.actions.actions', +# string="Action", +# help="Linked Odoo action if needed" +# ) +# tax_regime = fields.Selection([ +# ('new', 'New Regime'), +# ('old', 'Old Regime') +# ], string="Tax Regime", required=True) +# sequence = fields.Integer() +# active = fields.Boolean(default=True) +# limit = fields.Integer() \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/it_tax_statement.py b/addons_extensions/employee_it_declaration/models/it_tax_statement.py new file mode 100644 index 000000000..daad22c0b --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/it_tax_statement.py @@ -0,0 +1,20 @@ + +from odoo import models, fields, api, _ + +class ITTaxStatement(models.Model): + _name = 'it.tax.statement' + _description = 'IT Tax Statement' + _rec_name = 'employee_id' + + employee_id = fields.Many2one('hr.employee', required=True) + declaration_id = fields.Many2one('emp.it.declaration', required=True) + period_id = fields.Many2one('payroll.period', related='declaration_id.period_id', store=True) + period_line = fields.Many2one('payroll.period.line') + total_declared_amount = fields.Float(compute='_compute_totals') + tax_regime = fields.Selection(related='declaration_id.tax_regime') + pdf_generated = fields.Boolean(default=False) + + @api.depends('declaration_id') + def _compute_totals(self): + for rec in self: + rec.total_declared_amount = sum(rec.declaration_id.investment_costing_ids.mapped('amount')) diff --git a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py new file mode 100644 index 000000000..96640ca72 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py @@ -0,0 +1,594 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from datetime import date, timedelta +from dateutil.relativedelta import relativedelta +import math + + +class ITTaxStatementWizard(models.TransientModel): + _name = 'it.tax.statement.wizard' + _rec_name = 'employee_id' + _description = 'Generate IT Tax Statement (Old vs New)' + + # Inputs + employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id) + emp_doj = fields.Date(related='employee_id.doj', store=True) + contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id', required=True) + + period_id = fields.Many2one('payroll.period', required=True) + period_line = fields.Many2one('payroll.period.line') + + # Taxpayer profile + taxpayer_name = fields.Char(related='employee_id.name') + taxpayer_age = fields.Integer(required=True, default=False) + residential_status = fields.Selection([ + ('RESIDENT', 'Resident'), + ('NON-RESIDENT', 'Non-Resident') + ], default='RESIDENT', required=True) + parent_age = fields.Integer(default=65) + + # Tax regime selection + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime') + ], string="Tax Regime", required=True, default='new') + lock_regime = fields.Selection([ + ('UNLOCKED', 'Unlocked'), + ('LOCKED', 'Locked') + ], default='UNLOCKED', string="Lock Regime") + + # Computed fields for salary breakdown + basic_salary = fields.Float(compute='_compute_salary_components', store=False) + hra_salary = fields.Float(compute='_compute_salary_components', store=False) + lta_salary = fields.Float(compute='_compute_salary_components', store=False) + special_allowance = fields.Float(compute='_compute_salary_components', store=False) + gross_salary = fields.Float(compute='_compute_salary_components', store=False) + + # Deductions + professional_tax = fields.Float(compute='_compute_deductions', store=False) + standard_deduction = fields.Float(compute='_compute_deductions', store=False) + nps_employer_contribution = fields.Float(compute='_compute_deductions', store=False) + + # Other income + other_income = fields.Float(string="Other Income", default=0.0) + + # Additional fields for tax calculation + hra_exemption = fields.Float(string="HRA Exemption", default=0.0) + interest_home_loan_self = fields.Float(string="Interest on Home Loan (Self Occupied)", default=0.0) + interest_home_loan_letout = fields.Float(string="Interest on Home Loan (Let Out)", default=0.0) + rental_income = fields.Float(string="Rental Income", default=0.0) + + # Deductions under Chapter VI-A + ded_80C = fields.Float(string="Deduction under 80C", default=0.0) + ded_80CCD1B = fields.Float(string="Deduction under 80CCD(1B)", default=0.0) + ded_80D_self = fields.Float(string="Deduction under 80D (Self)", default=0.0) + ded_80D_parents = fields.Float(string="Deduction under 80D (Parents)", default=0.0) + ded_80G = fields.Float(string="Deduction under 80G", default=0.0) + ded_other = fields.Float(string="Other Deductions", default=0.0) + + def _get_applicable_slab(self, regime, age, residence_type): + """Get the applicable tax slab based on regime, age, and residence type""" + # Determine age category + if age < 60: + age_category = 'below_60' + elif age < 80: + age_category = '60_to_80' + else: + age_category = 'above_80' + + # Search for slab master + slab_master = self.env['it.slab.master'].search([ + ('regime', '=', regime), + ('age_category', '=', age_category), + '|', + ('residence_type', '=', residence_type.lower()), + ('residence_type', '=', 'both') + ], limit=1) + + if not slab_master: + raise ValidationError(_( + "No tax slab found for %s Regime with Age Category: %s and Residence Type: %s" + ) % (regime.capitalize(), age_category.replace('_', ' ').title(), residence_type)) + + return slab_master + + def _compute_tax_using_slab(self, taxable, slab_master): + """Compute tax using slab master rules""" + tax = 0.0 + + # Get rules sorted by min_income + rules = slab_master.rules.sorted('min_income') + + for rule in rules: + if taxable <= rule.min_income: + continue + + # Calculate amount in this bracket + bracket_max = rule.max_income if rule.max_income else float('inf') + amount_in_bracket = min(taxable, bracket_max) - rule.min_income + + # Apply tax calculation based on rule structure + if rule.fixed_amount and rule.excess_threshold: + # Rule with fixed amount and excess threshold + excess_amount = max(0, taxable - rule.excess_threshold) + tax_for_bracket = rule.fixed_amount + (excess_amount * rule.tax_rate / 100) + else: + # Standard bracket calculation + tax_for_bracket = amount_in_bracket * rule.tax_rate / 100 + + tax += tax_for_bracket + + return tax + + @api.depends('employee_id', 'contract_id', 'period_id') + def _compute_salary_components(self): + """Compute salary components from payroll data""" + for rec in self: + if not rec.employee_id or not rec.contract_id: + continue + # Get payslip for the period + payslip = self.env['hr.payslip'].search([ + ('employee_id', '=', rec.employee_id.id), + ('date_from', '>=', rec.period_line.from_date), + ('date_to', '<=', rec.period_line.to_date), + ('state', 'in', ['verify','done','paid']) + ], limit=1) + + if payslip: + # Extract salary components from payslip lines + rec.basic_salary = self._get_salary_rule_amount(payslip, 'BASIC') + rec.hra_salary = self._get_salary_rule_amount(payslip, 'HRA') + rec.lta_salary = self._get_salary_rule_amount(payslip, 'LTA') + rec.special_allowance = self._get_salary_rule_amount(payslip, 'SPA') + rec.gross_salary = self._get_salary_rule_amount(payslip, 'GROSS') + else: + # Fallback to contract values + rec.basic_salary = rec.contract_id.wage * 0.4 # Assuming 40% basic + rec.hra_salary = rec.contract_id.wage * 0.2 # Assuming 20% HRA + rec.lta_salary = rec.contract_id.wage * 0.1 # Assuming 10% LTA + rec.special_allowance = rec.contract_id.wage * 0.3 # Remaining as special allowance + rec.gross_salary = rec.contract_id.wage + + + def fetch_salary_components(self): + """fetch salary components from payroll data""" + for rec in self: + if not rec.employee_id or not rec.contract_id: + continue + data = { + 'basic_salary' : {'actual':[],'projected':[]}, + 'hra_salary': {'actual': [], 'projected': []}, + 'lta_salary': {'actual': [], 'projected': []}, + 'special_allowance' : {'actual':[],'projected':[]}, + 'gross_salary' : {'actual':[],'projected':[]} + } + period_lines = rec.period_id.period_line_ids + + for line in period_lines: + basic_salary = float() + hra_salary = float() + lta_salary = float() + special_allowance = float() + gross_salary = float() + payslip = self.env['hr.payslip'].search([ + ('employee_id', '=', rec.employee_id.id), + ('date_from', '>=', line.from_date), + ('date_to', '<=', line.to_date), + ('state', 'in', ['verify', 'done', 'paid']) + ], limit=1) + if payslip: + # Extract salary components from payslip lines + basic_salary = self._get_salary_rule_amount(payslip, 'BASIC') + hra_salary = self._get_salary_rule_amount(payslip, 'HRA') + lta_salary = self._get_salary_rule_amount(payslip, 'LTA') + special_allowance = self._get_salary_rule_amount(payslip, 'SPA') + gross_salary = self._get_salary_rule_amount(payslip, 'GROSS') + else: + payslip = self.env['hr.payslip'].sudo().create({ + 'name': 'Test Payslip', + 'employee_id': rec.employee_id.id, + 'date_from': line.from_date, + 'date_to': line.to_date + }) + payslip.sudo().compute_sheet() + + # Extract salary components from payslip lines + basic_salary = self._get_salary_rule_amount(payslip, 'BASIC') + hra_salary = self._get_salary_rule_amount(payslip, 'HRA') + lta_salary = self._get_salary_rule_amount(payslip, 'LTA') + special_allowance = self._get_salary_rule_amount(payslip, 'SPA') + gross_salary = self._get_salary_rule_amount(payslip, 'GROSS') + + payslip.sudo().action_payslip_cancel() + payslip.sudo().unlink() + + if line.from_date <= rec.period_line.from_date: + data['basic_salary']['actual'].append(basic_salary) + data['hra_salary']['actual'].append(hra_salary) + data['lta_salary']['actual'].append(lta_salary) + data['special_allowance']['actual'].append(special_allowance) + data['gross_salary']['actual'].append(gross_salary) + else: + data['basic_salary']['projected'].append(basic_salary) + data['hra_salary']['projected'].append(hra_salary) + data['lta_salary']['projected'].append(lta_salary) + data['special_allowance']['projected'].append(special_allowance) + data['gross_salary']['projected'].append(gross_salary) + return data + + def _get_salary_rule_amount(self, payslip, rule_code): + """Get amount for a specific salary rule from payslip""" + line = payslip.line_ids.filtered(lambda l: l.salary_rule_id.code == rule_code) + return line.total if line else 0.0 + + @api.depends('employee_id', 'contract_id', 'period_id', 'tax_regime') + def _compute_deductions(self): + """Compute deductions from payroll data""" + for rec in self: + if not rec.employee_id or not rec.contract_id: + continue + + # Get payslip for the period + payslip = self.env['hr.payslip'].search([ + ('employee_id', '=', rec.employee_id.id), + ('date_from', '>=', rec.period_id.from_date), + ('date_to', '<=', rec.period_id.to_date), + ('state', 'in', ['verify', 'done', 'paid']) + ], limit=1) + + fy_start = self.period_id.from_date + fy_end = self.period_id.to_date + total_months = ((fy_end.year - fy_start.year) * 12 + + (fy_end.month - fy_start.month) + 1) + + line_start = self.period_line.from_date + current_month_index = ((line_start.year - fy_start.year) * 12 + + (line_start.month - fy_start.month) + 1) + if payslip: + rec.professional_tax = (self._get_salary_rule_amount(payslip, 'PT'))*current_month_index + rec.nps_employer_contribution = self._get_salary_rule_amount(payslip, 'PFE') + else: + rec.professional_tax = 0.0 + rec.nps_employer_contribution = 0.0 + + # Get standard deduction from slab master + if rec.tax_regime == 'new': + slab_master = self._get_applicable_slab('new', rec.taxpayer_age, rec.residential_status) + else: + slab_master = self._get_applicable_slab('old', rec.taxpayer_age, rec.residential_status) + + rec.standard_deduction = slab_master.standard_deduction if slab_master else ( + 75000 if rec.tax_regime == 'new' else 50000 + ) + + + @api.onchange('employee_id') + def onchange_employee_id(self): + for rec in self: + if rec.employee_id and rec.employee_id.birthday: + age = relativedelta(date.today(), rec.employee_id.birthday).years + rec.taxpayer_age = age + else: + rec.taxpayer_age = rec.taxpayer_age or 0 + + @api.onchange('basic_salary', 'hra_salary') + def onchange_hra_exemption(self): + """Calculate HRA exemption based on salary components""" + for rec in self: + if rec.basic_salary and rec.hra_salary: + # Basic formula for HRA exemption + # Minimum of: + # 1. Actual HRA received + # 2. 50% of basic salary (for metro cities) or 40% (non-metro) + # 3. Rent paid minus 10% of basic salary + + # Assuming metro city for calculation + exemption_option1 = rec.hra_salary + exemption_option2 = 0.5 * rec.basic_salary + # Rent paid would need to be input by user + rent_paid = rec.hra_exemption or 0 + exemption_option3 = max(0, rent_paid - 0.1 * rec.basic_salary) + + rec.hra_exemption = min(exemption_option1, exemption_option2, exemption_option3) + + # --- Tax Calculation Methods --- + def _old_basic_exemption(self, age): + if age < 60: + return 250000.0 + elif age < 80: + return 300000.0 + return 500000.0 + + def _rebate_old(self, taxable, slab_tax): + if self.residential_status != 'RESIDENT': + return 0.0 + if taxable <= 500000.0: + return min(12500.0, slab_tax) + return 0.0 + + def _rebate_new(self, taxable, slab_tax): + if self.residential_status != 'RESIDENT': + return 0.0 + if taxable >= 1200000.0: + needed = taxable - 1200000.0 + if slab_tax >= needed: + return max(0.0, slab_tax - needed) + return 0.0 + else: + return min(60000.0, slab_tax) + + def _apply_surcharge_with_mr(self, slab_master, taxable, tax_after_rebate, regime): + rules = slab_master.rules.sorted('min_income') + table = [(rule.min_income, rule.surcharge_rate) for rule in rules if rule.surcharge_rate > 0] + + threshold = None + rate = 0.0 + for th, rt in table: + if taxable > th: + threshold, rate = th, rt + + if not threshold: + return 0.0, 0.0, tax_after_rebate + + surcharge = tax_after_rebate * rate + total_before_mr = tax_after_rebate + surcharge + + mr = max(0.0, total_before_mr) + tax_with_surcharge = total_before_mr - mr + return surcharge, mr, tax_with_surcharge + + def _compute_tax_old_regime(self, taxable): + # Get applicable slab + slab_master = self._get_applicable_slab('old', self.taxpayer_age, self.residential_status) + + # Compute slab tax + slab_tax = self._compute_tax_using_slab(taxable, slab_master) + + # Apply rebate + rebate = self._rebate_old(taxable, slab_tax) + tax_after_rebate = max(0.0, slab_tax - rebate) + + # Apply surcharge and marginal relief + surcharge, marginal_relief, tax_with_surcharge = self._apply_surcharge_with_mr( + slab_master, taxable, tax_after_rebate, regime='old' + ) + + # Apply cess + rules = slab_master.rules.sorted('min_income') + + cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable] + cess = tax_with_surcharge * cess_rate[0]/100 + + total_tax = tax_with_surcharge + cess + + return { + 'taxable_income': taxable, + 'slab_tax': slab_tax, + 'rebate_87a': rebate, + 'tax_after_rebate': tax_after_rebate, + 'surcharge': surcharge, + 'marginal_relief': marginal_relief, + 'tax_with_surcharge': tax_with_surcharge, + 'cess_4pct': cess, + 'total_tax': total_tax + } + + def _compute_tax_new_regime(self, taxable): + # Get applicable slab (new regime doesn't depend on age) + slab_master = self._get_applicable_slab('new', self.taxpayer_age, self.residential_status) + + # Compute slab tax + slab_tax = self._compute_tax_using_slab(taxable, slab_master) + + # Apply rebate + rebate = self._rebate_new(taxable, slab_tax) + tax_after_rebate = max(0.0, slab_tax - rebate) + + # Apply surcharge and marginal relief + surcharge, marginal_relief, tax_with_surcharge = self._apply_surcharge_with_mr( + slab_master, taxable, tax_after_rebate, regime='new' + ) + + rules = slab_master.rules.sorted('min_income') + cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable] + # Apply cess + cess = tax_with_surcharge * cess_rate[0]/100 + total_tax = tax_with_surcharge + cess + + return { + 'taxable_income': taxable, + 'slab_tax': slab_tax, + 'rebate_87a': rebate, + 'tax_after_rebate': tax_after_rebate, + 'surcharge': surcharge, + 'marginal_relief': marginal_relief, + 'tax_with_surcharge': tax_with_surcharge, + 'cess_4pct': cess, + 'total_tax': total_tax + } + + def _compute_house_property_income(self): + """Returns net house property income""" + rent = float(self.rental_income or 0.0) + if rent > 0.0: + nav = rent + std_ded = 0.30 * nav + interest_allowed = float(self.interest_home_loan_letout or 0.0) + hp_income = nav - std_ded - interest_allowed + else: + interest_allowed = min(float(self.interest_home_loan_self or 0.0), 200000.0) + hp_income = -interest_allowed + return hp_income + + def _prepare_income_tax_data(self): + """Prepare data for the tax statement report""" + today = date.today() + fy_start = self.period_id.from_date + fy_end = self.period_id.to_date + total_months = ((fy_end.year - fy_start.year) * 12 + + (fy_end.month - fy_start.month) + 1) + + line_start = self.period_line.from_date + current_month_index = ((line_start.year - fy_start.year) * 12 + + (line_start.month - fy_start.month) + 1) + if today.month >= 4: + fy_start = date(today.year, 4, 1) + fy_end = date(today.year + 1, 3, 31) + else: + fy_start = date(today.year - 1, 4, 1) + fy_end = date(today.year, 3, 31) + + # Calculate taxable income for both regimes + # Old regime + old_deductions = ( + self.standard_deduction + + self.hra_exemption + + self.professional_tax + + self.ded_80C + + self.ded_80CCD1B + + self.ded_80D_self + + self.ded_80D_parents + + self.ded_80G + + self.ded_other + + self.nps_employer_contribution + ) + + # House property income + hp_income = self._compute_house_property_income() + + # Taxable income for old regime + taxable_old = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction) + + # New regime - fewer deductions + new_deductions = ( + self.standard_deduction + + self.professional_tax + + self.nps_employer_contribution + ) + + # Taxable income for new regime + + taxable_new = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction) + + # Compute tax for both regimes + tax_result_old = self._compute_tax_old_regime(taxable_old) + tax_result_new = self._compute_tax_new_regime(taxable_new) + + # Determine which regime to use + if self.tax_regime == 'old': + tax_result = tax_result_old + taxable_income = taxable_old + chosen = 'old' + else: + tax_result = tax_result_new + taxable_income = taxable_new + chosen = 'new' + + # Calculate tax savings + tax_savings = abs(tax_result_old['total_tax'] - tax_result_new['total_tax']) + beneficial_regime = 'old' if tax_result_old['total_tax'] < tax_result_new['total_tax'] else 'new' + + # Prepare data structure matching screenshot format + # Financial year (period_id) + fy_start = self.period_id.from_date + fy_end = self.period_id.to_date + total_months = ((fy_end.year - fy_start.year) * 12 + + (fy_end.month - fy_start.month) + 1) + + # Current month (period_line) + line_start = self.period_line.from_date + current_month_index = ((line_start.year - fy_start.year) * 12 + + (line_start.month - fy_start.month) + 1) + tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10) + birthday = self.employee_id.birthday + if birthday: + diff = relativedelta(date.today(), birthday) + years_months = f"{diff.years} years {diff.months} months" + else: + years_months = "N/A" + month_age = str(self.period_line.name)+ " / " + str(years_months) + salary_components_data = self.fetch_salary_components() + data = { + 'financial_year': f"{fy_start.year}-{fy_end.year}", + 'assessment_year': fy_end.year + 1, + 'report_time': today.strftime('%d-%m-%Y %H:%M'), + 'user': 'ESS', + 'emp_code': self.employee_id.employee_id, + 'company_name': self.employee_id.company_id.name, + 'profile': { + 'name': self.taxpayer_name or (self.employee_id.name if self.employee_id else ''), + 'age': self.taxpayer_age, + 'residential_status': self.residential_status, + 'parent_age': self.parent_age, + 'doj': self.employee_id.doj, + 'pan': self.employee_id.pan_no if hasattr(self.employee_id, 'pan_no') else '', + 'gender': self.employee_id.gender, + 'month_age': month_age + }, + + 'regime_info': { + 'lock_regime': self.lock_regime, + 'tax_regime': 'NEW REGIME' if self.tax_regime == 'new' else 'OLD REGIME', + }, + + 'salary_components': { + 'basic': {'actual': sum(salary_components_data['basic_salary']['actual']), 'projected': sum(salary_components_data['basic_salary']['projected']), 'total':sum(salary_components_data['basic_salary']['actual']) + sum(salary_components_data['basic_salary']['projected'])}, + 'house_rent': {'actual': sum(salary_components_data['hra_salary']['actual']), 'projected': sum(salary_components_data['hra_salary']['projected']), 'total':sum(salary_components_data['hra_salary']['actual']) + sum(salary_components_data['hra_salary']['projected'])}, + 'lta': {'actual': sum(salary_components_data['lta_salary']['actual']), 'projected': sum(salary_components_data['lta_salary']['projected']), 'total':sum(salary_components_data['lta_salary']['actual']) + sum(salary_components_data['lta_salary']['projected'])}, + 'special_allowance':{'actual': sum(salary_components_data['special_allowance']['actual']), 'projected': sum(salary_components_data['special_allowance']['projected']), 'total':sum(salary_components_data['special_allowance']['actual']) + sum(salary_components_data['special_allowance']['projected'])}, + 'perquisites': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months}, + 'reimbursement': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months}, + 'gross_salary': {'actual': sum(salary_components_data['gross_salary']['actual']), 'projected': sum(salary_components_data['gross_salary']['projected']), 'total':sum(salary_components_data['gross_salary']['actual']) + sum(salary_components_data['gross_salary']['projected'])}, + 'net_salary': {'actual': self.gross_salary * current_month_index, 'projected': self.gross_salary * (total_months - current_month_index), + 'total': self.gross_salary * total_months} + }, + + 'deductions': { + 'professional_tax': self.professional_tax, + 'standard_deduction': self.standard_deduction, + 'nps_employer': self.nps_employer_contribution, + 'hra_exemption': self.hra_exemption, + 'interest_home_loan': self.interest_home_loan_self + self.interest_home_loan_letout, + 'ded_80C': self.ded_80C, + 'ded_80CCD1B': self.ded_80CCD1B, + 'ded_80D_self': self.ded_80D_self, + 'ded_80D_parents': self.ded_80D_parents, + 'ded_80G': self.ded_80G, + 'ded_other': self.ded_other, + 'total_deductions': old_deductions if self.tax_regime == 'old' else new_deductions, + }, + + 'income_details': { + 'gross_salary': self.gross_salary, + 'other_income': self.other_income, + 'house_property_income': hp_income, + 'gross_total_income': (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction, + }, + + 'taxable_income': { + 'old_regime': taxable_old, + 'new_regime': taxable_new, + 'current_regime': taxable_income, + }, + + 'tax_computation': tax_result, + 'regime_used': chosen, + + 'comparison': { + 'old_regime_tax': tax_result_old['total_tax'], + 'new_regime_tax': tax_result_new['total_tax'], + 'tax_savings': tax_savings, + 'beneficial_regime': beneficial_regime, + } + } + + return {'data': data} + + def action_generate_report(self): + report_data = self._prepare_income_tax_data() + + return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action( + self, + data={'report_data': report_data}, + ) \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/payroll_periods.py b/addons_extensions/employee_it_declaration/models/payroll_periods.py new file mode 100644 index 000000000..6e6a31a91 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/payroll_periods.py @@ -0,0 +1,61 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError +from datetime import datetime, timedelta +import calendar + + +class PayrollPeriod(models.Model): + _name = 'payroll.period' + _description = 'Payroll Period' + _rec_name = 'name' + _sql_constraints = [ + ('unique_name', 'unique(name)', 'The name must be unique.') + ] + + from_date = fields.Date(string="From Date", required=True) + to_date = fields.Date(string="To Date", required=True) + name = fields.Char(string="Name", required=True) + period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods") + + @api.onchange('from_date', 'to_date') + def onchange_from_to_date(self): + for rec in self: + if rec.from_date and rec.to_date: + rec.name = f"{rec.from_date.year}-{rec.to_date.year}" + + + def action_generate_month_lines(self): + self.ensure_one() + if not (self.from_date and self.to_date): + raise ValidationError("Please set both From Date and To Date.") + + lines = [] + current = self.from_date.replace(day=1) + while current <= self.to_date: + end_of_month = current.replace(day=calendar.monthrange(current.year, current.month)[1]) + if end_of_month > self.to_date: + end_of_month = self.to_date + + lines.append((0, 0, { + 'from_date': current, + 'to_date': end_of_month, + 'name': f"{current.strftime('%b')} - {str(current.year)[-2:]}" + })) + + if current.month == 12: + current = current.replace(year=current.year + 1, month=1) + else: + current = current.replace(month=current.month + 1) + + self.period_line_ids = lines + + +class PayrollPeriodLine(models.Model): + _name = 'payroll.period.line' + _description = 'Payroll Period Line' + _rec_name = 'name' + + period_id = fields.Many2one('payroll.period', string="Payroll Period", required=True, ondelete='cascade') + from_date = fields.Date(string="From Date") + to_date = fields.Date(string="To Date") + name = fields.Char(string="Name") diff --git a/addons_extensions/employee_it_declaration/models/slab_master.py b/addons_extensions/employee_it_declaration/models/slab_master.py new file mode 100644 index 000000000..e27bebca3 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/slab_master.py @@ -0,0 +1,72 @@ +from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError + +class IncomeTaxSlabMaster(models.Model): + _name = 'it.slab.master' + _description = 'Income Tax Slab Master' + _rec_name = 'name' + _sql_constraints = [ + ( + 'unique_slab', + 'unique(regime, age_category, residence_type)', + 'Slab must be unique for the same Regime, Age Category, and Residence Type!' + ) + ] + + name = fields.Char(string="Slab Name", required=True) + regime = fields.Selection([ + ('old', 'Old Tax Regime'), + ('new', 'New Tax Regime') + ], required=True) + age_category = fields.Selection([ + ('below_60', 'Below 60 Years'), + ('60_to_80', '60-80 Years'), + ('above_80', 'Above 80 Years') + ], required=True) + residence_type = fields.Selection([ + ('resident', 'Resident'), + ('non_resident', 'Non Resident'), + ('both', 'Both') + ], required=True) + standard_deduction = fields.Float(string="Standard Deduction") + active = fields.Boolean(default=True) + rules = fields.One2many('it.slab.master.rules','slab_id', string="Slab Rules") + +class IncomeTaxSlabMasterRules(models.Model): + _name = 'it.slab.master.rules' + _description = 'Income Tax slab rules' + _rec_name = 'slab_id' + _sql_constraints = [ + ( + 'check_min_max_income', + 'CHECK (max_income IS NULL OR max_income > min_income)', + 'Max Income must be greater than Min Income!' + ) + ] + + min_income = fields.Float(string="Min Income (₹)", required=True) + max_income = fields.Float(string="Max Income (₹)") + tax_rate = fields.Float(string="Tax Rate (%)", required=True) + fixed_amount = fields.Float(string="Fixed Amount (₹)") + excess_threshold = fields.Float(string="Excess Threshold (₹)") + surcharge_rate = fields.Float(string="Surcharge Rate (%)") + cess_rate = fields.Float(string="Health & Education (%)", default=4.0) + slab_id = fields.Many2one('it.slab.master') + + + + @api.constrains('min_income', 'max_income', 'slab_id') + def _check_overlap(self): + """Ensure no overlapping or duplicate ranges within the same slab""" + for rule in self: + domain = [ + ('slab_id', '=', rule.slab_id.id), + ('id', '!=', rule.id) + ] + others = self.search(domain) + for other in others: + if not (rule.max_income and other.min_income >= rule.max_income) and \ + not (other.max_income and rule.min_income >= other.max_income): + raise ValidationError( + f"Income ranges overlap with another slab rule: {other.min_income} - {other.max_income}" + ) diff --git a/addons_extensions/employee_it_declaration/report/it_tax_template.xml b/addons_extensions/employee_it_declaration/report/it_tax_template.xml new file mode 100644 index 000000000..646ce30a0 --- /dev/null +++ b/addons_extensions/employee_it_declaration/report/it_tax_template.xml @@ -0,0 +1,726 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/report/report_action.xml b/addons_extensions/employee_it_declaration/report/report_action.xml new file mode 100644 index 000000000..82223abde --- /dev/null +++ b/addons_extensions/employee_it_declaration/report/report_action.xml @@ -0,0 +1,12 @@ + + + + diff --git a/addons_extensions/employee_it_declaration/security/ir.model.access.csv b/addons_extensions/employee_it_declaration/security/ir.model.access.csv new file mode 100644 index 000000000..8aca96ed2 --- /dev/null +++ b/addons_extensions/employee_it_declaration/security/ir.model.access.csv @@ -0,0 +1,65 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + + +access_payroll_period_user,payroll.period,model_payroll_period,,1,1,1,1 +access_payroll_period_line_user,payroll.period.line,model_payroll_period_line,,1,1,1,1 + +access_it_investment_type,it.investment.type,model_it_investment_type,,1,1,1,1 + + +access_past_employment_investment_type,past_employment.investment.type,model_past_employment_investment_type,,1,1,1,1 +access_us80c_investment_type,us80c.investment.type,model_us80c_investment_type,,1,1,1,1 +access_us80d_investment_type,us80d.investment.type,model_us80d_investment_type,,1,1,1,1 +access_us10_investment_type,us10.investment.type,model_us10_investment_type,,1,1,1,1 +access_us80g_investment_type,us80g.investment.type,model_us80g_investment_type,,1,1,1,1 +access_chapter_via_investment_type,chapter.via.investment.type,model_chapter_via_investment_type,,1,1,1,1 +access_us17_investment_type,us17.investment.type,model_us17_investment_type,,1,1,1,1 +access_other_il_investment_type,other.il.investment.type,model_other_il_investment_type,,1,1,1,1 +access_other_declaration_investment_type,other.declaration.investment.type,model_other_declaration_investment_type,,1,1,1,1 + + +access_investment_costings,investment.costings,model_investment_costings,,1,1,1,1 + +access_past_employment_costing_type,past_employment.costing.type,model_past_employment_costing_type,,1,1,1,1 +access_us80c_costing_type,us80c.costing.type,model_us80c_costing_type,,1,1,1,1 +access_us80d_costing_type,us80d.costing.type,model_us80d_costing_type,,1,1,1,1 +access_us10_costing_type,us10.costing.type,model_us10_costing_type,,1,1,1,1 +access_us80g_costing_type,us80g.costing.type,model_us80g_costing_type,,1,1,1,1 +access_chapter_via_costing_type,chapter.via.costing.type,model_chapter_via_costing_type,,1,1,1,1 +access_us17_costing_type,us17.costing.type,model_us17_costing_type,,1,1,1,1 +access_other_il_costing_type,other.il.costing.type,model_other_il_costing_type,,1,1,1,1 +access_other_declaration_costing_type,other.declaration.costing.type,model_other_declaration_costing_type,,1,1,1,1 + + +access_emp_it_declaration_user,emp.it.declarations,model_emp_it_declaration,base.group_user,1,1,1,1 + + +access_children_education,access.children.education,model_children_education,base.group_user,1,1,1,1 +access_children_education_costing,access.children.education.costing,model_children_education_costing,base.group_user,1,1,1,1 + +access_us80c_insurance_line,access.us80c.insurance.line,model_us80c_insurance_line,,1,1,1,1 +access_employee_life_insurance,access.employee.life.insurance,model_employee_life_insurance,,1,1,1,1 + +access_nsc_declaration_line_user,nsc.declaration.line,model_nsc_declaration_line,base.group_user,1,1,1,1 +access_nsc_entry_user,nsc.entry,model_nsc_entry,base.group_user,1,1,1,1 + + +access_self_occupied_property_user,self.occupied.property,model_self_occupied_property,base.group_user,1,1,1,1 + +access_letout_house_property_user,access.letout.house.property.user,model_letout_house_property,base.group_user,1,1,1,1 + + +access_nsc_interest_line_user,nsc.interest.line,model_nsc_interest_line,base.group_user,1,1,1,1 +access_nsc_interest_entry_user,nsc.interest.entry,model_nsc_interest_entry,base.group_user,1,1,1,1 + +access_house_rent_declaration_user,access.house.rent.declaration.user,model_house_rent_declaration,base.group_user,1,1,1,1 + +access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0 +access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0 + +access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1 +access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1 + + +access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1 +access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1 diff --git a/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml new file mode 100644 index 000000000..304b1933f --- /dev/null +++ b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml @@ -0,0 +1,355 @@ + + + + + emp.it.declaration.list + emp.it.declaration + + + + + + + + + + + + + emp.it.declarations.form + emp.it.declaration + +
+ +
+
+ +
+
+ + + + + + + + + + + +
+
+ +
+