diff --git a/addons_extensions/cwf_timesheet/models/timesheet.py b/addons_extensions/cwf_timesheet/models/timesheet.py index 4d257ab38..8245e95d0 100644 --- a/addons_extensions/cwf_timesheet/models/timesheet.py +++ b/addons_extensions/cwf_timesheet/models/timesheet.py @@ -115,7 +115,6 @@ class CwfTimesheet(models.Model): external_group_id = self.env.ref("hr_employee_extended.group_external_user") users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)]) employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)]) - print(employees) # Loop through each day of the week and create timesheet lines for each employee while current_date <= end_date: for employee in employees: diff --git a/addons_extensions/document_parser/__manifest__.py b/addons_extensions/document_parser/__manifest__.py index 05d96bc4f..65eec6e90 100644 --- a/addons_extensions/document_parser/__manifest__.py +++ b/addons_extensions/document_parser/__manifest__.py @@ -14,6 +14,6 @@ "application": False, "auto_install": False, "external_dependencies": { - "python": ["requests"], + "python": ["requests","python-docx"], }, } diff --git a/addons_extensions/document_parser/models/document_parser_service.py b/addons_extensions/document_parser/models/document_parser_service.py index a672c9706..ff5d8da16 100644 --- a/addons_extensions/document_parser/models/document_parser_service.py +++ b/addons_extensions/document_parser/models/document_parser_service.py @@ -421,6 +421,91 @@ Document: def _get_param(self, key): return self.env["ir.config_parameter"].sudo().get_param(key) + @api.model + def validate_explicit_skills(self, resume_text, skills): + if not skills: + return [] + + prompt = f""" + You are validating resume skills. + + Resume: + {resume_text[:30000]} + + Extracted Skills: + {json.dumps(skills)} + + Keep ONLY skills explicitly claimed by the candidate. + + A skill is explicit if: + - It is presented as the candidate's expertise. + - It appears in a skill list or competency list. + + Reject skills appearing only in: + - job responsibilities + - project descriptions + - achievements + - employer history + + Return ONLY JSON. + + Example: + {{ + "skills": ["Python", "Django"] + }} + """ + + errors = [] + + together_key = self._get_param( + "document_parser.together_ai_key" + ) or self._get_param( + "document_parser.together_api_key" + ) + + openrouter_key = self._get_param( + "document_parser.openrouter_ai_key" + ) or self._get_param( + "document_parser.openrouter_api_key" + ) + + if together_key: + result, provider_errors = self._call_provider( + provider_name="Together", + endpoint=self.TOGETHER_ENDPOINT, + headers={ + "Authorization": f"Bearer {together_key}", + "Content-Type": "application/json", + }, + models=self.TOGETHER_MODELS, + prompt=prompt, + ) + + if result: + return result.get("skills", skills) + + errors.extend(provider_errors) + + if openrouter_key: + result, provider_errors = self._call_provider( + provider_name="OpenRouter", + endpoint=self.OPENROUTER_ENDPOINT, + headers={ + "Authorization": f"Bearer {openrouter_key}", + "Content-Type": "application/json", + "HTTP-Referer": self._get_param("web.base.url") or "odoo.local", + "X-Title": "Document Parser", + }, + models=self.OPENROUTER_MODELS, + prompt=prompt, + ) + + if result: + return result.get("skills", skills) + + errors.extend(provider_errors) + + return skills def _normalize_required_fields(self, fields): if isinstance(fields, dict): normalized = {} 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 2df9f0f4d..76035b6b5 100644 --- a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py +++ b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py @@ -37,38 +37,44 @@ class ITDeclarationSubmittedLockMixin(models.AbstractModel): 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 = "" - + _name = 'emp.it.declaration' + _rec_name = 'employee_id' + _description = "IT Declaration" + + _sql_constraints = [ + ('name_emp_tax_period', 'unique(employee_id, period_id, tax_regime)', + "Avoid creating duplicate records for the same period_id and tax_regime."), + ] + + # @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, + copy=False + ) + + 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') @@ -119,8 +125,8 @@ class EmpITDeclaration(models.Model): 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] + lambda e: e.investment_type_id.investment_type == 'house_rent' + )[:1] else: rec.house_rent_costing_id = False @@ -166,13 +172,13 @@ class EmpITDeclaration(models.Model): 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',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]) other_declaration_costings_new = fields.One2many('other.declaration.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]) - show_past_employment = fields.Boolean(compute="_compute_show_records") - show_us_80c = fields.Boolean(compute="_compute_show_records") - show_us_80d = fields.Boolean(compute="_compute_show_records") - show_us_10 = fields.Boolean(compute="_compute_show_records") - show_us_80g = fields.Boolean(compute="_compute_show_records") - show_chapter_via = fields.Boolean(compute="_compute_show_records") - show_us_17 = fields.Boolean(compute="_compute_show_records") + show_past_employment = fields.Boolean(compute="_compute_show_records") + show_us_80c = fields.Boolean(compute="_compute_show_records") + show_us_80d = fields.Boolean(compute="_compute_show_records") + show_us_10 = fields.Boolean(compute="_compute_show_records") + show_us_80g = fields.Boolean(compute="_compute_show_records") + show_chapter_via = fields.Boolean(compute="_compute_show_records") + show_us_17 = fields.Boolean(compute="_compute_show_records") show_house_rent = fields.Boolean(compute="_compute_show_records") show_other_i_or_l = fields.Boolean(compute="_compute_show_records") show_other_declaration = fields.Boolean(compute="_compute_show_records") @@ -353,7 +359,7 @@ class EmpITDeclaration(models.Model): rec[field_name] = bool(visible_investment_types.filtered( lambda inv: inv.investment_type == investment_type_key )) - + def toggle_section_visibility(self): for rec in self: rec.is_section_open = not rec.is_section_open @@ -372,37 +378,37 @@ class EmpITDeclaration(models.Model): # 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'])] - # } - # } + 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: 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 index 358b0da2f..2e82ad7cd 100644 --- a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py +++ b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py @@ -42,8 +42,6 @@ class EmployeePayslipDownloadWizard(models.TransientModel): def _compute_is_hr_manager(self): for rec in self: - import pdb - pdb.set_trace() rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager') @api.onchange('download_type', 'period_id') 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 index 6ebe1fd82..3f19c27df 100644 --- a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py +++ b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py @@ -756,9 +756,9 @@ class ITTaxStatementWizard(models.TransientModel): if not self.employee_id or not self.contract_id or not self.period_id or not self.period_line: raise ValidationError(_("Select employee, period, and period line before checking comparison.")) - values = self._get_tax_base_values(include_comparison=True) + values = self.sudo()._get_tax_base_values(include_comparison=True) if not values['comparison_available']: - self._reset_regime_comparison() + self.sudo()._reset_regime_comparison() raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured.")) self.write({ @@ -938,7 +938,7 @@ class ITTaxStatementWizard(models.TransientModel): return {'data': data} def action_generate_report(self): - report_data = self._prepare_income_tax_data(include_comparison=False) + report_data = self.sudo()._prepare_income_tax_data(include_comparison=False) return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action( self, @@ -946,7 +946,7 @@ class ITTaxStatementWizard(models.TransientModel): ) def action_generate_comparison_report(self): - report_data = self._prepare_income_tax_data(include_comparison=True) + report_data = self.sudo()._prepare_income_tax_data(include_comparison=True) if not report_data['data']['comparison']['available']: raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured.")) 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 b19816111..af6d56b98 100644 --- a/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml +++ b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml @@ -5,60 +5,60 @@ emp.it.declaration.list emp.it.declaration - - - - - - - - - - + + + + + + + + + + emp.it.declarations.form - emp.it.declaration - -
-
-
- -
-
- -
-
- - - - - - - - - - - + emp.it.declaration + + +
+
+ +
+
+ +
+
+ + + + + + + + + + + @@ -73,9 +73,9 @@

- +
- - + @@ -25,14 +22,27 @@ -
- - -
-
- - -
+ +
+ + +
+ +
+ + +
+
diff --git a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py index ba2bb0b8c..4010d4888 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py @@ -9,6 +9,18 @@ from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension class HRApplicant(models.Model): _inherit = 'hr.applicant' _track_duration_field = 'recruitment_stage_id' + _rec_names_search = ['candidate_id', 'candidate_id.candidate_sequence', 'candidate_id.partner_phone','partner_name','job_id.name','hr_job_recruitment.recruitment_sequence'] + _sql_constraints = [ + ('name_applicant_job_id', 'unique(candidate_id, hr_job_recruitment)', + "Avoid creating Multiple applications for the same job")] + + @api.constrains('candidate_id','hr_job_recruitment') + def _check_name_applicant_job_id(self): + for rec in self: + if rec.candidate_id and rec.hr_job_recruitment: + candidate_history = self.sudo().search([('id','!=',rec.id),('candidate_id','=',rec.candidate_id.id),('hr_job_recruitment','=',rec.hr_job_recruitment.id)]) + if candidate_history: + raise ValidationError(_("Avoid creating Multiple applications for the same Job Position.")) hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True) primary_skill_match_percentage = fields.Float( @@ -31,24 +43,24 @@ class HRApplicant(models.Model): ) candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True) - submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True) - client_submission_date = fields.Datetime(string="Submission Date", tracking=True) - submitted_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True) + submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True) + client_submission_date = fields.Datetime(string="Submission Date", tracking=True) + submitted_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True) refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage") refused_comments = fields.Text(string='Reject Comments') is_on_hold = fields.Boolean(string="Is On Hold", default=False) - hold_state = fields.Selection( - [('hold', 'On Hold')], - string="Hold Status", - compute='_compute_hold_state', - ) - stage_comment_ids = fields.One2many( - 'hr.applicant.stage.comment', - 'applicant_id', - string='Stage Comments', - ) - stage_comment_count = fields.Integer(compute='_compute_stage_comment_count') - stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips') + hold_state = fields.Selection( + [('hold', 'On Hold')], + string="Hold Status", + compute='_compute_hold_state', + ) + stage_comment_ids = fields.One2many( + 'hr.applicant.stage.comment', + 'applicant_id', + string='Stage Comments', + ) + stage_comment_count = fields.Integer(compute='_compute_stage_comment_count') + stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips') @api.depends('is_on_hold') def _compute_hold_state(self): @@ -63,63 +75,63 @@ class HRApplicant(models.Model): rec.is_on_hold = True return {'type': 'ir.actions.client', 'tag': 'reload'} - def action_toggle_chatter_visibility(self): - for record in self: - record.hide_chatter_suggestion = not record.hide_chatter_suggestion - return {'type': 'ir.actions.client', 'tag': 'reload'} - - @api.depends('stage_comment_ids') - def _compute_stage_comment_count(self): - for applicant in self: - applicant.stage_comment_count = len(applicant.stage_comment_ids) - - @api.depends('stage_comment_ids.comment', 'stage_comment_ids.stage_id', 'stage_comment_ids.user_id', 'stage_comment_ids.comment_date') - def _compute_stage_comment_tooltips(self): - for applicant in self: - grouped_comments = {} - for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()): - if not comment.stage_id: - continue - line = '%s: %s' % (comment.user_id.name, comment.comment) - grouped_comments.setdefault(comment.stage_id.id, []).append(line) - applicant.stage_comment_tooltips = { - stage_id: '\n'.join(comments[-4:]) - for stage_id, comments in grouped_comments.items() - } - - def action_open_stage_comment_wizard(self): - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': _('Stage Comments'), - 'res_model': 'applicant.stage.comment.wizard', - 'view_mode': 'form', - 'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id, - 'target': 'new', - 'context': { - 'active_id': self.id, - 'default_applicant_id': self.id, - 'default_stage_id': self.recruitment_stage_id.id, - }, - } - - @api.onchange('submitted_to_client') - def _onchange_submitted_to_client(self): - for applicant in self: - if applicant.submitted_to_client: - applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now() - applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id - else: - applicant.client_submission_date = False - applicant.submitted_stage = False - - @api.model_create_multi - def create(self, vals_list): - for vals in vals_list: - if vals.get('submitted_to_client'): - vals.setdefault('client_submission_date', fields.Datetime.now()) - vals.setdefault('submitted_stage', vals.get('recruitment_stage_id')) - return super().create(vals_list) + def action_toggle_chatter_visibility(self): + for record in self: + record.hide_chatter_suggestion = not record.hide_chatter_suggestion + return {'type': 'ir.actions.client', 'tag': 'reload'} + + @api.depends('stage_comment_ids') + def _compute_stage_comment_count(self): + for applicant in self: + applicant.stage_comment_count = len(applicant.stage_comment_ids) + + @api.depends('stage_comment_ids.comment', 'stage_comment_ids.stage_id', 'stage_comment_ids.user_id', 'stage_comment_ids.comment_date') + def _compute_stage_comment_tooltips(self): + for applicant in self: + grouped_comments = {} + for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()): + if not comment.stage_id: + continue + line = '%s: %s' % (comment.user_id.name, comment.comment) + grouped_comments.setdefault(comment.stage_id.id, []).append(line) + applicant.stage_comment_tooltips = { + stage_id: '\n'.join(comments[-4:]) + for stage_id, comments in grouped_comments.items() + } + + def action_open_stage_comment_wizard(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': _('Stage Comments'), + 'res_model': 'applicant.stage.comment.wizard', + 'view_mode': 'form', + 'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id, + 'target': 'new', + 'context': { + 'active_id': self.id, + 'default_applicant_id': self.id, + 'default_stage_id': self.recruitment_stage_id.id, + }, + } + + @api.onchange('submitted_to_client') + def _onchange_submitted_to_client(self): + for applicant in self: + if applicant.submitted_to_client: + applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now() + applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id + else: + applicant.client_submission_date = False + applicant.submitted_stage = False + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('submitted_to_client'): + vals.setdefault('client_submission_date', fields.Datetime.now()) + vals.setdefault('submitted_stage', vals.get('recruitment_stage_id')) + return super().create(vals_list) @api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids') def _compute_skill_match_percentages(self): @@ -153,27 +165,27 @@ class HRApplicant(models.Model): stage_ids = stages.sudo()._search(search_domain, order=stages._order) return stages.browse(stage_ids) - def write(self, vals): - if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1: - for applicant in self: - applicant_vals = dict(vals) - applicant_vals.setdefault('client_submission_date', fields.Datetime.now()) - applicant_vals['submitted_stage'] = vals.get('recruitment_stage_id') or applicant.recruitment_stage_id.id or False - applicant.write(applicant_vals) - return True - if vals.get('submitted_to_client'): - vals.setdefault('client_submission_date', fields.Datetime.now()) - if 'submitted_stage' not in vals: - submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id - if submitted_stage: - vals['submitted_stage'] = submitted_stage - elif vals.get('submitted_to_client') is False: - vals.setdefault('client_submission_date', False) - vals.setdefault('submitted_stage', False) - if 'recruitment_stage_id' in vals: - blocked_records = self.filtered( - lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id') - ) + def write(self, vals): + if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1: + for applicant in self: + applicant_vals = dict(vals) + applicant_vals.setdefault('client_submission_date', fields.Datetime.now()) + applicant_vals['submitted_stage'] = vals.get('recruitment_stage_id') or applicant.recruitment_stage_id.id or False + applicant.write(applicant_vals) + return True + if vals.get('submitted_to_client'): + vals.setdefault('client_submission_date', fields.Datetime.now()) + if 'submitted_stage' not in vals: + submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id + if submitted_stage: + vals['submitted_stage'] = submitted_stage + elif vals.get('submitted_to_client') is False: + vals.setdefault('client_submission_date', False) + vals.setdefault('submitted_stage', False) + if 'recruitment_stage_id' in vals: + blocked_records = self.filtered( + lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id') + ) if blocked_records: raise ValidationError(_("You cannot change the stage of an applicant while it is on hold. Please unhold it first.")) if 'stage_id' in vals: diff --git a/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py b/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py index 2397a119e..160cc9c2b 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py @@ -2,14 +2,23 @@ from odoo import models, fields, api, _ from odoo.exceptions import ValidationError +class HRCandidate(models.Model): + _inherit = 'hr.candidate' + + education_history = fields.Many2many('education.history', string='Education Details') + + employer_history = fields.Many2many('employer.history', string='Education Details') + + family_details = fields.Many2many('family.details', string='Family Details') + class HRApplicant(models.Model): _inherit = 'hr.applicant' - education_history = fields.One2many('education.history', 'applicant_id', string='Education Details') + education_history = fields.Many2many('education.history', related='candidate_id.education_history', readonly=False, string='Education Details') - employer_history = fields.One2many('employer.history', 'applicant_id', string='Education Details') + employer_history = fields.Many2many('employer.history', related='candidate_id.employer_history', readonly=False, string='Education Details') - family_details = fields.One2many('family.details', 'applicant_id', string='Family Details') + family_details = fields.Many2many('family.details', related='candidate_id.family_details', readonly=False, string='Family Details') family_education_employer_details_status = fields.Selection([('pending', 'Pending'), @@ -27,15 +36,6 @@ class HRApplicant(models.Model): raise ValidationError(_("No Data to Validate")) -class HRCandidate(models.Model): - _inherit = 'hr.candidate' - - education_history = fields.One2many('education.history', 'candidate_id', string='Education Details') - - employer_history = fields.One2many('employer.history', 'candidate_id', string='Education Details') - - family_details = fields.One2many('family.details', 'candidate_id', string='Family Details') - class FamilyDetails(models.Model): _inherit = "family.details" diff --git a/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py index b610af552..9b9f38701 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py @@ -10,6 +10,8 @@ class HrCandidate(models.Model): _sql_constraints = [ ('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'), ] + _rec_names_search = ['partner_name', 'candidate_sequence', 'partner_phone'] + #personal Details candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False) hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True) diff --git a/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js b/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js index d5bc89689..e28dd0a52 100644 --- a/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js +++ b/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js @@ -27,6 +27,12 @@ patch(RecruitmentFormController.prototype, { this._matchPanelSearchTerm = ""; this._matchPanelActiveTab = "candidates"; this._matchPanelAddingCandidateId = null; + this._primarySkillsExpanded = false; + this._secondarySkillsExpanded = false; + this._matchPanelScrollPosition = { + candidates: 0, + applicants: 0, + }; onMounted(() => this._syncRecruitmentMatchPanel()); onPatched(() => this._syncRecruitmentMatchPanel()); @@ -161,7 +167,7 @@ patch(RecruitmentFormController.prototype, { _renderSkillTags(skillNames, className) { if (!skillNames.length) { - return `${escapeHtml(_t("None"))}`; + return `${escapeHtml(_t("No Skills Added"))}`; } return skillNames .map((skillName) => `${escapeHtml(skillName)}`) @@ -288,6 +294,7 @@ patch(RecruitmentFormController.prototype, { ? _t("Applicants added to this recruitment will appear here with the same match insights.") : _t("Try refreshing after updating the recruitment skills or candidate pool.") ); + const previousScroll = this._matchPanelScrollPosition[this._matchPanelActiveTab] || 0; panel.innerHTML = `
@@ -295,7 +302,7 @@ patch(RecruitmentFormController.prototype, {

${escapeHtml(_t("Candidate Pool"))}

${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}

- ${escapeHtml(`${payload.candidate_count || 0} ${_t("pool candidates")} • ${payload.applicant_count || 0} ${_t("applicants")}`)} + ${escapeHtml(`${payload.candidate_count || 0} ${_t(" pool candidates ")} • ${payload.applicant_count || 0} ${_t(" applicants")}`)}
@@ -322,28 +329,88 @@ patch(RecruitmentFormController.prototype, {
-
- -
- ${this._renderSkillTags(payload.primary_skill_names || [], "o_hr_match_chip_primary")} -
-
-
- -
- ${this._renderSkillTags(payload.secondary_skill_names || [], "o_hr_match_chip_secondary")} -
-
-
+ + ${ + payload.primary_skill_names?.length + ? ` +
+ + ${this._primarySkillsExpanded ? "▼" : "▶"} + ${escapeHtml(_t("Primary Skills"))} + + + + (${payload.primary_skill_names.length}) + +
+ + ${ + this._primarySkillsExpanded + ? ` +
+ ${this._renderSkillTags(payload.primary_skill_names, "o_hr_match_chip_primary")} +
+ ` + : "" + } + ` + : "" + } + + ${ + payload.secondary_skill_names?.length + ? ` +
+ + ${this._secondarySkillsExpanded ? "▼" : "▶"} + ${escapeHtml(_t("Secondary Skills"))} + + + + (${payload.secondary_skill_names.length}) + +
+ + ${ + this._secondarySkillsExpanded + ? ` +
+ ${this._renderSkillTags(payload.secondary_skill_names, "o_hr_match_chip_secondary")} +
+ ` + : "" + } + ` + : "" + } + +
${this._matchPanelLoading ? `
${escapeHtml(_t("Refreshing matches..."))}
` : activeCards}
`; panel.classList.add("o_hr_match_panel_open"); + panel.querySelector('[data-toggle="primary"]')?.addEventListener("click", () => { + this._primarySkillsExpanded = !this._primarySkillsExpanded; + this._renderRecruitmentMatchPanel(); + }); + panel.querySelector('[data-toggle="secondary"]')?.addEventListener("click", () => { + this._secondarySkillsExpanded = !this._secondarySkillsExpanded; + this._renderRecruitmentMatchPanel(); + }); panel.querySelector(".o_hr_match_close")?.addEventListener("click", () => this._closeRecruitmentMatchPanel()); - panel.querySelector(".o_hr_match_refresh")?.addEventListener("click", () => this._loadRecruitmentMatchPanelData()); + panel.querySelector(".o_hr_match_refresh")?.addEventListener("click", () => { + this._matchPanelScrollPosition = { + candidates: 0, + applicants: 0, + }; + this._loadRecruitmentMatchPanelData() + } + ); panel.querySelectorAll(".o_hr_match_tab").forEach((tabButton) => { tabButton.addEventListener("click", () => { this._matchPanelActiveTab = tabButton.dataset.tab || "candidates"; @@ -401,7 +468,17 @@ patch(RecruitmentFormController.prototype, { } }); } + const body = panel.querySelector(".o_hr_match_panel_body"); + if (body) { + // Restore previous scroll for this tab + body.scrollTop = previousScroll; + + // Continuously save scroll position + body.addEventListener("scroll", () => { + this._matchPanelScrollPosition[this._matchPanelActiveTab] = body.scrollTop; + }); + } const button = this._getRecruitmentMatchPanelButton(); if (button) { button.classList.add("o_hr_match_fab_hidden"); diff --git a/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss b/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss index 208544dec..e9732c4ca 100644 --- a/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss +++ b/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss @@ -100,6 +100,7 @@ margin-top: 6px; font-size: 22px; font-weight: 700; + color: white; } .o_hr_match_panel_header span { @@ -416,6 +417,44 @@ display: grid; gap: 14px; } +.o_hr_match_collapsible { + margin-bottom: 12px; +} + +.o_hr_match_collapsible_header { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + padding: 8px 12px; + border-radius: 8px; + background: #f5f5f5; + font-weight: 600; +} + +.o_hr_match_collapsible_header:hover { + background: #ececec; +} +.o_hr_match_skill_header { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .08em; + margin-bottom: 10px; + color: #334155; +} + +.o_hr_match_skill_header:hover { + color: var(--o-brand-primary); +} + +.o_hr_match_skill_count { + color: #64748b; + font-weight: 600; +} @media (min-width: 768px) { .o_hr_match_skill_split { diff --git a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml index 4412341cd..3528775de 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml @@ -7,6 +7,7 @@ hold_state == 'hold' + id desc @@ -16,7 +17,7 @@ - + @@ -289,6 +290,26 @@ is_on_hold + + + + +
+
+ + + + +
+ + + + +
+
@@ -297,7 +318,7 @@ + quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1" default_order="id desc"> diff --git a/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml b/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml index ebda49b45..4cfe126ae 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml @@ -29,7 +29,7 @@ name="action_validate_personal_details" type="object" class="btn btn-danger" - invisible="not employee_id"> + invisible="employee_id">
Click here to save, Validate and Update into Employee Data + + hr.candidate.view.tree.inherit + hr.candidate + + + + id desc + + + hr.candidate.view.kanban.inherit hr.candidate @@ -284,14 +294,30 @@ + {'active_test': False, 'kanban': True} + id desc - +
+
+ + +
+ + + + +
diff --git a/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py b/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py index b7aefcee0..145505d9e 100644 --- a/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py +++ b/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py @@ -98,7 +98,6 @@ class PostOnboardingAttachmentWizard(models.TransientModel): lambda a: a.attachment_type == 'previous_employer').mapped('name') other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name') - print(request_upload_url) email_context = { 'applicant_request_form_id': rec.request_form_id.id, 'applicant_request_form_token': request_token, diff --git a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py index 6c6ee5e0f..150aa9f59 100644 --- a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py +++ b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py @@ -37,7 +37,6 @@ class HrmsEmployeeDashboard(http.Controller): calendar_attendances = self._get_attendances(employee, calendar_start, calendar_end) calendar_leaves = self._get_leaves(employee, calendar_start, calendar_end) calendar_public_holidays = self._get_public_holidays(employee, calendar_start, calendar_end) - print(self._expense_data(employee, range_start, range_end)) return { "success": True, "employee": self._employee_card(employee), @@ -325,12 +324,6 @@ class HrmsEmployeeDashboard(http.Controller): ("date", "<=", date_to), ]) for expense in emp_expenses: - print( - "Expense: %s | Date: %s | Amount: %s", - expense.name, - expense.date, - expense.total_amount - ) totals[expense.date.strftime("%Y-%m")] += expense.total_amount or 0.0 state_totals[expense.state or "draft"] += expense.total_amount or 0.0 diff --git a/addons_extensions/offer_letters/__manifest__.py b/addons_extensions/offer_letters/__manifest__.py index b7a27c728..3104f490b 100644 --- a/addons_extensions/offer_letters/__manifest__.py +++ b/addons_extensions/offer_letters/__manifest__.py @@ -12,6 +12,7 @@ 'data': [ 'security/ir.model.access.csv', 'data/mail_template.xml', + 'views/stages.xml', 'views/offer_letter_views.xml', 'views/hr_applicant_offer_views.xml', 'views/offer_response_templates.xml', diff --git a/addons_extensions/offer_letters/models/__init__.py b/addons_extensions/offer_letters/models/__init__.py index 3afed8da1..2e82655fc 100644 --- a/addons_extensions/offer_letters/models/__init__.py +++ b/addons_extensions/offer_letters/models/__init__.py @@ -1,3 +1,4 @@ +from . import stages from . import offer_letter from . import hr_applicant from . import hr_candidate \ No newline at end of file diff --git a/addons_extensions/offer_letters/models/hr_applicant.py b/addons_extensions/offer_letters/models/hr_applicant.py index b47e5e51c..5f768a093 100644 --- a/addons_extensions/offer_letters/models/hr_applicant.py +++ b/addons_extensions/offer_letters/models/hr_applicant.py @@ -26,6 +26,8 @@ class HRApplicant(models.Model): store=False, ) + request_offer_release = fields.Boolean(related='recruitment_stage_id.request_offer_release') + @api.depends('offer_letter_ids', 'offer_letter_ids.create_date', 'offer_letter_ids.state') def _compute_current_offer_letter(self): for applicant in self: diff --git a/addons_extensions/offer_letters/models/offer_letter.py b/addons_extensions/offer_letters/models/offer_letter.py index d8b18eea7..25287fccb 100644 --- a/addons_extensions/offer_letters/models/offer_letter.py +++ b/addons_extensions/offer_letters/models/offer_letter.py @@ -174,12 +174,12 @@ class OfferLetter(models.Model): first_day = today.replace(day=1) last_day = today.replace(day=calendar.monthrange(today.year, today.month)[1]) - payslip = self.env['hr.payslip'].new({ + payslip = self.env['hr.payslip'].sudo().new({ 'date_from': first_day, 'date_to': last_day, }) - contract = self.env['hr.contract'].new({ + contract = self.env['hr.contract'].sudo().new({ 'date_start': first_day, 'date_end': last_day, 'l10n_in_medical_insurance':self.mi, @@ -205,7 +205,7 @@ class OfferLetter(models.Model): 'inputs': {}, } blacklisted_ids = set(self.env.context.get('prevent_payslip_computation_line_ids', [])) - for rule in sorted(self.pay_struct_id.rule_ids, key=lambda r: r.sequence): + for rule in sorted(self.sudo().pay_struct_id.rule_ids, key=lambda r: r.sequence): if rule.id in blacklisted_ids or not rule._satisfy_condition(localdict): continue qty = 1.0 @@ -223,15 +223,15 @@ class OfferLetter(models.Model): except Exception as e: raise UserError(_("Error in rule %s: %s") % (rule.name, str(e))) - total = payslip._get_payslip_line_total(amount, qty, rate, rule) + total = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule) rule_code = rule.code previous_amount = localdict.get(rule.code, 0.0) category_code = rule.category_id.code - tot_rule = payslip._get_payslip_line_total(amount, qty, rate, rule) + tot_rule = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule) # Make sure _sum_salary_rule_category method exists if hasattr(rule.category_id, '_sum_salary_rule_category'): - localdict = rule.category_id._sum_salary_rule_category(localdict, tot_rule - previous_amount) + localdict = rule.category_id.sudo()._sum_salary_rule_category(localdict, tot_rule - previous_amount) localdict[rule_code] = total rules_dict[rule_code] = rule diff --git a/addons_extensions/offer_letters/models/stages.py b/addons_extensions/offer_letters/models/stages.py new file mode 100644 index 000000000..8699343c0 --- /dev/null +++ b/addons_extensions/offer_letters/models/stages.py @@ -0,0 +1,6 @@ +from odoo import models, fields, api, _ + +class RecruitmentStage(models.Model): + _inherit = 'hr.recruitment.stage' + + request_offer_release = fields.Boolean(string="Request Offer Release") \ No newline at end of file diff --git a/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml b/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml index 34cb2153c..ff04f7765 100644 --- a/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml +++ b/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml @@ -26,7 +26,7 @@
@@ -105,10 +90,10 @@
-
-
+
+
-
+
-
-

- -

-
- +
+ +
+
+ +
+ +
+
- +
+
-
+
-
-
+
@@ -174,13 +170,13 @@ groups="hr_recruitment.group_hr_recruitment_user" col="1">