recritment bug fixes

This commit is contained in:
pranaysaidurga 2026-06-19 14:05:09 +05:30
parent 7960d1926b
commit a9a9f08ff9
36 changed files with 651 additions and 182 deletions

View File

@ -115,7 +115,6 @@ class CwfTimesheet(models.Model):
external_group_id = self.env.ref("hr_employee_extended.group_external_user") external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)]) 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)]) 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 # Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date: while current_date <= end_date:
for employee in employees: for employee in employees:

View File

@ -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: 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.")) 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']: 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.")) raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))
self.write({ self.write({
@ -938,7 +938,7 @@ class ITTaxStatementWizard(models.TransientModel):
return {'data': data} return {'data': data}
def action_generate_report(self): 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( return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action(
self, self,
@ -946,7 +946,7 @@ class ITTaxStatementWizard(models.TransientModel):
) )
def action_generate_comparison_report(self): 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']: if not report_data['data']['comparison']['available']:
raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured.")) raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))

View File

@ -43,7 +43,7 @@
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="is_general_tax_statement" invisible="1"/> <field name="is_general_tax_statement" invisible="1"/>
<group> <group>
<field name="is_hr_manager" invisible="0" force_save="1"/> <field name="is_hr_manager" invisible="1" force_save="1"/>
<field name="employee_id" readonly="not is_hr_manager" options="{'no_edit': True, 'no_create': True}"/> <field name="employee_id" readonly="not is_hr_manager" options="{'no_edit': True, 'no_create': True}"/>
<field name="contract_id" readonly="1" force_save="1" invisible="0"/> <field name="contract_id" readonly="1" force_save="1" invisible="0"/>
</group> </group>

View File

@ -1,10 +1,10 @@
from odoo import models, fields, api from odoo import models, fields, api
import math import math
class LetoutHouseProperty(models.Model): class LetoutHouseProperty(models.Model):
_name = 'letout.house.property' _name = 'letout.house.property'
_inherit = ['it.declaration.submitted.lock.mixin'] _inherit = ['it.declaration.submitted.lock.mixin']
_description = 'Letout House Property Details' _description = 'Letout House Property Details'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration") it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type") other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type")
@ -36,9 +36,6 @@ class LetoutHouseProperty(models.Model):
deduction = net_annual_value * 0.30 deduction = net_annual_value * 0.30
income_loss = net_annual_value - deduction - record.interest_paid_to income_loss = net_annual_value - deduction - record.interest_paid_to
print(net_annual_value)
print(deduction)
print(income_loss)
record.net_annual_value = net_annual_value record.net_annual_value = net_annual_value
record.deduction_for_repairs = round(deduction) record.deduction_for_repairs = round(deduction)
record.income_loss = round(income_loss) record.income_loss = round(income_loss)

View File

@ -209,7 +209,6 @@ ORDER BY
except Exception as e: except Exception as e:
error_msg = f"Error executing attendance report query: {str(e)}" error_msg = f"Error executing attendance report query: {str(e)}"
print(error_msg)
raise UserError( raise UserError(
_("An error occurred while generating the attendance report. Please check the logs for details.")) _("An error occurred while generating the attendance report. Please check the logs for details."))

View File

@ -8,7 +8,8 @@ class HrApplicant(models.Model):
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0] action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0]
context = dict(self.env.context) context = dict(self.env.context)
context['single_parser'] = True context['single_parser'] = True
action["context"]['current_id'] = self.id context['applicant_id'] = self.id
context['target_model'] = 'applicant'
if len(self) == 1 and self.hr_job_recruitment: if len(self) == 1 and self.hr_job_recruitment:
context["default_job_recruitment_id"] = self.hr_job_recruitment.id context["default_job_recruitment_id"] = self.hr_job_recruitment.id
action["context"] = context action["context"] = context

View File

@ -8,6 +8,7 @@ class HrCandidate(models.Model):
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate").read()[0] action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate").read()[0]
action["context"] = dict(self.env.context) action["context"] = dict(self.env.context)
action["context"]['single_parser'] = True action["context"]['single_parser'] = True
action["context"]['current_id'] = self.id action["context"]['target_model'] = 'candidate'
action["context"]['candidate_id'] = self.id
action["name"] = _("Parse Resumes") action["name"] = _("Parse Resumes")
return action return action

View File

@ -7,6 +7,7 @@ class HrJobRecruitment(models.Model):
def action_open_auto_doc_wizard(self): def action_open_auto_doc_wizard(self):
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment").read()[0] action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment").read()[0]
context = dict(self.env.context) context = dict(self.env.context)
context['target_model'] = 'job_recruitment'
if len(self) == 1: if len(self) == 1:
context["default_job_recruitment_id"] = self.id context["default_job_recruitment_id"] = self.id
action["context"] = context action["context"] = context

View File

@ -39,11 +39,11 @@
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/> <field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//header" position="inside"> <xpath expr="//header" position="inside">
<button name="action_open_auto_doc_wizard" <!-- <button name="action_open_auto_doc_wizard"-->
string="Parse Resume" <!-- string="Parse Resume"-->
type="object" <!-- type="object"-->
class="btn-secondary" <!-- class="btn-secondary"-->
groups="hr_recruitment.group_hr_recruitment_user"/> <!-- groups="hr_recruitment.group_hr_recruitment_user"/>-->
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@ -95,10 +95,21 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
res["job_recruitment_id"] = default_job_recruitment_id res["job_recruitment_id"] = default_job_recruitment_id
elif len(active_ids) == 1: elif len(active_ids) == 1:
res["job_recruitment_id"] = active_ids[0] res["job_recruitment_id"] = active_ids[0]
if self.env.context.get("target_model"):
res["target_model"] = self.env.context.get("target_model")
if res["target_model"] == "candidate" and self.env.context.get("candidate_id"):
candidate = self.env["hr.candidate"].browse(int(self.env.context.get("candidate_id")))
if candidate and candidate.resume:
res.update({
"resume_file": candidate.resume,
"resume_filename": candidate.resume_name,
})
return res return res
def action_parse_documents(self): def action_parse_documents(self):
self.ensure_one() self.ensure_one()
self.line_ids.unlink()
self._sync_upload_lines() self._sync_upload_lines()
if not self.line_ids: if not self.line_ids:
raise UserError(_("Please add at least one document to parse.")) raise UserError(_("Please add at least one document to parse."))
@ -220,7 +231,8 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"updated_count": updated, "updated_count": updated,
"skipped_count": skipped, "skipped_count": skipped,
"result_html": self._build_summary_html(summary_rows), "result_html": self._build_summary_html(summary_rows),
"parsed_document": True "parsed_document": True,
"create_updated_records": False,
}) })
return { return {
@ -516,15 +528,24 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
def _find_or_create_candidate(self, line, parsed_data, parsed_payload): def _find_or_create_candidate(self, line, parsed_data, parsed_payload):
candidate = self._find_existing_candidate(parsed_data) candidate = self._find_existing_candidate(parsed_data)
existing_candidate = True
candidate_vals = self._prepare_candidate_vals(line, parsed_data, parsed_payload) candidate_vals = self._prepare_candidate_vals(line, parsed_data, parsed_payload)
if self.env.context.get('candidate_id') and not candidate:
existing_candidate = False
candidate = self.env['hr.candidate'].sudo().browse(int(self.env.context.get('candidate_id')))
if candidate: if candidate:
if self.update_existing_candidates: if self.update_existing_candidates:
candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals)) candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals, False))
self._sync_candidate_skills(candidate, parsed_data.get("skills") or []) self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
if self.target_model == "candidate": if self.target_model == "candidate":
self._sync_candidate_resume_histories(candidate, parsed_data) self._sync_candidate_resume_histories(candidate, parsed_data)
message = _("Matched existing candidate: %s") % candidate.display_name if existing_candidate:
message = _("Matched existing candidate: %s") % candidate.display_name
if self.env.context.get('candidate_id'):
self.env['hr.candidate'].sudo().browse(int(self.env.context.get('candidate_id'))).unlink()
else:
candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals, True))
message = _("Details Updated Successfully: %s") % candidate.display_name
return candidate, "updated", message return candidate, "updated", message
self._ensure_resume_creation_allowed(parsed_data, line.file_name) self._ensure_resume_creation_allowed(parsed_data, line.file_name)
@ -586,7 +607,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
candidate = False candidate = False
if email_value: if email_value:
candidate = search_model.search(['|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1) candidate = search_model.search([('id','!=',self.env.context.get('candidate_id')),'|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1)
if not candidate and phone_values: if not candidate and phone_values:
candidate = search_model.search([ candidate = search_model.search([
"|","|",('partner_phone',"in",phone_values), "|","|",('partner_phone',"in",phone_values),
@ -973,7 +994,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
if not skills: if not skills:
raise ValidationError(_("No reliable skills were found in the resume. Record creation was skipped.")) raise ValidationError(_("No reliable skills were found in the resume. Record creation was skipped."))
def _prepare_sparse_update_vals(self, candidate, values): def _prepare_sparse_update_vals(self, candidate, values, current_update:False):
update_vals = {} update_vals = {}
field_map = { field_map = {
"partner_name": not candidate.partner_name, "partner_name": not candidate.partner_name,
@ -988,8 +1009,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"resume_type": not getattr(candidate, "resume_type", False), "resume_type": not getattr(candidate, "resume_type", False),
} }
for field_name, can_update in field_map.items(): for field_name, can_update in field_map.items():
if can_update and values.get(field_name): if can_update and values.get(field_name) and not current_update:
update_vals[field_name] = values[field_name] update_vals[field_name] = values[field_name]
elif values.get(field_name) and current_update:
if field_name == 'partner_name':
update_vals[field_name] = values.get('partner_name')
else:
update_vals[field_name] = values[field_name]
return update_vals return update_vals
def _sync_candidate_resume_histories(self, candidate, parsed_data): def _sync_candidate_resume_histories(self, candidate, parsed_data):

View File

@ -9,6 +9,18 @@ from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
class HRApplicant(models.Model): class HRApplicant(models.Model):
_inherit = 'hr.applicant' _inherit = 'hr.applicant'
_track_duration_field = 'recruitment_stage_id' _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) hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
primary_skill_match_percentage = fields.Float( 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) 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) submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True)
client_submission_date = fields.Datetime(string="Submission Date", 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_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True)
refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage") refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage")
refused_comments = fields.Text(string='Reject Comments') refused_comments = fields.Text(string='Reject Comments')
is_on_hold = fields.Boolean(string="Is On Hold", default=False) is_on_hold = fields.Boolean(string="Is On Hold", default=False)
hold_state = fields.Selection( hold_state = fields.Selection(
[('hold', 'On Hold')], [('hold', 'On Hold')],
string="Hold Status", string="Hold Status",
compute='_compute_hold_state', compute='_compute_hold_state',
) )
stage_comment_ids = fields.One2many( stage_comment_ids = fields.One2many(
'hr.applicant.stage.comment', 'hr.applicant.stage.comment',
'applicant_id', 'applicant_id',
string='Stage Comments', string='Stage Comments',
) )
stage_comment_count = fields.Integer(compute='_compute_stage_comment_count') stage_comment_count = fields.Integer(compute='_compute_stage_comment_count')
stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips') stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips')
@api.depends('is_on_hold') @api.depends('is_on_hold')
def _compute_hold_state(self): def _compute_hold_state(self):
@ -63,63 +75,63 @@ class HRApplicant(models.Model):
rec.is_on_hold = True rec.is_on_hold = True
return {'type': 'ir.actions.client', 'tag': 'reload'} return {'type': 'ir.actions.client', 'tag': 'reload'}
def action_toggle_chatter_visibility(self): def action_toggle_chatter_visibility(self):
for record in self: for record in self:
record.hide_chatter_suggestion = not record.hide_chatter_suggestion record.hide_chatter_suggestion = not record.hide_chatter_suggestion
return {'type': 'ir.actions.client', 'tag': 'reload'} return {'type': 'ir.actions.client', 'tag': 'reload'}
@api.depends('stage_comment_ids') @api.depends('stage_comment_ids')
def _compute_stage_comment_count(self): def _compute_stage_comment_count(self):
for applicant in self: for applicant in self:
applicant.stage_comment_count = len(applicant.stage_comment_ids) 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') @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): def _compute_stage_comment_tooltips(self):
for applicant in self: for applicant in self:
grouped_comments = {} grouped_comments = {}
for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()): for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()):
if not comment.stage_id: if not comment.stage_id:
continue continue
line = '%s: %s' % (comment.user_id.name, comment.comment) line = '%s: %s' % (comment.user_id.name, comment.comment)
grouped_comments.setdefault(comment.stage_id.id, []).append(line) grouped_comments.setdefault(comment.stage_id.id, []).append(line)
applicant.stage_comment_tooltips = { applicant.stage_comment_tooltips = {
stage_id: '\n'.join(comments[-4:]) stage_id: '\n'.join(comments[-4:])
for stage_id, comments in grouped_comments.items() for stage_id, comments in grouped_comments.items()
} }
def action_open_stage_comment_wizard(self): def action_open_stage_comment_wizard(self):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': _('Stage Comments'), 'name': _('Stage Comments'),
'res_model': 'applicant.stage.comment.wizard', 'res_model': 'applicant.stage.comment.wizard',
'view_mode': 'form', 'view_mode': 'form',
'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id, 'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id,
'target': 'new', 'target': 'new',
'context': { 'context': {
'active_id': self.id, 'active_id': self.id,
'default_applicant_id': self.id, 'default_applicant_id': self.id,
'default_stage_id': self.recruitment_stage_id.id, 'default_stage_id': self.recruitment_stage_id.id,
}, },
} }
@api.onchange('submitted_to_client') @api.onchange('submitted_to_client')
def _onchange_submitted_to_client(self): def _onchange_submitted_to_client(self):
for applicant in self: for applicant in self:
if applicant.submitted_to_client: if applicant.submitted_to_client:
applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now() applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now()
applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id
else: else:
applicant.client_submission_date = False applicant.client_submission_date = False
applicant.submitted_stage = False applicant.submitted_stage = False
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
for vals in vals_list: for vals in vals_list:
if vals.get('submitted_to_client'): if vals.get('submitted_to_client'):
vals.setdefault('client_submission_date', fields.Datetime.now()) vals.setdefault('client_submission_date', fields.Datetime.now())
vals.setdefault('submitted_stage', vals.get('recruitment_stage_id')) vals.setdefault('submitted_stage', vals.get('recruitment_stage_id'))
return super().create(vals_list) return super().create(vals_list)
@api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids') @api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
def _compute_skill_match_percentages(self): def _compute_skill_match_percentages(self):
@ -153,27 +165,27 @@ class HRApplicant(models.Model):
stage_ids = stages.sudo()._search(search_domain, order=stages._order) stage_ids = stages.sudo()._search(search_domain, order=stages._order)
return stages.browse(stage_ids) return stages.browse(stage_ids)
def write(self, vals): def write(self, vals):
if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1: if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1:
for applicant in self: for applicant in self:
applicant_vals = dict(vals) applicant_vals = dict(vals)
applicant_vals.setdefault('client_submission_date', fields.Datetime.now()) 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_vals['submitted_stage'] = vals.get('recruitment_stage_id') or applicant.recruitment_stage_id.id or False
applicant.write(applicant_vals) applicant.write(applicant_vals)
return True return True
if vals.get('submitted_to_client'): if vals.get('submitted_to_client'):
vals.setdefault('client_submission_date', fields.Datetime.now()) vals.setdefault('client_submission_date', fields.Datetime.now())
if 'submitted_stage' not in vals: if 'submitted_stage' not in vals:
submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id
if submitted_stage: if submitted_stage:
vals['submitted_stage'] = submitted_stage vals['submitted_stage'] = submitted_stage
elif vals.get('submitted_to_client') is False: elif vals.get('submitted_to_client') is False:
vals.setdefault('client_submission_date', False) vals.setdefault('client_submission_date', False)
vals.setdefault('submitted_stage', False) vals.setdefault('submitted_stage', False)
if 'recruitment_stage_id' in vals: if 'recruitment_stage_id' in vals:
blocked_records = self.filtered( blocked_records = self.filtered(
lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id') lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id')
) )
if blocked_records: if blocked_records:
raise ValidationError(_("You cannot change the stage of an applicant while it is on hold. Please unhold it first.")) raise ValidationError(_("You cannot change the stage of an applicant while it is on hold. Please unhold it first."))
if 'stage_id' in vals: if 'stage_id' in vals:

View File

@ -2,14 +2,23 @@ from odoo import models, fields, api, _
from odoo.exceptions import ValidationError 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): class HRApplicant(models.Model):
_inherit = 'hr.applicant' _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'), family_education_employer_details_status = fields.Selection([('pending', 'Pending'),
@ -27,15 +36,6 @@ class HRApplicant(models.Model):
raise ValidationError(_("No Data to Validate")) 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): class FamilyDetails(models.Model):
_inherit = "family.details" _inherit = "family.details"

View File

@ -10,6 +10,8 @@ class HrCandidate(models.Model):
_sql_constraints = [ _sql_constraints = [
('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'), ('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'),
] ]
_rec_names_search = ['partner_name', 'candidate_sequence', 'partner_phone']
#personal Details #personal Details
candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False) 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) hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)

View File

@ -27,6 +27,12 @@ patch(RecruitmentFormController.prototype, {
this._matchPanelSearchTerm = ""; this._matchPanelSearchTerm = "";
this._matchPanelActiveTab = "candidates"; this._matchPanelActiveTab = "candidates";
this._matchPanelAddingCandidateId = null; this._matchPanelAddingCandidateId = null;
this._primarySkillsExpanded = false;
this._secondarySkillsExpanded = false;
this._matchPanelScrollPosition = {
candidates: 0,
applicants: 0,
};
onMounted(() => this._syncRecruitmentMatchPanel()); onMounted(() => this._syncRecruitmentMatchPanel());
onPatched(() => this._syncRecruitmentMatchPanel()); onPatched(() => this._syncRecruitmentMatchPanel());
@ -161,7 +167,7 @@ patch(RecruitmentFormController.prototype, {
_renderSkillTags(skillNames, className) { _renderSkillTags(skillNames, className) {
if (!skillNames.length) { if (!skillNames.length) {
return `<span class="o_hr_match_chip ${className}">${escapeHtml(_t("None"))}</span>`; return `<span class="o_hr_match_chip ${className}">${escapeHtml(_t("No Skills Added"))}</span>`;
} }
return skillNames return skillNames
.map((skillName) => `<span class="o_hr_match_chip ${className}">${escapeHtml(skillName)}</span>`) .map((skillName) => `<span class="o_hr_match_chip ${className}">${escapeHtml(skillName)}</span>`)
@ -288,6 +294,7 @@ patch(RecruitmentFormController.prototype, {
? _t("Applicants added to this recruitment will appear here with the same match insights.") ? _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.") : _t("Try refreshing after updating the recruitment skills or candidate pool.")
); );
const previousScroll = this._matchPanelScrollPosition[this._matchPanelActiveTab] || 0;
panel.innerHTML = ` panel.innerHTML = `
<div class="o_hr_match_panel_backdropless"> <div class="o_hr_match_panel_backdropless">
@ -295,7 +302,7 @@ patch(RecruitmentFormController.prototype, {
<div> <div>
<p>${escapeHtml(_t("Candidate Pool"))}</p> <p>${escapeHtml(_t("Candidate Pool"))}</p>
<h3>${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}</h3> <h3>${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}</h3>
<span>${escapeHtml(`${payload.candidate_count || 0} ${_t("pool candidates")}${payload.applicant_count || 0} ${_t("applicants")}`)}</span> <span>${escapeHtml(`${payload.candidate_count || 0} ${_t(" pool candidates ")}${payload.applicant_count || 0} ${_t(" applicants")}`)}</span>
</div> </div>
<div class="o_hr_match_panel_actions"> <div class="o_hr_match_panel_actions">
<button type="button" class="btn btn-light o_hr_match_refresh">${escapeHtml(_t("Refresh"))}</button> <button type="button" class="btn btn-light o_hr_match_refresh">${escapeHtml(_t("Refresh"))}</button>
@ -322,28 +329,88 @@ patch(RecruitmentFormController.prototype, {
</label> </label>
</section> </section>
<section class="o_hr_match_panel_skill_summary"> <section class="o_hr_match_panel_skill_summary">
<div>
<label>${escapeHtml(_t("Primary Skills"))}</label> ${
<div class="o_hr_match_chip_row"> payload.primary_skill_names?.length
${this._renderSkillTags(payload.primary_skill_names || [], "o_hr_match_chip_primary")} ? `
</div> <div class="o_hr_match_skill_header"
</div> data-toggle="primary">
<div> <span>
<label>${escapeHtml(_t("Secondary Skills"))}</label> ${this._primarySkillsExpanded ? "▼" : "▶"}
<div class="o_hr_match_chip_row"> ${escapeHtml(_t("Primary Skills"))}
${this._renderSkillTags(payload.secondary_skill_names || [], "o_hr_match_chip_secondary")} </span>
</div>
</div> <span class="o_hr_match_skill_count">
</section> (${payload.primary_skill_names.length})
</span>
</div>
${
this._primarySkillsExpanded
? `
<div class="o_hr_match_chip_row">
${this._renderSkillTags(payload.primary_skill_names, "o_hr_match_chip_primary")}
</div>
`
: ""
}
`
: ""
}
${
payload.secondary_skill_names?.length
? `
<div class="o_hr_match_skill_header"
data-toggle="secondary">
<span>
${this._secondarySkillsExpanded ? "▼" : "▶"}
${escapeHtml(_t("Secondary Skills"))}
</span>
<span class="o_hr_match_skill_count">
(${payload.secondary_skill_names.length})
</span>
</div>
${
this._secondarySkillsExpanded
? `
<div class="o_hr_match_chip_row">
${this._renderSkillTags(payload.secondary_skill_names, "o_hr_match_chip_secondary")}
</div>
`
: ""
}
`
: ""
}
</section>
<section class="o_hr_match_panel_body ${this._matchPanelLoading ? "o_hr_match_panel_body_loading" : ""}"> <section class="o_hr_match_panel_body ${this._matchPanelLoading ? "o_hr_match_panel_body_loading" : ""}">
${this._matchPanelLoading ? `<div class="o_hr_match_loading">${escapeHtml(_t("Refreshing matches..."))}</div>` : activeCards} ${this._matchPanelLoading ? `<div class="o_hr_match_loading">${escapeHtml(_t("Refreshing matches..."))}</div>` : activeCards}
</section> </section>
</div> </div>
`; `;
panel.classList.add("o_hr_match_panel_open"); 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_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) => { panel.querySelectorAll(".o_hr_match_tab").forEach((tabButton) => {
tabButton.addEventListener("click", () => { tabButton.addEventListener("click", () => {
this._matchPanelActiveTab = tabButton.dataset.tab || "candidates"; 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(); const button = this._getRecruitmentMatchPanelButton();
if (button) { if (button) {
button.classList.add("o_hr_match_fab_hidden"); button.classList.add("o_hr_match_fab_hidden");

View File

@ -100,6 +100,7 @@
margin-top: 6px; margin-top: 6px;
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: white;
} }
.o_hr_match_panel_header span { .o_hr_match_panel_header span {
@ -416,6 +417,44 @@
display: grid; display: grid;
gap: 14px; 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) { @media (min-width: 768px) {
.o_hr_match_skill_split { .o_hr_match_skill_split {

View File

@ -7,6 +7,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//list" position="attributes"> <xpath expr="//list" position="attributes">
<attribute name="decoration-muted">hold_state == 'hold'</attribute> <attribute name="decoration-muted">hold_state == 'hold'</attribute>
<attribute name="default_order">id desc</attribute>
</xpath> </xpath>
<xpath expr="//field[@name='message_needaction']" position="after"> <xpath expr="//field[@name='message_needaction']" position="after">
<field name="is_on_hold" column_invisible="1"/> <field name="is_on_hold" column_invisible="1"/>
@ -16,7 +17,7 @@
</xpath> </xpath>
<xpath expr="//field[@name='stage_id']" position="after"> <xpath expr="//field[@name='stage_id']" position="after">
<field name="recruitment_stage_id"/> <field name="recruitment_stage_id"/>
<field name="hold_state" widget="badge" decoration-muted="hold_state == 'hold'" optional="show" nolabel="1"/> <field name="hold_state" string="Hold State" widget="badge" decoration-muted="hold_state == 'hold'" optional="show"/>
<field name="submitted_to_client" optional="show"/> <field name="submitted_to_client" optional="show"/>
<field name="client_submission_date" optional="show"/> <field name="client_submission_date" optional="show"/>
<field name="submitted_stage" optional="hide"/> <field name="submitted_stage" optional="hide"/>
@ -289,6 +290,26 @@
<xpath expr="//field[@name='kanban_state']" position="attributes"> <xpath expr="//field[@name='kanban_state']" position="attributes">
<attribute name="invisible">is_on_hold</attribute> <attribute name="invisible">is_on_hold</attribute>
</xpath> </xpath>
<xpath expr="//t[@t-name='card']/field[@name='categ_ids']" position="replace"/>
<xpath expr="//t[@t-name='card']/field[@name='applicant_properties']" position="replace"/>
<xpath expr="//t[@t-name='card']/field[@name='job_id']" position="replace">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<field name="job_id" invisible="context.get('search_default_job_id', False)" on_change="1"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="applicant_properties" widget="properties"/>
<!-- <field name="partner_name" class="fw-bold fs-5"/>-->
</div>
<t t-if="record.candidate_image.raw_value">
<field name="candidate_image"
widget="image"
class="rounded-circle ms-2"
options="{'size': [48, 48]}"/>
</t>
</div>
</xpath>
</field> </field>
</record> </record>
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view"> <record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
@ -297,7 +318,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<kanban highlight_color="color" default_group_by="recruitment_stage_id" <kanban highlight_color="color" default_group_by="recruitment_stage_id"
class="o_kanban_applicant o_search_matching_applicant" class="o_kanban_applicant o_search_matching_applicant"
quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1"> quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1" default_order="id desc">
<field name="recruitment_stage_id" options='{"group_by_tooltip": {"requirements": "Requirements"}}'/> <field name="recruitment_stage_id" options='{"group_by_tooltip": {"requirements": "Requirements"}}'/>
<field name="legend_normal"/> <field name="legend_normal"/>
<field name="legend_blocked"/> <field name="legend_blocked"/>

View File

@ -29,7 +29,7 @@
name="action_validate_personal_details" name="action_validate_personal_details"
type="object" type="object"
class="btn btn-danger" class="btn btn-danger"
invisible="not employee_id"> invisible="employee_id">
<div> <div>
Click here to save, Validate and Update into Employee Data Click here to save, Validate and Update into Employee Data
<field name="personal_details_status" <field name="personal_details_status"

View File

@ -276,6 +276,16 @@
</xpath> </xpath>
</field> </field>
</record> </record>
<record id="hr_candidate_view_tree_inherit" model="ir.ui.view">
<field name="name">hr.candidate.view.tree.inherit</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="default_order">id desc</attribute>
</xpath>
</field>
</record>
<record id="hr_candidate_view_kanban_inherit" model="ir.ui.view"> <record id="hr_candidate_view_kanban_inherit" model="ir.ui.view">
<field name="name">hr.candidate.view.kanban.inherit</field> <field name="name">hr.candidate.view.kanban.inherit</field>
<field name="model">hr.candidate</field> <field name="model">hr.candidate</field>
@ -284,14 +294,30 @@
<!-- Ensure applicant_ids is included in the kanban field list --> <!-- Ensure applicant_ids is included in the kanban field list -->
<xpath expr="//kanban" position="inside"> <xpath expr="//kanban" position="inside">
<field name="applicant_ids" context="{'active_test': False}"/> <field name="applicant_ids" context="{'active_test': False}"/>
<field name="candidate_image"/>
</xpath> </xpath>
<xpath expr="//kanban" position="attributes"> <xpath expr="//kanban" position="attributes">
<attribute name="context">{'active_test': False, 'kanban': True}</attribute> <attribute name="context">{'active_test': False, 'kanban': True}</attribute>
<attribute name="default_order">id desc</attribute>
</xpath> </xpath>
<xpath expr="//field[@name='partner_name']" position="before"> <xpath expr="//field[@name='partner_name']" position="before">
<field t-if="record.candidate_sequence.raw_value" name="candidate_sequence" class="fw-bold fs-4"/> <div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<field t-if="record.candidate_sequence.raw_value"
name="candidate_sequence"
class="fw-bold fs-4"/>
<!-- <field name="partner_name" class="fw-bold fs-5"/>-->
</div>
<t t-if="record.candidate_image.raw_value">
<field name="candidate_image"
widget="image"
class="rounded-circle ms-2"
options="{'size': [48, 48]}"/>
</t>
</div>
</xpath> </xpath>
<xpath expr="//t[@t-name='card']" position="inside"> <xpath expr="//t[@t-name='card']" position="inside">

View File

@ -98,7 +98,6 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
lambda a: a.attachment_type == 'previous_employer').mapped('name') lambda a: a.attachment_type == 'previous_employer').mapped('name')
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name') other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
print(request_upload_url)
email_context = { email_context = {
'applicant_request_form_id': rec.request_form_id.id, 'applicant_request_form_id': rec.request_form_id.id,
'applicant_request_form_token': request_token, 'applicant_request_form_token': request_token,

View File

@ -37,7 +37,6 @@ class HrmsEmployeeDashboard(http.Controller):
calendar_attendances = self._get_attendances(employee, calendar_start, calendar_end) calendar_attendances = self._get_attendances(employee, calendar_start, calendar_end)
calendar_leaves = self._get_leaves(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) calendar_public_holidays = self._get_public_holidays(employee, calendar_start, calendar_end)
print(self._expense_data(employee, range_start, range_end))
return { return {
"success": True, "success": True,
"employee": self._employee_card(employee), "employee": self._employee_card(employee),
@ -325,12 +324,6 @@ class HrmsEmployeeDashboard(http.Controller):
("date", "<=", date_to), ("date", "<=", date_to),
]) ])
for expense in emp_expenses: 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 totals[expense.date.strftime("%Y-%m")] += expense.total_amount or 0.0
state_totals[expense.state or "draft"] += expense.total_amount or 0.0 state_totals[expense.state or "draft"] += expense.total_amount or 0.0

View File

@ -12,6 +12,7 @@
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/mail_template.xml', 'data/mail_template.xml',
'views/stages.xml',
'views/offer_letter_views.xml', 'views/offer_letter_views.xml',
'views/hr_applicant_offer_views.xml', 'views/hr_applicant_offer_views.xml',
'views/offer_response_templates.xml', 'views/offer_response_templates.xml',

View File

@ -1,3 +1,4 @@
from . import stages
from . import offer_letter from . import offer_letter
from . import hr_applicant from . import hr_applicant
from . import hr_candidate from . import hr_candidate

View File

@ -26,6 +26,8 @@ class HRApplicant(models.Model):
store=False, 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') @api.depends('offer_letter_ids', 'offer_letter_ids.create_date', 'offer_letter_ids.state')
def _compute_current_offer_letter(self): def _compute_current_offer_letter(self):
for applicant in self: for applicant in self:

View File

@ -174,12 +174,12 @@ class OfferLetter(models.Model):
first_day = today.replace(day=1) first_day = today.replace(day=1)
last_day = today.replace(day=calendar.monthrange(today.year, today.month)[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_from': first_day,
'date_to': last_day, 'date_to': last_day,
}) })
contract = self.env['hr.contract'].new({ contract = self.env['hr.contract'].sudo().new({
'date_start': first_day, 'date_start': first_day,
'date_end': last_day, 'date_end': last_day,
'l10n_in_medical_insurance':self.mi, 'l10n_in_medical_insurance':self.mi,
@ -205,7 +205,7 @@ class OfferLetter(models.Model):
'inputs': {}, 'inputs': {},
} }
blacklisted_ids = set(self.env.context.get('prevent_payslip_computation_line_ids', [])) 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): if rule.id in blacklisted_ids or not rule._satisfy_condition(localdict):
continue continue
qty = 1.0 qty = 1.0
@ -223,15 +223,15 @@ class OfferLetter(models.Model):
except Exception as e: except Exception as e:
raise UserError(_("Error in rule %s: %s") % (rule.name, str(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 rule_code = rule.code
previous_amount = localdict.get(rule.code, 0.0) previous_amount = localdict.get(rule.code, 0.0)
category_code = rule.category_id.code 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 # Make sure _sum_salary_rule_category method exists
if hasattr(rule.category_id, '_sum_salary_rule_category'): 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 localdict[rule_code] = total
rules_dict[rule_code] = rule rules_dict[rule_code] = rule

View File

@ -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")

View File

@ -26,7 +26,7 @@
<xpath expr="//button[@name='action_share_applicant']" position="before"> <xpath expr="//button[@name='action_share_applicant']" position="before">
<button name="action_request_offer_release" string="Request Offer Release" type="object" class="btn-primary" <button name="action_request_offer_release" string="Request Offer Release" type="object" class="btn-primary"
groups="hr_recruitment.group_hr_recruitment_user" groups="hr_recruitment.group_hr_recruitment_user"
invisible="not id"/> invisible="not id or not request_offer_release"/>
</xpath> </xpath>
<xpath expr="//notebook/page[@name='request_forms']" position="after"> <xpath expr="//notebook/page[@name='request_forms']" position="after">
<page string="Offer Letters" name="offer_letters"> <page string="Offer Letters" name="offer_letters">

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="hr_recruitment_stage_offer_request_form_extended">
<field name="name">hr.recruitment.stage.form.offer.request.extended</field>
<field name="model">hr.recruitment.stage</field>
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_stage_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='fold']" position="after">
<field name="request_offer_release"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -70,7 +70,7 @@ class OfferReleaseRequestWizard(models.TransientModel):
def _get_default_pay_structure(self, applicant): def _get_default_pay_structure(self, applicant):
company = applicant.company_id or self.env.company company = applicant.company_id or self.env.company
return self.env['hr.payroll.structure'].search([ return self.env['hr.payroll.structure'].sudo().search([
], limit=1) ], limit=1)
def _get_default_manager(self, applicant): def _get_default_manager(self, applicant):
@ -104,7 +104,7 @@ class OfferReleaseRequestWizard(models.TransientModel):
f"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>" f"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>"
) )
mail = self.env['mail.mail'].create({ mail = self.env['mail.mail'].sudo().create({
'email_from': self.email_from, 'email_from': self.email_from,
'email_to': self.email_to, 'email_to': self.email_to,
'email_cc': self.email_cc, 'email_cc': self.email_cc,
@ -114,5 +114,5 @@ class OfferReleaseRequestWizard(models.TransientModel):
'model': 'offer.letter', 'model': 'offer.letter',
'res_id': offer_letter.id, 'res_id': offer_letter.id,
}) })
mail.send() mail.sudo().send()
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}

View File

@ -20,19 +20,15 @@ class ProjectDashboardController(http.Controller):
# 1. Get project data # 1. Get project data
project_data = self._get_project_data(project) project_data = self._get_project_data(project)
print(project_data)
# 2. Get tasks data # 2. Get tasks data
tasks_data = self._get_tasks_data(project) tasks_data = self._get_tasks_data(project)
print(tasks_data)
# 3. Get employee performance # 3. Get employee performance
employee_performance = self._get_employee_performance(project) employee_performance = self._get_employee_performance(project)
print(employee_performance)
# 4. Get budget data # 4. Get budget data
budget_data = self._get_budget_data(project) budget_data = self._get_budget_data(project)
print(budget_data)
return { return {
'success': True, 'success': True,

View File

@ -7,4 +7,8 @@
color: var(--body-color, #374151); color: var(--body-color, #374151);
padding: 0.35rem 0.75rem; padding: 0.35rem 0.75rem;
} }
.nav-link.active {
color: #fff !important;
}
} }

View File

@ -36,6 +36,12 @@ class ApplicantCandidate(models.Model):
string="Resume Type" string="Resume Type"
) )
certificate_ids = fields.Many2many(
'applicant.certificate',
string='Certificates'
)
def action_open_resume_candidate(self): def action_open_resume_candidate(self):
self.ensure_one() self.ensure_one()
@ -152,9 +158,10 @@ class CandidateApplicant(models.Model):
string="Online Tests" string="Online Tests"
) )
certificate_ids = fields.One2many( certificate_ids = fields.Many2many(
'applicant.certificate', 'applicant.certificate',
'applicant_id', related='candidate_id.certificate_ids',
readonly=False,
string='Certificates' string='Certificates'
) )

View File

@ -51,4 +51,25 @@
.job-name { .job-name {
font-size: 0.9rem; font-size: 0.9rem;
}
/* Make the email field occupy the available width */
.o_field_widget[name="email_from"] {
width: 100% !important;
max-width: none !important;
flex: 1 1 auto !important;
}
/* Input inside the email field */
.o_field_widget[name="email_from"] input {
width: 100% !important;
max-width: none !important;
min-width: 350px !important; /* adjust as needed */
}
/* If rendered as a link instead of input */
.o_field_widget[name="email_from"] a {
white-space: nowrap !important;
overflow: visible !important;
text-overflow: clip !important;
} }

View File

@ -105,6 +105,7 @@
<div class="candidate-name"> <div class="candidate-name">
<field name="candidate_id" <field name="candidate_id"
options="{'no_quick_create': True}"
nolabel="1" nolabel="1"
can_create="True" can_create="True"
can_write="True"/> can_write="True"/>
@ -268,7 +269,7 @@
<!-- EXPERIENCE TAB --> <!-- EXPERIENCE TAB -->
<page string="Past Experience"> <page string="Past Experience">
<field name="employer_history" nolabel="1"> <field name="employer_history" nolabel="1" force_save="1">
<kanban create="1" class="o_kanban_mobile"> <kanban create="1" class="o_kanban_mobile">

View File

@ -84,6 +84,232 @@
options="{'color_field':'color','no_create':True}"/> options="{'color_field':'color','no_create':True}"/>
</group> </group>
</group> </group>
<notebook>
<!-- EXPERIENCE TAB -->
<page string="Past Experience">
<field name="employer_history" nolabel="1" force_save="1">
<kanban create="1" class="o_kanban_mobile">
<templates>
<t t-name="card">
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
style="border-left:4px solid #4F46E5 !important;">
<div class="d-flex justify-content-between">
<div class="fw-bold fs-4 text-dark">
<field name="designation"/>
</div>
</div>
<div class="mt-2">
<i class="fa fa-building me-1 text-primary"/>
<strong>Company:</strong>
<field name="company_name"/>
</div>
<div class="mt-2 text-muted">
<i class="fa fa-calendar me-1 text-dark"/>
<field name="date_of_joining"/>
-
<field name="last_working_day"/>
</div>
<div class="mt-2 text-muted">
<i class="fa fa-money me-1 text-success"/>
<span class="fw-bold text-dark me-1">
CTC :
</span>
<field name="ctc"/>
</div>
<div class="mt-3 p-3 rounded-3 border bg-light"
style="border-left:4px solid #875A7B !important;">
<div class="fw-bold mb-2 text-primary fs-6">
<i class="fa fa-file-text-o me-1"/>
Summary
</div>
<div style="font-size:13px; line-height:1.7;"
class="text-dark">
<field name="summary"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
<!-- FORM -->
<form string="Employer Details">
<sheet>
<group>
<group>
<field name="company_name"/>
<field name="designation"/>
</group>
<group>
<field name="date_of_joining"/>
<field name="last_working_day"/>
<field name="ctc"/>
</group>
<group>
<field name="summary"
widget="html"/>
</group>
</group>
</sheet>
</form>
</field>
</page>
<!-- EDUCATION TAB -->
<page string="Education">
<field name="education_history" nolabel="1">
<kanban create="1" class="o_kanban_mobile">
<templates>
<t t-name="card">
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
style="border-left:4px solid #4F46E5 !important;">
<div class="fw-bold fs-5 text-dark">
<field name="name"/>
</div>
<div class="mt-2">
<i class="fa fa-university me-1 text-primary"/>
<strong>University:</strong>
<field name="university"/>
</div>
<div class="mt-2">
<strong>Education:</strong>
<span class="badge rounded-pill text-bg-light border px-3 py-2">
<field name="education_type"/>
</span>
</div>
<div class="mt-2 text-muted">
<i class="fa fa-calendar me-1 text-dark"/>
<field name="start_year"/>
-
<field name="end_year"/>
</div>
<div class="mt-3 p-2 rounded-3 bg-light">
<span class="fw-bold text-dark">
Grade:
</span>
<field name="marks_or_grade"/>
</div>
</div>
</t>
</templates>
</kanban>
<form>
<sheet>
<group>
<group>
<field name="education_type"/>
<field name="name"/>
<field name="university"/>
</group>
<group>
<field name="start_year"/>
<field name="end_year"/>
<field name="marks_or_grade"/>
</group>
</group>
</sheet>
</form>
</field>
</page>
<page string="Certificates">
<field name="certificate_ids" nolabel="1">
<kanban create="1" class="o_kanban_mobile">
<templates>
<t t-name="card">
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
style="border-left:4px solid #F97316 !important;">
<div class="fw-bold fs-5 text-dark">
<i class="fa fa-certificate text-warning me-2"/>
<field name="name"/>
</div>
<div class="mt-3">
<field name="certificate_file"
filename="certificate_filename"
widget="binary"/>
</div>
</div>
</t>
</templates>
</kanban>
<!-- FORM -->
<form string="Certificate">
<sheet>
<group>
<field name="name"/>
<field name="certificate_file"
filename="certificate_filename"/>
<field name="certificate_filename"
invisible="1"/>
</group>
</sheet>
</form>
</field>
</page>
</notebook>
</page> </page>
<page string="Resume" name="resume_page"> <page string="Resume" name="resume_page">
<group> <group>

View File

@ -956,7 +956,6 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
)) ))
error_message = 'An application already exists for %s Duplicates might be rejected. %s '%(value,recruiter_contact) error_message = 'An application already exists for %s Duplicates might be rejected. %s '%(value,recruiter_contact)
print(error_message)
return { return {
'message': _(error_message) 'message': _(error_message)
} }

View File

@ -16,6 +16,4 @@ class Website(models.Model):
result = super()._search_get_details(search_type, order, options) result = super()._search_get_details(search_type, order, options)
if search_type in ['job_requests', 'all']: if search_type in ['job_requests', 'all']:
result.append(self.env['hr.job.recruitment']._search_get_detail(self, order, options)) result.append(self.env['hr.job.recruitment']._search_get_detail(self, order, options))
print(result)
print("hello result")
return result return result