From 604d556501a35aa99c749ff08ee29045afdff7de Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Wed, 3 Jun 2026 10:48:17 +0530 Subject: [PATCH] Recruitment changes --- .../employee_it_declaration/__init__.py | 5 +- .../employee_it_declaration/__manifest__.py | 9 +- .../controllers/__init__.py | 1 + .../controllers/main.py | 63 + .../models/__init__.py | 15 +- .../models/emp_it_declaration.py | 235 ++- .../models/employee_payslip_download_wiz.py | 127 ++ .../models/investment_costings.py | 79 +- .../report/report_action.xml | 24 +- .../security/ir.model.access.csv | 11 +- .../views/emp_it_declaration.xml | 137 +- ...employee_payslip_download_wizard_views.xml | 61 + .../views/it_tax_menu_and_wizard_view.xml | 7 +- .../views/report_it_tax_statement.xml | 186 ++- .../wizards/children_education_costing.py | 14 +- .../wizards/employee_life_insurance.py | 14 +- .../wizards/letout_house_property.py | 9 +- .../wizards/nsc_declaration.py | 14 +- .../wizards/nsc_income_loss.py | 14 +- .../wizards/self_occupied_property.py | 9 +- .../controllers/emp_jod_controller.py | 4 +- .../employee_jod/models/emp_jod.py | 53 +- .../employee_jod/views/emp_jod.xml | 2 +- .../hr_employee_extended/__manifest__.py | 3 +- .../models/employer_history.py | 15 + .../wizards/html_preview_wizard.py | 16 + .../wizards/html_preview_wizard.xml | 25 + .../wizard/hr_recruitment_auto_doc_wizard.py | 388 ++++- .../hr_recruitment_extended/__manifest__.py | 48 +- .../controllers/controllers.py | 173 ++- .../hr_recruitment_extended/data/data.xml | 3 +- .../data/mail_template.xml | 237 +++- .../models/__init__.py | 9 +- .../models/applicant_request_forms.py | 83 ++ .../models/hr_applicant.py | 287 ++-- .../models/hr_applicant_stage_comment.py | 21 + .../models/hr_job_recruitment.py | 494 +++---- .../models/hr_recruitment.py | 112 +- .../hr_recruitment_extended/models/stages.py | 4 +- .../security/ir.model.access.csv | 11 +- .../security/security.xml | 17 +- .../js/pre_onboarding_attachment_requests.js | 47 +- .../static/src/js/stage_comment_statusbar.js | 27 + .../static/src/scss/hr_applicant_hold.scss | 12 + .../src/xml/stage_comment_statusbar.xml | 14 + .../views/candidate_experience.xml | 1 - .../views/hr_applicant_views.xml | 333 +++-- .../hr_employee_education_employer_family.xml | 44 +- .../views/hr_job_recruitment.xml | 165 +-- .../views/hr_recruitment.xml | 36 +- .../hr_recruitment_application_templates.xml | 22 +- .../views/requisitions.xml | 3 +- .../views/res_partner.xml | 4 +- .../hr_recruitment_extended/views/stages.xml | 4 +- .../wizards/__init__.py | 1 + .../wizards/applicant_offer_mail_wizard.py | 178 +++ .../wizards/applicant_offer_mail_wizard.xml | 47 + .../wizards/applicant_stage_comment_wizard.py | 45 + .../applicant_stage_comment_wizard.xml | 30 + .../post_onboarding_attachment_wizard.py | 46 +- .../menu_control_center/__manifest__.py | 2 +- .../menu_control_center/controllers/main.py | 30 +- .../menu_control_center/models/menu.py | 19 + .../menu_control_center/views/groups.xml | 4 +- .../static/src/js/module_switcher.js | 8 +- addons_extensions/offer_letters/__init__.py | 4 +- .../offer_letters/__manifest__.py | 10 +- .../offer_letters/controllers/__init__.py | 1 + .../offer_letters/controllers/main.py | 77 + .../offer_letters/data/mail_template.xml | 64 + .../offer_letters/models/__init__.py | 4 +- .../offer_letters/models/hr_applicant.py | 52 + .../offer_letters/models/hr_candidate.py | 33 + .../offer_letters/models/offer_letter.py | 42 +- .../report/offer_letter_template.xml | 1255 ++++++++++++----- .../security/ir.model.access.csv | 2 + .../views/hr_applicant_offer_views.xml | 99 ++ .../offer_letters/views/menu_views.xml | 2 +- .../views/offer_letter_views.xml | 20 +- .../views/offer_response_templates.xml | 46 + .../offer_letters/wizards/__init__.py | 3 + .../wizards/applicant_offer_mail_wizard.py | 225 +++ .../wizards/applicant_offer_mail_wizard.xml | 49 + .../wizards/offer_letter_reject_wizard.py | 14 + .../wizards/offer_letter_reject_wizard.xml | 19 + .../wizards/offer_release_request_wizard.py | 118 ++ .../wizards/offer_release_request_wizard.xml | 30 + .../TASK_PROJECT_BY_MODULE.md | 277 ---- .../models/res_config_settings.py | 7 +- .../views/res_config_settings.xml | 6 +- .../attachment_preview_popup.xml | 8 +- .../web_portal_form_custom/__init__.py | 202 +++ .../web_portal_form_custom/__manifest__.py | 28 + .../data/mail_template_data.xml | 37 + .../web_portal_form_custom/models/__init__.py | 2 + .../models/application_candidate_changes.py | 217 +++ .../models/survey_line.py | 188 +++ .../security/ir.model.access.csv | 4 + .../static/src/css/candidate_card.css | 20 + ... - importance of teamwork [ARTICLE]-3.jpeg | Bin 0 -> 72798 bytes .../views/hr_applicant_form.xml | 592 ++++++++ .../views/hr_candidate_form.xml | 194 +++ .../views/job_portal_changes.xml | 447 ++++++ .../wizards/__init__.py | 1 + .../wizards/survey_invite.py | 85 ++ .../weekly_timesheets/__init__.py | 1 + .../weekly_timesheets/__manifest__.py | 24 + .../weekly_timesheets/models/__init__.py | 2 + .../models/week_timesheet.py | 113 ++ .../models/weekly_period_timesheets.py | 444 ++++++ .../security/ir.model.access.csv | 5 + .../views/hr_timesheet_inherit.xml | 28 + .../views/timesheet_inherit.xml | 32 + .../views/week_timesheet.xml | 219 +++ .../views/weekly_period_timesheets.xml | 124 ++ 115 files changed, 7793 insertions(+), 1844 deletions(-) create mode 100644 addons_extensions/employee_it_declaration/controllers/__init__.py create mode 100644 addons_extensions/employee_it_declaration/controllers/main.py create mode 100644 addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py create mode 100644 addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml create mode 100644 addons_extensions/hr_employee_extended/wizards/html_preview_wizard.py create mode 100644 addons_extensions/hr_employee_extended/wizards/html_preview_wizard.xml create mode 100644 addons_extensions/hr_recruitment_extended/models/applicant_request_forms.py create mode 100644 addons_extensions/hr_recruitment_extended/models/hr_applicant_stage_comment.py create mode 100644 addons_extensions/hr_recruitment_extended/static/src/js/stage_comment_statusbar.js create mode 100644 addons_extensions/hr_recruitment_extended/static/src/scss/hr_applicant_hold.scss create mode 100644 addons_extensions/hr_recruitment_extended/static/src/xml/stage_comment_statusbar.xml create mode 100644 addons_extensions/hr_recruitment_extended/wizards/applicant_offer_mail_wizard.py create mode 100644 addons_extensions/hr_recruitment_extended/wizards/applicant_offer_mail_wizard.xml create mode 100644 addons_extensions/hr_recruitment_extended/wizards/applicant_stage_comment_wizard.py create mode 100644 addons_extensions/hr_recruitment_extended/wizards/applicant_stage_comment_wizard.xml create mode 100644 addons_extensions/offer_letters/controllers/__init__.py create mode 100644 addons_extensions/offer_letters/controllers/main.py create mode 100644 addons_extensions/offer_letters/data/mail_template.xml create mode 100644 addons_extensions/offer_letters/models/hr_applicant.py create mode 100644 addons_extensions/offer_letters/models/hr_candidate.py create mode 100644 addons_extensions/offer_letters/views/hr_applicant_offer_views.xml create mode 100644 addons_extensions/offer_letters/views/offer_response_templates.xml create mode 100644 addons_extensions/offer_letters/wizards/__init__.py create mode 100644 addons_extensions/offer_letters/wizards/applicant_offer_mail_wizard.py create mode 100644 addons_extensions/offer_letters/wizards/applicant_offer_mail_wizard.xml create mode 100644 addons_extensions/offer_letters/wizards/offer_letter_reject_wizard.py create mode 100644 addons_extensions/offer_letters/wizards/offer_letter_reject_wizard.xml create mode 100644 addons_extensions/offer_letters/wizards/offer_release_request_wizard.py create mode 100644 addons_extensions/offer_letters/wizards/offer_release_request_wizard.xml delete mode 100644 addons_extensions/project_task_timesheet_extended/TASK_PROJECT_BY_MODULE.md create mode 100644 addons_extensions/web_portal_form_custom/__init__.py create mode 100644 addons_extensions/web_portal_form_custom/__manifest__.py create mode 100644 addons_extensions/web_portal_form_custom/data/mail_template_data.xml create mode 100644 addons_extensions/web_portal_form_custom/models/__init__.py create mode 100644 addons_extensions/web_portal_form_custom/models/application_candidate_changes.py create mode 100644 addons_extensions/web_portal_form_custom/models/survey_line.py create mode 100644 addons_extensions/web_portal_form_custom/security/ir.model.access.csv create mode 100644 addons_extensions/web_portal_form_custom/static/src/css/candidate_card.css create mode 100644 addons_extensions/web_portal_form_custom/static/src/img/Delivery URL - BetterUp - importance of teamwork [ARTICLE]-3.jpeg create mode 100644 addons_extensions/web_portal_form_custom/views/hr_applicant_form.xml create mode 100644 addons_extensions/web_portal_form_custom/views/hr_candidate_form.xml create mode 100644 addons_extensions/web_portal_form_custom/views/job_portal_changes.xml create mode 100644 addons_extensions/web_portal_form_custom/wizards/__init__.py create mode 100644 addons_extensions/web_portal_form_custom/wizards/survey_invite.py create mode 100644 addons_extensions/weekly_timesheets/__init__.py create mode 100644 addons_extensions/weekly_timesheets/__manifest__.py create mode 100644 addons_extensions/weekly_timesheets/models/__init__.py create mode 100644 addons_extensions/weekly_timesheets/models/week_timesheet.py create mode 100644 addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py create mode 100644 addons_extensions/weekly_timesheets/security/ir.model.access.csv create mode 100644 addons_extensions/weekly_timesheets/views/hr_timesheet_inherit.xml create mode 100644 addons_extensions/weekly_timesheets/views/timesheet_inherit.xml create mode 100644 addons_extensions/weekly_timesheets/views/week_timesheet.xml create mode 100644 addons_extensions/weekly_timesheets/views/weekly_period_timesheets.xml diff --git a/addons_extensions/employee_it_declaration/__init__.py b/addons_extensions/employee_it_declaration/__init__.py index cf2479fe8..4ba794020 100644 --- a/addons_extensions/employee_it_declaration/__init__.py +++ b/addons_extensions/employee_it_declaration/__init__.py @@ -1,2 +1,3 @@ -from . import models -from . import wizards \ No newline at end of file +from . import models +from . import wizards +from . import controllers diff --git a/addons_extensions/employee_it_declaration/__manifest__.py b/addons_extensions/employee_it_declaration/__manifest__.py index 504b2607a..8368b2904 100644 --- a/addons_extensions/employee_it_declaration/__manifest__.py +++ b/addons_extensions/employee_it_declaration/__manifest__.py @@ -45,10 +45,11 @@ '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/hr_tds_calculation.xml', + 'report/report_action.xml', + 'report/it_tax_template.xml', + 'views/it_tax_menu_and_wizard_view.xml', + 'views/employee_payslip_download_wizard_views.xml', + 'wizards/hr_tds_calculation.xml', 'wizards/children_education_costing.xml', 'wizards/employee_life_insurance.xml', 'wizards/nsc_declaration.xml', diff --git a/addons_extensions/employee_it_declaration/controllers/__init__.py b/addons_extensions/employee_it_declaration/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/addons_extensions/employee_it_declaration/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons_extensions/employee_it_declaration/controllers/main.py b/addons_extensions/employee_it_declaration/controllers/main.py new file mode 100644 index 000000000..743002ff1 --- /dev/null +++ b/addons_extensions/employee_it_declaration/controllers/main.py @@ -0,0 +1,63 @@ +import io +import zipfile + +from odoo import _ +from odoo.exceptions import AccessError, UserError +from odoo.http import Controller, content_disposition, request, route + + +class EmployeePayslipDownloadController(Controller): + + @route('/employee_it_declaration/my_payslips/', type='http', auth='user') + def download_my_payslips(self, wizard_id, **kwargs): + wizard = request.env['employee.payslip.download.wizard'].browse(wizard_id) + if not wizard.exists() or wizard.create_uid != request.env.user: + return request.not_found() + + try: + payslips = wizard._get_payslips_for_download() + except (AccessError, UserError): + return request.not_found() + + if wizard.download_type == 'single': + payslip = payslips[:1] + report, pdf_content = wizard._get_pdf_content(payslip) + headers = [ + ('Content-Type', 'application/pdf'), + ('Content-Length', len(pdf_content)), + ('Content-Disposition', content_disposition(wizard._get_pdf_filename(payslip, report))), + ] + return request.make_response(pdf_content, headers=headers) + + zip_buffer = io.BytesIO() + used_filenames = set() + with zipfile.ZipFile(zip_buffer, 'w', compression=zipfile.ZIP_DEFLATED) as payslip_zip: + for payslip in payslips: + report, pdf_content = wizard._get_pdf_content(payslip) + filename = self._deduplicate_filename(wizard._get_pdf_filename(payslip, report), used_filenames) + payslip_zip.writestr(filename, pdf_content) + + zip_content = zip_buffer.getvalue() + headers = [ + ('Content-Type', 'application/zip'), + ('Content-Length', len(zip_content)), + ('Content-Disposition', content_disposition(wizard._get_zip_filename())), + ] + return request.make_response(zip_content, headers=headers) + + @staticmethod + def _deduplicate_filename(filename, used_filenames): + if filename not in used_filenames: + used_filenames.add(filename) + return filename + + stem, extension = filename.rsplit('.', 1) if '.' in filename else (filename, '') + counter = 2 + while True: + candidate = _('%(stem)s (%(counter)s)', stem=stem, counter=counter) + if extension: + candidate = '%s.%s' % (candidate, extension) + if candidate not in used_filenames: + used_filenames.add(candidate) + return candidate + counter += 1 diff --git a/addons_extensions/employee_it_declaration/models/__init__.py b/addons_extensions/employee_it_declaration/models/__init__.py index f25fe19db..628d086d5 100644 --- a/addons_extensions/employee_it_declaration/models/__init__.py +++ b/addons_extensions/employee_it_declaration/models/__init__.py @@ -1,7 +1,8 @@ -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 +from . import payroll_periods +from . import investment_types +from . import emp_it_declaration +from . import investment_costings +from . import slab_master +from . import it_tax_statement +from . import it_tax_statement_wiz +from . import employee_payslip_download_wiz diff --git a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py index 19b3db83a..2df9f0f4d 100644 --- a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py +++ b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py @@ -1,7 +1,42 @@ -from odoo import models, fields, api - - -class EmpITDeclaration(models.Model): +from urllib.parse import urlencode + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class ITDeclarationSubmittedLockMixin(models.AbstractModel): + _name = 'it.declaration.submitted.lock.mixin' + _description = 'IT Declaration Submitted Lock Mixin' + + def _get_related_it_declarations(self): + if 'it_declaration_id' in self._fields: + return self.mapped('it_declaration_id') + if 'child_education_id' in self._fields: + return self.mapped('child_education_id.it_declaration_id') + if 'parent_id' in self._fields: + return self.mapped('parent_id.it_declaration_id') + return self.env['emp.it.declaration'] + + def _check_it_declaration_is_editable(self): + if any(declaration.state == 'submitted' for declaration in self._get_related_it_declarations()): + raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.')) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._check_it_declaration_is_editable() + return records + + def write(self, vals): + self._check_it_declaration_is_editable() + return super().write(vals) + + def unlink(self): + self._check_it_declaration_is_editable() + return super().unlink() + + +class EmpITDeclaration(models.Model): _name = 'emp.it.declaration' _rec_name = 'employee_id' _description = "IT Declaration" @@ -34,11 +69,17 @@ class EmpITDeclaration(models.Model): else: rec.display_period_label = "" - tax_regime = fields.Selection([ - ('new', 'New Regime'), - ('old', 'Old Regime') - ], string="Tax Regime", required=True, default='new') - + tax_regime = fields.Selection([ + ('new', 'New Regime'), + ('old', 'Old Regime') + ], string="Tax Regime", required=True, default='new') + state = fields.Selection([ + ('draft', 'Draft'), + ('submitted', 'Submitted'), + ], string="Status", default='draft', required=True, copy=False) + return_reason = fields.Text(string="Return Reason", copy=False) + is_payroll_manager = fields.Boolean(compute='_compute_is_payroll_manager') + total_investment = fields.Float(string='Total Investment') costing_details_generated = fields.Boolean(default=False) @@ -50,7 +91,29 @@ class EmpITDeclaration(models.Model): string='Visible Investment Costings', ) house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing") - is_section_open = fields.Boolean() + is_section_open = fields.Boolean() + + def _compute_is_payroll_manager(self): + is_manager = self.env.user.has_group('hr_payroll.group_hr_payroll_manager') + for rec in self: + rec.is_payroll_manager = is_manager + + def _check_submitted_write_allowed(self, vals): + protected_vals = set(vals) - {'state', 'return_reason'} + submitted_records = self.filtered(lambda rec: rec.state == 'submitted') + if submitted_records and protected_vals: + raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.')) + if submitted_records and set(vals) & {'state', 'return_reason'} and not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'): + raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.')) + + def write(self, vals): + self._check_submitted_write_allowed(vals) + return super().write(vals) + + def unlink(self): + if any(rec.state == 'submitted' for rec in self): + raise UserError(_('Submitted IT declarations cannot be deleted.')) + return super().unlink() @api.depends('costing_details_generated','investment_costing_ids') def _compute_investment_costing(self): for rec in self: @@ -346,3 +409,155 @@ class EmpITDeclaration(models.Model): rec._ensure_investment_costing_records() rec._update_investment_amounts() rec.costing_details_generated = True + + def action_submit(self): + for rec in self: + rec._update_investment_amounts() + rec.write({ + 'state': 'submitted', + 'return_reason': False, + }) + + def action_return_to_draft(self): + for rec in self: + if not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'): + raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.')) + if not rec.return_reason: + raise UserError(_('Please enter a return reason before returning the declaration to draft.')) + rec.state = 'draft' + + def action_download_submission_pdf(self): + self.ensure_one() + if self.state != 'submitted': + raise UserError(_('You can download the IT declaration PDF only after it is submitted.')) + return self.env.ref('employee_it_declaration.action_report_it_tax_statement').report_action(self) + + def _get_regime_label(self): + self.ensure_one() + return dict(self._fields['tax_regime'].selection).get(self.tax_regime, self.tax_regime) + + def _get_binary_link(self, record, field_name, filename_field=False): + if not record or not record[field_name]: + return False + params = { + 'model': record._name, + 'id': record.id, + 'field': field_name, + 'download': 'true', + } + if filename_field: + params['filename_field'] = filename_field + return '/web/content?%s' % urlencode(params) + + def _prepare_report_line(self, line): + name = line.investment_type_line_id.name if line.investment_type_line_id else '' + return { + 'name': name, + 'declaration_amount': line.declaration_amount, + 'proof_amount': line.proof_amount, + 'limit': line.limit, + 'remarks': line.remarks, + 'attachment_name': line.proof_name or _('Proof'), + 'attachment_url': self._get_binary_link(line, 'proof', 'proof_name'), + } + + def _get_report_sections(self): + self.ensure_one() + old_regime = self.tax_regime == 'old' + section_fields = [ + (_('Past Employment'), 'past_employment_costings' if old_regime else 'past_employment_costings_new'), + (_('US 80C'), 'us80c_costings' if old_regime else 'us80c_costings_new'), + (_('US 80D'), { + 'self_family': 'us80d_costings' if old_regime else 'us80d_costings_new', + 'self_family_parent': 'us80d_costings_parents' if old_regime else 'us80d_costings_parents_new', + 'self_family_senior_parent': 'us80d_costings_senior_parents' if old_regime else 'us80d_costings_senior_parents_new', + }[self.us80d_selection_type]), + (_('US 10'), 'us10_costings' if old_regime else 'us10_costings_new'), + (_('US 80G'), 'us80g_costings' if old_regime else 'us80g_costings_new'), + (_('Chapter VIA'), 'chapter_via_costings' if old_regime else 'chapter_via_costings_new'), + (_('US 17'), 'us17_costings' if old_regime else 'us17_costings_new'), + (_('Other Income / Loss'), 'other_il_costings' if old_regime else 'other_il_costings_new'), + (_('Other Declarations'), 'other_declaration_costings' if old_regime else 'other_declaration_costings_new'), + ] + sections = [] + for title, field_name in section_fields: + lines = self[field_name].filtered(lambda line: line.declaration_amount or line.proof_amount or line.remarks or line.proof) + if lines: + sections.append({ + 'title': title, + 'lines': [self._prepare_report_line(line) for line in lines], + }) + + if old_regime and self.house_rent_costings: + sections.append({ + 'title': _('House Rent'), + 'house_rent': True, + 'lines': [{ + 'hra_exemption_type': dict(line._fields['hra_exemption_type'].selection).get(line.hra_exemption_type, ''), + 'rent_amount': line.rent_amount, + 'from_date': line.from_date, + 'to_date': line.to_date, + 'landlord_pan_status': dict(line._fields['landlord_pan_status'].selection).get(line.landlord_pan_status, ''), + 'landlord_pan_no': line.landlord_pan_no, + 'landlord_name_address': line.landlord_name_address, + 'remarks': line.remarks, + 'attachment_name': line.attachment_filename or _('Proof'), + 'attachment_url': self._get_binary_link(line, 'attachment', 'attachment_filename'), + } for line in self.house_rent_costings], + }) + return sections + + def _get_report_extra_sections(self): + self.ensure_one() + extra_sections = [] + us80c_lines = self.us80c_costings if self.tax_regime == 'old' else self.us80c_costings_new + other_il_lines = self.other_il_costings if self.tax_regime == 'old' else self.other_il_costings_new + + children_records = self.env['children.education'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)]) + if children_records: + extra_sections.append({ + 'title': _('Children Education Details'), + 'headers': [_('Child'), _('Name'), _('Class / Grade'), _('School / College'), _('Tuition Fee')], + 'rows': [[child.child_id, child.name, child.chile_class, child.organization, child.tuition_fee] for record in children_records for child in record.children_ids], + }) + + insurance_records = self.env['us80c.insurance.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)]) + if insurance_records: + extra_sections.append({ + 'title': _('Life Insurance Details'), + 'headers': [_('Company'), _('Insured For'), _('Insured Name'), _('Policy No'), _('Premium'), _('Payment Date'), _('Exempt Amount')], + 'rows': [[line.name_of_insurance_company, line.insured_in_favour_of, line.name_of_insured, line.policy_number, line.premium_amount, line.payment_date, line.exempt_amount] for record in insurance_records for line in record.life_insurance_ids], + }) + + nsc_records = self.env['nsc.declaration.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)]) + if nsc_records: + extra_sections.append({ + 'title': _('NSC Declaration Details'), + 'headers': [_('NSC Number'), _('Amount'), _('Payment Date')], + 'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date] for record in nsc_records for line in record.nsc_entry_ids], + }) + + self_occupied_records = self.env['self.occupied.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)]) + if self_occupied_records: + extra_sections.append({ + 'title': _('Self Occupied Property Details'), + 'headers': [_('Address'), _('Period From'), _('Period To'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')], + 'rows': [[record.address, record.period_from, record.period_to, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in self_occupied_records], + }) + + letout_records = self.env['letout.house.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)]) + if letout_records: + extra_sections.append({ + 'title': _('Let-out House Property Details'), + 'headers': [_('Address'), _('Rent'), _('Property Tax'), _('Water Tax'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')], + 'rows': [[record.address, record.rent_received, record.property_tax, record.water_tax, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in letout_records], + }) + + nsc_interest_records = self.env['nsc.interest.line'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)]) + if nsc_interest_records: + extra_sections.append({ + 'title': _('NSC Interest Details'), + 'headers': [_('NSC Number'), _('Amount'), _('Payment Date'), _('Interest Amount')], + 'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date, line.nsc_interest_amount] for record in nsc_interest_records for line in record.nsc_entry_ids], + }) + return [section for section in extra_sections if section['rows']] diff --git a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py new file mode 100644 index 000000000..9c5664a17 --- /dev/null +++ b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py @@ -0,0 +1,127 @@ +import re + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval + + +class EmployeePayslipDownloadWizard(models.TransientModel): + _name = 'employee.payslip.download.wizard' + _rec_name = 'employee_id' + _description = 'Employee Payslip Download Wizard' + + employee_id = fields.Many2one( + 'hr.employee', + required=True, + default=lambda self: self.env.user.employee_id.id, + readonly=True, + ) + download_type = fields.Selection( + selection=[ + ('single', 'Single Payslip'), + ('multi', 'Multiple Payslips'), + ], + string='Download Option', + required=True, + default='single', + ) + period_id = fields.Many2one( + 'payroll.period', + string='Payroll Period', + required=True, + ) + period_line = fields.Many2one( + 'payroll.period.line', + string='Month', + domain="[('period_id', '=', period_id)]", + ) + payslip_count = fields.Integer( + string='Available Payslips', + compute='_compute_payslip_count', + ) + + @api.onchange('download_type', 'period_id') + def _onchange_download_type_period_id(self): + for rec in self: + rec.period_line = False + + @api.depends('download_type', 'period_id', 'period_line') + def _compute_payslip_count(self): + for rec in self: + if not rec.period_id or (rec.download_type == 'single' and not rec.period_line): + rec.payslip_count = 0 + else: + rec.payslip_count = len(rec._get_available_payslips()) + + def _get_current_employee(self): + employee = self.env.user.employee_id + if not employee: + raise UserError(_('No employee is linked to your user. Please contact HR.')) + return employee + + def _get_date_range(self): + self.ensure_one() + if self.download_type == 'single': + if not self.period_line: + raise UserError(_('Please select a month to download a single payslip.')) + return self.period_line.from_date, self.period_line.to_date + return self.period_id.from_date, self.period_id.to_date + + def _get_available_payslips(self): + self.ensure_one() + employee = self._get_current_employee() + date_from, date_to = self._get_date_range() + if not date_from or not date_to: + return self.env['hr.payslip'] + + return self.env['hr.payslip'].sudo().search([ + ('employee_id', '=', employee.id), + ('state', 'in', ['done', 'paid']), + ('date_from', '>=', date_from), + ('date_to', '<=', date_to), + ('company_id', 'in', self.env.companies.ids), + ], order='date_from asc, date_to asc, id asc') + + def _get_payslips_for_download(self): + self.ensure_one() + payslips = self._get_available_payslips() + if not payslips: + raise UserError(_('No confirmed or paid payslip is available for the selected period.')) + if self.download_type == 'single' and len(payslips) > 1: + raise UserError(_('More than one payslip is available for this month. Please contact HR.')) + return payslips + + def _get_pdf_content(self, payslip): + report = payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip') + pdf_content, dummy = self.env['ir.actions.report'].sudo().with_context( + lang=payslip.employee_id.lang or self.env.lang, + )._render_qweb_pdf(report, payslip.id, data={'company_id': payslip.company_id}) + return report, pdf_content + + def _get_pdf_filename(self, payslip, report=False): + report = report or payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip') + if report.print_report_name: + filename = safe_eval(report.print_report_name, {'object': payslip}) + else: + filename = _('Payslip - %(employee)s - %(period)s', employee=payslip.employee_id.name, period=payslip.name) + return self._sanitize_filename(filename, 'payslip') + '.pdf' + + def _get_zip_filename(self): + self.ensure_one() + employee = self._get_current_employee() + filename = _('Payslips - %(employee)s - %(period)s', employee=employee.name, period=self.period_id.name) + return self._sanitize_filename(filename, 'payslips') + '.zip' + + @api.model + def _sanitize_filename(self, filename, fallback): + filename = re.sub(r'[\\/:*?"<>|]+', '-', str(filename or '')).strip(' .') + return filename or fallback + + def action_download_payslips(self): + self.ensure_one() + self._get_payslips_for_download() + return { + 'type': 'ir.actions.act_url', + 'url': '/employee_it_declaration/my_payslips/%s' % self.id, + 'target': 'self', + } diff --git a/addons_extensions/employee_it_declaration/models/investment_costings.py b/addons_extensions/employee_it_declaration/models/investment_costings.py index 08bef3d22..838ba5898 100644 --- a/addons_extensions/employee_it_declaration/models/investment_costings.py +++ b/addons_extensions/employee_it_declaration/models/investment_costings.py @@ -5,9 +5,10 @@ import calendar import re -class investmentCostings(models.Model): - _name = 'investment.costings' - _rec_name = 'investment_type_id' +class investmentCostings(models.Model): + _name = 'investment.costings' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_id' investment_type_id = fields.Many2one('it.investment.type') amount = fields.Integer() @@ -23,9 +24,10 @@ class investmentCostings(models.Model): related='it_declaration_id.period_id' ) -class pastEmpcostingType(models.Model): - _name = 'past_employment.costing.type' - _rec_name = 'investment_type_line_id' +class pastEmpcostingType(models.Model): + _name = 'past_employment.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') @@ -102,9 +104,10 @@ class pastEmpcostingType(models.Model): rec.declaration_amount = 0 # fallback -class us80cCostingType(models.Model): - _name = 'us80c.costing.type' - _rec_name = 'investment_type_line_id' +class us80cCostingType(models.Model): + _name = 'us80c.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -138,9 +141,10 @@ class us80cCostingType(models.Model): 'view_mode': self.action_id.view_mode, 'target': 'new', } -class us80dCostingType(models.Model): - _name = 'us80d.costing.type' - _rec_name = 'investment_type_line_id' +class us80dCostingType(models.Model): + _name = 'us80d.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -155,9 +159,10 @@ class us80dCostingType(models.Model): -class us10CostingType(models.Model): - _name = 'us10.costing.type' - _rec_name = 'investment_type_line_id' +class us10CostingType(models.Model): + _name = 'us10.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -171,9 +176,10 @@ class us10CostingType(models.Model): limit = fields.Integer() -class us80gCostingType(models.Model): - _name = 'us80g.costing.type' - _rec_name = 'investment_type_line_id' +class us80gCostingType(models.Model): + _name = 'us80g.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -187,9 +193,10 @@ class us80gCostingType(models.Model): limit = fields.Integer() -class chapterViaCostingType(models.Model): - _name = 'chapter.via.costing.type' - _rec_name = 'investment_type_line_id' +class chapterViaCostingType(models.Model): + _name = 'chapter.via.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -203,9 +210,10 @@ class chapterViaCostingType(models.Model): limit = fields.Integer() -class us17CostingType(models.Model): - _name = 'us17.costing.type' - _rec_name = 'investment_type_line_id' +class us17CostingType(models.Model): + _name = 'us17.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -217,9 +225,10 @@ class us17CostingType(models.Model): proof_name = fields.Char() limit = fields.Integer() -class OtherILCostingType(models.Model): - _name = 'other.il.costing.type' - _rec_name = 'investment_type_line_id' +class OtherILCostingType(models.Model): + _name = 'other.il.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -254,9 +263,10 @@ class OtherILCostingType(models.Model): } -class OtherDeclarationCostingType(models.Model): - _name = 'other.declaration.costing.type' - _rec_name = 'investment_type_line_id' +class OtherDeclarationCostingType(models.Model): + _name = 'other.declaration.costing.type' + _inherit = ['it.declaration.submitted.lock.mixin'] + _rec_name = 'investment_type_line_id' costing_type = fields.Many2one('investment.costings') it_declaration_id = fields.Many2one('emp.it.declaration') @@ -269,9 +279,10 @@ class OtherDeclarationCostingType(models.Model): limit = fields.Integer() -class HouseRentDeclaration(models.Model): - _name = 'house.rent.declaration' - _description = 'House Rent Declaration' +class HouseRentDeclaration(models.Model): + _name = 'house.rent.declaration' + _inherit = ['it.declaration.submitted.lock.mixin'] + _description = 'House Rent Declaration' it_declaration_id = fields.Many2one('emp.it.declaration') @@ -304,4 +315,4 @@ class HouseRentDeclaration(models.Model): if self.env.context.get('default_it_declaration_id'): 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 + return super().create(vals) diff --git a/addons_extensions/employee_it_declaration/report/report_action.xml b/addons_extensions/employee_it_declaration/report/report_action.xml index c6f4bdeb1..6f52bf0fb 100644 --- a/addons_extensions/employee_it_declaration/report/report_action.xml +++ b/addons_extensions/employee_it_declaration/report/report_action.xml @@ -1,12 +1,12 @@ - - - - + + + + IT Declaration Submission + emp.it.declaration + qweb-pdf + employee_it_declaration.report_it_tax_statement + employee_it_declaration.report_it_tax_statement + 'IT Declaration - %s - %s' % (object.employee_id.name or '', object.period_id.name or '') + + + diff --git a/addons_extensions/employee_it_declaration/security/ir.model.access.csv b/addons_extensions/employee_it_declaration/security/ir.model.access.csv index f092b4acd..00e9aa80a 100644 --- a/addons_extensions/employee_it_declaration/security/ir.model.access.csv +++ b/addons_extensions/employee_it_declaration/security/ir.model.access.csv @@ -54,13 +54,14 @@ access_nsc_interest_entry_user,nsc.interest.entry,model_nsc_interest_entry,base. 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,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_employee_payslip_download_wizard,employee.payslip.download.wizard,model_employee_payslip_download_wizard,base.group_user,1,1,1,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 -access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1 \ No newline at end of file +access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_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 index 2ccf198f9..b19816111 100644 --- a/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml +++ b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml @@ -5,36 +5,60 @@ emp.it.declaration.list emp.it.declaration - - - - - - - - + + + + + + + + + + emp.it.declarations.form - emp.it.declaration - -
- -
-
- -
-
- - - - - - - - - + emp.it.declaration + + +
+
+ +
+
+ +
+
+ + + + + + + + + + + @@ -49,9 +73,9 @@

- +
- + + + + + + + + + + + + + 1 + - - -