Recruitment changes
This commit is contained in:
parent
90211776a1
commit
604d556501
|
|
@ -1,2 +1,3 @@
|
|||
from . import models
|
||||
from . import wizards
|
||||
from . import models
|
||||
from . import wizards
|
||||
from . import controllers
|
||||
|
|
|
|||
|
|
@ -45,10 +45,11 @@
|
|||
'views/slab_master.xml',
|
||||
'views/emp_it_declaration.xml',
|
||||
'views/report_it_tax_statement.xml',
|
||||
'report/report_action.xml',
|
||||
'report/it_tax_template.xml',
|
||||
'views/it_tax_menu_and_wizard_view.xml',
|
||||
'wizards/hr_tds_calculation.xml',
|
||||
'report/report_action.xml',
|
||||
'report/it_tax_template.xml',
|
||||
'views/it_tax_menu_and_wizard_view.xml',
|
||||
'views/employee_payslip_download_wizard_views.xml',
|
||||
'wizards/hr_tds_calculation.xml',
|
||||
'wizards/children_education_costing.xml',
|
||||
'wizards/employee_life_insurance.xml',
|
||||
'wizards/nsc_declaration.xml',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import io
|
||||
import zipfile
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.http import Controller, content_disposition, request, route
|
||||
|
||||
|
||||
class EmployeePayslipDownloadController(Controller):
|
||||
|
||||
@route('/employee_it_declaration/my_payslips/<int:wizard_id>', type='http', auth='user')
|
||||
def download_my_payslips(self, wizard_id, **kwargs):
|
||||
wizard = request.env['employee.payslip.download.wizard'].browse(wizard_id)
|
||||
if not wizard.exists() or wizard.create_uid != request.env.user:
|
||||
return request.not_found()
|
||||
|
||||
try:
|
||||
payslips = wizard._get_payslips_for_download()
|
||||
except (AccessError, UserError):
|
||||
return request.not_found()
|
||||
|
||||
if wizard.download_type == 'single':
|
||||
payslip = payslips[:1]
|
||||
report, pdf_content = wizard._get_pdf_content(payslip)
|
||||
headers = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(pdf_content)),
|
||||
('Content-Disposition', content_disposition(wizard._get_pdf_filename(payslip, report))),
|
||||
]
|
||||
return request.make_response(pdf_content, headers=headers)
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
used_filenames = set()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', compression=zipfile.ZIP_DEFLATED) as payslip_zip:
|
||||
for payslip in payslips:
|
||||
report, pdf_content = wizard._get_pdf_content(payslip)
|
||||
filename = self._deduplicate_filename(wizard._get_pdf_filename(payslip, report), used_filenames)
|
||||
payslip_zip.writestr(filename, pdf_content)
|
||||
|
||||
zip_content = zip_buffer.getvalue()
|
||||
headers = [
|
||||
('Content-Type', 'application/zip'),
|
||||
('Content-Length', len(zip_content)),
|
||||
('Content-Disposition', content_disposition(wizard._get_zip_filename())),
|
||||
]
|
||||
return request.make_response(zip_content, headers=headers)
|
||||
|
||||
@staticmethod
|
||||
def _deduplicate_filename(filename, used_filenames):
|
||||
if filename not in used_filenames:
|
||||
used_filenames.add(filename)
|
||||
return filename
|
||||
|
||||
stem, extension = filename.rsplit('.', 1) if '.' in filename else (filename, '')
|
||||
counter = 2
|
||||
while True:
|
||||
candidate = _('%(stem)s (%(counter)s)', stem=stem, counter=counter)
|
||||
if extension:
|
||||
candidate = '%s.%s' % (candidate, extension)
|
||||
if candidate not in used_filenames:
|
||||
used_filenames.add(candidate)
|
||||
return candidate
|
||||
counter += 1
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from . import payroll_periods
|
||||
from . import investment_types
|
||||
from . import investment_costings
|
||||
from . import emp_it_declaration
|
||||
from . import slab_master
|
||||
from . import it_tax_statement
|
||||
from . import it_tax_statement_wiz
|
||||
from . import payroll_periods
|
||||
from . import investment_types
|
||||
from . import emp_it_declaration
|
||||
from . import investment_costings
|
||||
from . import slab_master
|
||||
from . import it_tax_statement
|
||||
from . import it_tax_statement_wiz
|
||||
from . import employee_payslip_download_wiz
|
||||
|
|
|
|||
|
|
@ -1,7 +1,42 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class EmpITDeclaration(models.Model):
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ITDeclarationSubmittedLockMixin(models.AbstractModel):
|
||||
_name = 'it.declaration.submitted.lock.mixin'
|
||||
_description = 'IT Declaration Submitted Lock Mixin'
|
||||
|
||||
def _get_related_it_declarations(self):
|
||||
if 'it_declaration_id' in self._fields:
|
||||
return self.mapped('it_declaration_id')
|
||||
if 'child_education_id' in self._fields:
|
||||
return self.mapped('child_education_id.it_declaration_id')
|
||||
if 'parent_id' in self._fields:
|
||||
return self.mapped('parent_id.it_declaration_id')
|
||||
return self.env['emp.it.declaration']
|
||||
|
||||
def _check_it_declaration_is_editable(self):
|
||||
if any(declaration.state == 'submitted' for declaration in self._get_related_it_declarations()):
|
||||
raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records._check_it_declaration_is_editable()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
self._check_it_declaration_is_editable()
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
self._check_it_declaration_is_editable()
|
||||
return super().unlink()
|
||||
|
||||
|
||||
class EmpITDeclaration(models.Model):
|
||||
_name = 'emp.it.declaration'
|
||||
_rec_name = 'employee_id'
|
||||
_description = "IT Declaration"
|
||||
|
|
@ -34,11 +69,17 @@ class EmpITDeclaration(models.Model):
|
|||
else:
|
||||
rec.display_period_label = ""
|
||||
|
||||
tax_regime = fields.Selection([
|
||||
('new', 'New Regime'),
|
||||
('old', 'Old Regime')
|
||||
], string="Tax Regime", required=True, default='new')
|
||||
|
||||
tax_regime = fields.Selection([
|
||||
('new', 'New Regime'),
|
||||
('old', 'Old Regime')
|
||||
], string="Tax Regime", required=True, default='new')
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('submitted', 'Submitted'),
|
||||
], string="Status", default='draft', required=True, copy=False)
|
||||
return_reason = fields.Text(string="Return Reason", copy=False)
|
||||
is_payroll_manager = fields.Boolean(compute='_compute_is_payroll_manager')
|
||||
|
||||
total_investment = fields.Float(string='Total Investment')
|
||||
|
||||
costing_details_generated = fields.Boolean(default=False)
|
||||
|
|
@ -50,7 +91,29 @@ class EmpITDeclaration(models.Model):
|
|||
string='Visible Investment Costings',
|
||||
)
|
||||
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
|
||||
is_section_open = fields.Boolean()
|
||||
is_section_open = fields.Boolean()
|
||||
|
||||
def _compute_is_payroll_manager(self):
|
||||
is_manager = self.env.user.has_group('hr_payroll.group_hr_payroll_manager')
|
||||
for rec in self:
|
||||
rec.is_payroll_manager = is_manager
|
||||
|
||||
def _check_submitted_write_allowed(self, vals):
|
||||
protected_vals = set(vals) - {'state', 'return_reason'}
|
||||
submitted_records = self.filtered(lambda rec: rec.state == 'submitted')
|
||||
if submitted_records and protected_vals:
|
||||
raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.'))
|
||||
if submitted_records and set(vals) & {'state', 'return_reason'} and not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'):
|
||||
raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.'))
|
||||
|
||||
def write(self, vals):
|
||||
self._check_submitted_write_allowed(vals)
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
if any(rec.state == 'submitted' for rec in self):
|
||||
raise UserError(_('Submitted IT declarations cannot be deleted.'))
|
||||
return super().unlink()
|
||||
@api.depends('costing_details_generated','investment_costing_ids')
|
||||
def _compute_investment_costing(self):
|
||||
for rec in self:
|
||||
|
|
@ -346,3 +409,155 @@ class EmpITDeclaration(models.Model):
|
|||
rec._ensure_investment_costing_records()
|
||||
rec._update_investment_amounts()
|
||||
rec.costing_details_generated = True
|
||||
|
||||
def action_submit(self):
|
||||
for rec in self:
|
||||
rec._update_investment_amounts()
|
||||
rec.write({
|
||||
'state': 'submitted',
|
||||
'return_reason': False,
|
||||
})
|
||||
|
||||
def action_return_to_draft(self):
|
||||
for rec in self:
|
||||
if not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'):
|
||||
raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.'))
|
||||
if not rec.return_reason:
|
||||
raise UserError(_('Please enter a return reason before returning the declaration to draft.'))
|
||||
rec.state = 'draft'
|
||||
|
||||
def action_download_submission_pdf(self):
|
||||
self.ensure_one()
|
||||
if self.state != 'submitted':
|
||||
raise UserError(_('You can download the IT declaration PDF only after it is submitted.'))
|
||||
return self.env.ref('employee_it_declaration.action_report_it_tax_statement').report_action(self)
|
||||
|
||||
def _get_regime_label(self):
|
||||
self.ensure_one()
|
||||
return dict(self._fields['tax_regime'].selection).get(self.tax_regime, self.tax_regime)
|
||||
|
||||
def _get_binary_link(self, record, field_name, filename_field=False):
|
||||
if not record or not record[field_name]:
|
||||
return False
|
||||
params = {
|
||||
'model': record._name,
|
||||
'id': record.id,
|
||||
'field': field_name,
|
||||
'download': 'true',
|
||||
}
|
||||
if filename_field:
|
||||
params['filename_field'] = filename_field
|
||||
return '/web/content?%s' % urlencode(params)
|
||||
|
||||
def _prepare_report_line(self, line):
|
||||
name = line.investment_type_line_id.name if line.investment_type_line_id else ''
|
||||
return {
|
||||
'name': name,
|
||||
'declaration_amount': line.declaration_amount,
|
||||
'proof_amount': line.proof_amount,
|
||||
'limit': line.limit,
|
||||
'remarks': line.remarks,
|
||||
'attachment_name': line.proof_name or _('Proof'),
|
||||
'attachment_url': self._get_binary_link(line, 'proof', 'proof_name'),
|
||||
}
|
||||
|
||||
def _get_report_sections(self):
|
||||
self.ensure_one()
|
||||
old_regime = self.tax_regime == 'old'
|
||||
section_fields = [
|
||||
(_('Past Employment'), 'past_employment_costings' if old_regime else 'past_employment_costings_new'),
|
||||
(_('US 80C'), 'us80c_costings' if old_regime else 'us80c_costings_new'),
|
||||
(_('US 80D'), {
|
||||
'self_family': 'us80d_costings' if old_regime else 'us80d_costings_new',
|
||||
'self_family_parent': 'us80d_costings_parents' if old_regime else 'us80d_costings_parents_new',
|
||||
'self_family_senior_parent': 'us80d_costings_senior_parents' if old_regime else 'us80d_costings_senior_parents_new',
|
||||
}[self.us80d_selection_type]),
|
||||
(_('US 10'), 'us10_costings' if old_regime else 'us10_costings_new'),
|
||||
(_('US 80G'), 'us80g_costings' if old_regime else 'us80g_costings_new'),
|
||||
(_('Chapter VIA'), 'chapter_via_costings' if old_regime else 'chapter_via_costings_new'),
|
||||
(_('US 17'), 'us17_costings' if old_regime else 'us17_costings_new'),
|
||||
(_('Other Income / Loss'), 'other_il_costings' if old_regime else 'other_il_costings_new'),
|
||||
(_('Other Declarations'), 'other_declaration_costings' if old_regime else 'other_declaration_costings_new'),
|
||||
]
|
||||
sections = []
|
||||
for title, field_name in section_fields:
|
||||
lines = self[field_name].filtered(lambda line: line.declaration_amount or line.proof_amount or line.remarks or line.proof)
|
||||
if lines:
|
||||
sections.append({
|
||||
'title': title,
|
||||
'lines': [self._prepare_report_line(line) for line in lines],
|
||||
})
|
||||
|
||||
if old_regime and self.house_rent_costings:
|
||||
sections.append({
|
||||
'title': _('House Rent'),
|
||||
'house_rent': True,
|
||||
'lines': [{
|
||||
'hra_exemption_type': dict(line._fields['hra_exemption_type'].selection).get(line.hra_exemption_type, ''),
|
||||
'rent_amount': line.rent_amount,
|
||||
'from_date': line.from_date,
|
||||
'to_date': line.to_date,
|
||||
'landlord_pan_status': dict(line._fields['landlord_pan_status'].selection).get(line.landlord_pan_status, ''),
|
||||
'landlord_pan_no': line.landlord_pan_no,
|
||||
'landlord_name_address': line.landlord_name_address,
|
||||
'remarks': line.remarks,
|
||||
'attachment_name': line.attachment_filename or _('Proof'),
|
||||
'attachment_url': self._get_binary_link(line, 'attachment', 'attachment_filename'),
|
||||
} for line in self.house_rent_costings],
|
||||
})
|
||||
return sections
|
||||
|
||||
def _get_report_extra_sections(self):
|
||||
self.ensure_one()
|
||||
extra_sections = []
|
||||
us80c_lines = self.us80c_costings if self.tax_regime == 'old' else self.us80c_costings_new
|
||||
other_il_lines = self.other_il_costings if self.tax_regime == 'old' else self.other_il_costings_new
|
||||
|
||||
children_records = self.env['children.education'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||
if children_records:
|
||||
extra_sections.append({
|
||||
'title': _('Children Education Details'),
|
||||
'headers': [_('Child'), _('Name'), _('Class / Grade'), _('School / College'), _('Tuition Fee')],
|
||||
'rows': [[child.child_id, child.name, child.chile_class, child.organization, child.tuition_fee] for record in children_records for child in record.children_ids],
|
||||
})
|
||||
|
||||
insurance_records = self.env['us80c.insurance.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||
if insurance_records:
|
||||
extra_sections.append({
|
||||
'title': _('Life Insurance Details'),
|
||||
'headers': [_('Company'), _('Insured For'), _('Insured Name'), _('Policy No'), _('Premium'), _('Payment Date'), _('Exempt Amount')],
|
||||
'rows': [[line.name_of_insurance_company, line.insured_in_favour_of, line.name_of_insured, line.policy_number, line.premium_amount, line.payment_date, line.exempt_amount] for record in insurance_records for line in record.life_insurance_ids],
|
||||
})
|
||||
|
||||
nsc_records = self.env['nsc.declaration.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||
if nsc_records:
|
||||
extra_sections.append({
|
||||
'title': _('NSC Declaration Details'),
|
||||
'headers': [_('NSC Number'), _('Amount'), _('Payment Date')],
|
||||
'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date] for record in nsc_records for line in record.nsc_entry_ids],
|
||||
})
|
||||
|
||||
self_occupied_records = self.env['self.occupied.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||
if self_occupied_records:
|
||||
extra_sections.append({
|
||||
'title': _('Self Occupied Property Details'),
|
||||
'headers': [_('Address'), _('Period From'), _('Period To'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')],
|
||||
'rows': [[record.address, record.period_from, record.period_to, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in self_occupied_records],
|
||||
})
|
||||
|
||||
letout_records = self.env['letout.house.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||
if letout_records:
|
||||
extra_sections.append({
|
||||
'title': _('Let-out House Property Details'),
|
||||
'headers': [_('Address'), _('Rent'), _('Property Tax'), _('Water Tax'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')],
|
||||
'rows': [[record.address, record.rent_received, record.property_tax, record.water_tax, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in letout_records],
|
||||
})
|
||||
|
||||
nsc_interest_records = self.env['nsc.interest.line'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||
if nsc_interest_records:
|
||||
extra_sections.append({
|
||||
'title': _('NSC Interest Details'),
|
||||
'headers': [_('NSC Number'), _('Amount'), _('Payment Date'), _('Interest Amount')],
|
||||
'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date, line.nsc_interest_amount] for record in nsc_interest_records for line in record.nsc_entry_ids],
|
||||
})
|
||||
return [section for section in extra_sections if section['rows']]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class EmployeePayslipDownloadWizard(models.TransientModel):
|
||||
_name = 'employee.payslip.download.wizard'
|
||||
_rec_name = 'employee_id'
|
||||
_description = 'Employee Payslip Download Wizard'
|
||||
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
required=True,
|
||||
default=lambda self: self.env.user.employee_id.id,
|
||||
readonly=True,
|
||||
)
|
||||
download_type = fields.Selection(
|
||||
selection=[
|
||||
('single', 'Single Payslip'),
|
||||
('multi', 'Multiple Payslips'),
|
||||
],
|
||||
string='Download Option',
|
||||
required=True,
|
||||
default='single',
|
||||
)
|
||||
period_id = fields.Many2one(
|
||||
'payroll.period',
|
||||
string='Payroll Period',
|
||||
required=True,
|
||||
)
|
||||
period_line = fields.Many2one(
|
||||
'payroll.period.line',
|
||||
string='Month',
|
||||
domain="[('period_id', '=', period_id)]",
|
||||
)
|
||||
payslip_count = fields.Integer(
|
||||
string='Available Payslips',
|
||||
compute='_compute_payslip_count',
|
||||
)
|
||||
|
||||
@api.onchange('download_type', 'period_id')
|
||||
def _onchange_download_type_period_id(self):
|
||||
for rec in self:
|
||||
rec.period_line = False
|
||||
|
||||
@api.depends('download_type', 'period_id', 'period_line')
|
||||
def _compute_payslip_count(self):
|
||||
for rec in self:
|
||||
if not rec.period_id or (rec.download_type == 'single' and not rec.period_line):
|
||||
rec.payslip_count = 0
|
||||
else:
|
||||
rec.payslip_count = len(rec._get_available_payslips())
|
||||
|
||||
def _get_current_employee(self):
|
||||
employee = self.env.user.employee_id
|
||||
if not employee:
|
||||
raise UserError(_('No employee is linked to your user. Please contact HR.'))
|
||||
return employee
|
||||
|
||||
def _get_date_range(self):
|
||||
self.ensure_one()
|
||||
if self.download_type == 'single':
|
||||
if not self.period_line:
|
||||
raise UserError(_('Please select a month to download a single payslip.'))
|
||||
return self.period_line.from_date, self.period_line.to_date
|
||||
return self.period_id.from_date, self.period_id.to_date
|
||||
|
||||
def _get_available_payslips(self):
|
||||
self.ensure_one()
|
||||
employee = self._get_current_employee()
|
||||
date_from, date_to = self._get_date_range()
|
||||
if not date_from or not date_to:
|
||||
return self.env['hr.payslip']
|
||||
|
||||
return self.env['hr.payslip'].sudo().search([
|
||||
('employee_id', '=', employee.id),
|
||||
('state', 'in', ['done', 'paid']),
|
||||
('date_from', '>=', date_from),
|
||||
('date_to', '<=', date_to),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
], order='date_from asc, date_to asc, id asc')
|
||||
|
||||
def _get_payslips_for_download(self):
|
||||
self.ensure_one()
|
||||
payslips = self._get_available_payslips()
|
||||
if not payslips:
|
||||
raise UserError(_('No confirmed or paid payslip is available for the selected period.'))
|
||||
if self.download_type == 'single' and len(payslips) > 1:
|
||||
raise UserError(_('More than one payslip is available for this month. Please contact HR.'))
|
||||
return payslips
|
||||
|
||||
def _get_pdf_content(self, payslip):
|
||||
report = payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip')
|
||||
pdf_content, dummy = self.env['ir.actions.report'].sudo().with_context(
|
||||
lang=payslip.employee_id.lang or self.env.lang,
|
||||
)._render_qweb_pdf(report, payslip.id, data={'company_id': payslip.company_id})
|
||||
return report, pdf_content
|
||||
|
||||
def _get_pdf_filename(self, payslip, report=False):
|
||||
report = report or payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip')
|
||||
if report.print_report_name:
|
||||
filename = safe_eval(report.print_report_name, {'object': payslip})
|
||||
else:
|
||||
filename = _('Payslip - %(employee)s - %(period)s', employee=payslip.employee_id.name, period=payslip.name)
|
||||
return self._sanitize_filename(filename, 'payslip') + '.pdf'
|
||||
|
||||
def _get_zip_filename(self):
|
||||
self.ensure_one()
|
||||
employee = self._get_current_employee()
|
||||
filename = _('Payslips - %(employee)s - %(period)s', employee=employee.name, period=self.period_id.name)
|
||||
return self._sanitize_filename(filename, 'payslips') + '.zip'
|
||||
|
||||
@api.model
|
||||
def _sanitize_filename(self, filename, fallback):
|
||||
filename = re.sub(r'[\\/:*?"<>|]+', '-', str(filename or '')).strip(' .')
|
||||
return filename or fallback
|
||||
|
||||
def action_download_payslips(self):
|
||||
self.ensure_one()
|
||||
self._get_payslips_for_download()
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/employee_it_declaration/my_payslips/%s' % self.id,
|
||||
'target': 'self',
|
||||
}
|
||||
|
|
@ -5,9 +5,10 @@ import calendar
|
|||
import re
|
||||
|
||||
|
||||
class investmentCostings(models.Model):
|
||||
_name = 'investment.costings'
|
||||
_rec_name = 'investment_type_id'
|
||||
class investmentCostings(models.Model):
|
||||
_name = 'investment.costings'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_id'
|
||||
|
||||
investment_type_id = fields.Many2one('it.investment.type')
|
||||
amount = fields.Integer()
|
||||
|
|
@ -23,9 +24,10 @@ class investmentCostings(models.Model):
|
|||
related='it_declaration_id.period_id'
|
||||
)
|
||||
|
||||
class pastEmpcostingType(models.Model):
|
||||
_name = 'past_employment.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class pastEmpcostingType(models.Model):
|
||||
_name = 'past_employment.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
|
|
@ -102,9 +104,10 @@ class pastEmpcostingType(models.Model):
|
|||
rec.declaration_amount = 0 # fallback
|
||||
|
||||
|
||||
class us80cCostingType(models.Model):
|
||||
_name = 'us80c.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class us80cCostingType(models.Model):
|
||||
_name = 'us80c.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -138,9 +141,10 @@ class us80cCostingType(models.Model):
|
|||
'view_mode': self.action_id.view_mode,
|
||||
'target': 'new',
|
||||
}
|
||||
class us80dCostingType(models.Model):
|
||||
_name = 'us80d.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class us80dCostingType(models.Model):
|
||||
_name = 'us80d.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -155,9 +159,10 @@ class us80dCostingType(models.Model):
|
|||
|
||||
|
||||
|
||||
class us10CostingType(models.Model):
|
||||
_name = 'us10.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class us10CostingType(models.Model):
|
||||
_name = 'us10.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -171,9 +176,10 @@ class us10CostingType(models.Model):
|
|||
limit = fields.Integer()
|
||||
|
||||
|
||||
class us80gCostingType(models.Model):
|
||||
_name = 'us80g.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class us80gCostingType(models.Model):
|
||||
_name = 'us80g.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -187,9 +193,10 @@ class us80gCostingType(models.Model):
|
|||
limit = fields.Integer()
|
||||
|
||||
|
||||
class chapterViaCostingType(models.Model):
|
||||
_name = 'chapter.via.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class chapterViaCostingType(models.Model):
|
||||
_name = 'chapter.via.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -203,9 +210,10 @@ class chapterViaCostingType(models.Model):
|
|||
limit = fields.Integer()
|
||||
|
||||
|
||||
class us17CostingType(models.Model):
|
||||
_name = 'us17.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class us17CostingType(models.Model):
|
||||
_name = 'us17.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -217,9 +225,10 @@ class us17CostingType(models.Model):
|
|||
proof_name = fields.Char()
|
||||
limit = fields.Integer()
|
||||
|
||||
class OtherILCostingType(models.Model):
|
||||
_name = 'other.il.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class OtherILCostingType(models.Model):
|
||||
_name = 'other.il.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -254,9 +263,10 @@ class OtherILCostingType(models.Model):
|
|||
}
|
||||
|
||||
|
||||
class OtherDeclarationCostingType(models.Model):
|
||||
_name = 'other.declaration.costing.type'
|
||||
_rec_name = 'investment_type_line_id'
|
||||
class OtherDeclarationCostingType(models.Model):
|
||||
_name = 'other.declaration.costing.type'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_rec_name = 'investment_type_line_id'
|
||||
|
||||
costing_type = fields.Many2one('investment.costings')
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -269,9 +279,10 @@ class OtherDeclarationCostingType(models.Model):
|
|||
limit = fields.Integer()
|
||||
|
||||
|
||||
class HouseRentDeclaration(models.Model):
|
||||
_name = 'house.rent.declaration'
|
||||
_description = 'House Rent Declaration'
|
||||
class HouseRentDeclaration(models.Model):
|
||||
_name = 'house.rent.declaration'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'House Rent Declaration'
|
||||
|
||||
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -304,4 +315,4 @@ class HouseRentDeclaration(models.Model):
|
|||
if self.env.context.get('default_it_declaration_id'):
|
||||
costing_id = self.env['investment.costings'].sudo().search([('id','=',self.env.context.get('it_declaration_id')),('investment_type_id.investment_type','=','house_rent')],limit=1)
|
||||
vals['costing_type'] = costing_id.id
|
||||
return super().create(vals)
|
||||
return super().create(vals)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<report
|
||||
id="action_report_it_tax_statement"
|
||||
model="emp.it.declaration"
|
||||
string="IT Tax Statement"
|
||||
report_type="qweb-pdf"
|
||||
name="your_module_name.report_it_tax_statement"
|
||||
file="your_module_name.report_it_tax_statement"
|
||||
print_report_name="'IT_Tax_Statement_%s' % (object.employee_id.name)"
|
||||
/>
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="action_report_it_tax_statement" model="ir.actions.report">
|
||||
<field name="name">IT Declaration Submission</field>
|
||||
<field name="model">emp.it.declaration</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">employee_it_declaration.report_it_tax_statement</field>
|
||||
<field name="report_file">employee_it_declaration.report_it_tax_statement</field>
|
||||
<field name="print_report_name">'IT Declaration - %s - %s' % (object.employee_id.name or '', object.period_id.name or '')</field>
|
||||
<field name="binding_model_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -54,13 +54,14 @@ access_nsc_interest_entry_user,nsc.interest.entry,model_nsc_interest_entry,base.
|
|||
|
||||
access_house_rent_declaration_user,access.house.rent.declaration.user,model_house_rent_declaration,base.group_user,1,1,1,1
|
||||
|
||||
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
|
||||
access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0
|
||||
|
||||
access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1
|
||||
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
|
||||
access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0
|
||||
access_employee_payslip_download_wizard,employee.payslip.download.wizard,model_employee_payslip_download_wizard,base.group_user,1,1,1,0
|
||||
|
||||
access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1
|
||||
access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1
|
||||
|
||||
|
||||
access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1
|
||||
access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1
|
||||
access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1
|
||||
access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -5,36 +5,60 @@
|
|||
<field name="name">emp.it.declaration.list</field>
|
||||
<field name="model">emp.it.declaration</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
|
||||
<field name="period_id"/>
|
||||
<field name="total_investment"/>
|
||||
<field name="tax_regime"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<list>
|
||||
|
||||
<field name="employee_id"/>
|
||||
<field name="period_id"/>
|
||||
<field name="total_investment"/>
|
||||
<field name="tax_regime"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_emp_it_declaration_form" model="ir.ui.view">
|
||||
<field name="name">emp.it.declarations.form</field>
|
||||
<field name="model">emp.it.declaration</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="IT Declaration">
|
||||
<sheet>
|
||||
<div class="oe_title mb24">
|
||||
<div class="o_row">
|
||||
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="period_id" readonly="costing_details_generated"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_investment"/>
|
||||
<field name="costing_details_generated" invisible="1" force_save="1"/>
|
||||
<field name="house_rent_costing_id"/>
|
||||
<field name="show_past_employment" invisible="1"/>
|
||||
<field name="model">emp.it.declaration</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="IT Declaration">
|
||||
<header>
|
||||
<button name="action_submit"
|
||||
string="Submit"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'draft' or not costing_details_generated"/>
|
||||
<button name="action_download_submission_pdf"
|
||||
string="Download PDF"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-download"
|
||||
invisible="state != 'submitted'"/>
|
||||
<button name="action_return_to_draft"
|
||||
string="Return to Draft"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
invisible="state != 'submitted'"
|
||||
groups="hr_payroll.group_hr_payroll_manager"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,submitted"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title mb24">
|
||||
<div class="o_row">
|
||||
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated or state == 'submitted'"/>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="period_id" readonly="costing_details_generated or state == 'submitted'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_investment" readonly="state == 'submitted'"/>
|
||||
<field name="return_reason" placeholder="Reason for returning the declaration to draft..." readonly="state != 'submitted' or not is_payroll_manager"/>
|
||||
<field name="is_payroll_manager" invisible="1"/>
|
||||
<field name="costing_details_generated" invisible="1" force_save="1"/>
|
||||
<field name="house_rent_costing_id"/>
|
||||
<field name="show_past_employment" invisible="1"/>
|
||||
<field name="show_us_80c" invisible="1"/>
|
||||
<field name="show_us_80d" invisible="1"/>
|
||||
<field name="show_us_10" invisible="1"/>
|
||||
|
|
@ -49,9 +73,9 @@
|
|||
|
||||
<br/>
|
||||
<br/>
|
||||
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}"/>
|
||||
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" readonly="state == 'submitted'"/>
|
||||
<br/>
|
||||
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period & Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated"/>
|
||||
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period & Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated or state == 'submitted'"/>
|
||||
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
|
||||
|
||||
<group invisible="not costing_details_generated">
|
||||
|
|
@ -71,7 +95,7 @@
|
|||
<page string="Total Investment Costing">
|
||||
<group>
|
||||
<group>
|
||||
<field name="visible_investment_costing_ids" nolabel="1">
|
||||
<field name="visible_investment_costing_ids" nolabel="1" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0" edit="0">
|
||||
<field name="investment_type_id"/>
|
||||
<field name="amount"/>
|
||||
|
|
@ -102,7 +126,7 @@
|
|||
<!-- </page>-->
|
||||
<page name="past_employment_costings" string="PAST EMPLOYMENT" invisible="not show_past_employment">
|
||||
|
||||
<field name="past_employment_costings" invisible="tax_regime != 'old'">
|
||||
<field name="past_employment_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -112,7 +136,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="past_employment_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="past_employment_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -126,7 +150,7 @@
|
|||
|
||||
<page name="us_80c_costings" string="US 80C" invisible="not show_us_80c">
|
||||
|
||||
<field name="us80c_costings" invisible="tax_regime != 'old'">
|
||||
<field name="us80c_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -142,7 +166,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us80c_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="us80c_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -162,10 +186,10 @@
|
|||
</page>
|
||||
<page name="us_80d_costings" string="US 80D" invisible="not show_us_80d">
|
||||
<group>
|
||||
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated"/>
|
||||
<field name="us80d_health_checkup"/>
|
||||
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated" readonly="state == 'submitted'"/>
|
||||
<field name="us80d_health_checkup" readonly="state == 'submitted'"/>
|
||||
</group>
|
||||
<field name="us80d_costings" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family'">
|
||||
<field name="us80d_costings" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -175,7 +199,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us80d_costings_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family'">
|
||||
<field name="us80d_costings_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -185,7 +209,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us80d_costings_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_parent'">
|
||||
<field name="us80d_costings_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -196,7 +220,7 @@
|
|||
</list>
|
||||
|
||||
</field>
|
||||
<field name="us80d_costings_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_parent'">
|
||||
<field name="us80d_costings_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -207,7 +231,7 @@
|
|||
</list>
|
||||
|
||||
</field>
|
||||
<field name="us80d_costings_senior_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_senior_parent'">
|
||||
<field name="us80d_costings_senior_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -218,7 +242,7 @@
|
|||
</list>
|
||||
|
||||
</field>
|
||||
<field name="us80d_costings_senior_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_senior_parent'">
|
||||
<field name="us80d_costings_senior_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -231,7 +255,7 @@
|
|||
</field>
|
||||
</page>
|
||||
<page name="us_10_costing" string="US 10" invisible="not show_us_10">
|
||||
<field name="us10_costings" invisible="tax_regime != 'old'">
|
||||
<field name="us10_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -241,7 +265,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us10_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="us10_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -253,7 +277,7 @@
|
|||
</field>
|
||||
</page>
|
||||
<page name="us_80g_costing" string="US 80G" invisible="not show_us_80g">
|
||||
<field name="us80g_costings" invisible="tax_regime != 'old'">
|
||||
<field name="us80g_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -263,7 +287,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us80g_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="us80g_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -276,7 +300,7 @@
|
|||
</page>
|
||||
|
||||
<page name="chapter_via_costings" string="CHAPTER VIA" invisible="not show_chapter_via">
|
||||
<field name="chapter_via_costings" invisible="tax_regime != 'old'">
|
||||
<field name="chapter_via_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -286,7 +310,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -299,7 +323,7 @@
|
|||
|
||||
</page>
|
||||
<page name="us_17_costings" string="US 17" invisible="not show_us_17">
|
||||
<field name="us17_costings" invisible="tax_regime != 'old'">
|
||||
<field name="us17_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -309,7 +333,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="us17_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="us17_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -322,7 +346,7 @@
|
|||
</page>
|
||||
<page name="house_rent_costings" string="HOUSE RENT" invisible="not show_house_rent">
|
||||
<!-- <field name="house_rent_costing_line_ids"/>-->
|
||||
<field name="house_rent_costings" context="{
|
||||
<field name="house_rent_costings" readonly="state == 'submitted'" context="{
|
||||
'default_costing_type': house_rent_costing_id
|
||||
}">
|
||||
<list string="House Rent Declarations">
|
||||
|
|
@ -373,7 +397,7 @@
|
|||
</field>
|
||||
</page>
|
||||
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS" invisible="not show_other_i_or_l">
|
||||
<field name="other_il_costings" invisible="tax_regime != 'old'">
|
||||
<field name="other_il_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -390,7 +414,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="other_il_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="other_il_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -409,7 +433,7 @@
|
|||
</field>
|
||||
</page>
|
||||
<page name="other_declaration_costings" string="Other Declarations" invisible="not show_other_declaration">
|
||||
<field name="other_declaration_costings" invisible="tax_regime != 'old'">
|
||||
<field name="other_declaration_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -419,7 +443,7 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="other_declaration_costings_new" invisible="tax_regime != 'new'">
|
||||
<field name="other_declaration_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||
<field name="declaration_amount" width="130px"/>
|
||||
|
|
@ -443,10 +467,13 @@
|
|||
<field name="res_model">emp.it.declaration</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_hr_payroll_emp_root" name="Payroll" sequence="190" web_icon="hr_payroll,static/description/icon.png" groups="base.group_user"/>
|
||||
|
||||
<menuitem id="menu_it_declarations" name="IT Declarations"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
action="action_emp_it_declaration" sequence="99"/>
|
||||
|
||||
<menuitem id="menu_it_declarations_emp" name="IT Declarations"
|
||||
parent="menu_hr_payroll_emp_root"
|
||||
action="action_emp_it_declaration" sequence="1"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_employee_payslip_download_wizard_form" model="ir.ui.view">
|
||||
<field name="name">employee.payslip.download.wizard.form</field>
|
||||
<field name="model">employee.payslip.download.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Download Payslips">
|
||||
<header>
|
||||
<button name="action_download_payslips"
|
||||
string="Download Payslip"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-download"
|
||||
invisible="download_type != 'single'"/>
|
||||
<button name="action_download_payslips"
|
||||
string="Download ZIP"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-archive-o"
|
||||
invisible="download_type != 'multi'"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
|
||||
<field name="download_type" widget="radio" options="{'horizontal': true}"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="period_id" options="{'no_edit': True, 'no_create': True, 'no_open': True}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="period_line"
|
||||
force_save="1"
|
||||
domain="[('period_id', '=', period_id)]"
|
||||
required="download_type == 'single'"
|
||||
invisible="download_type != 'single'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="payslip_count" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_employee_payslip_download_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Download Payslips</field>
|
||||
<field name="res_model">employee.payslip.download.wizard</field>
|
||||
<field name="path">download-payslips</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_employee_payslip_download_wizard"
|
||||
name="Salary Payslip"
|
||||
parent="employee_it_declaration.menu_hr_payroll_emp_root"
|
||||
action="action_employee_payslip_download_wizard"
|
||||
sequence="2"
|
||||
groups="base.group_user"/>
|
||||
</odoo>
|
||||
|
|
@ -87,7 +87,7 @@
|
|||
<field name="name">Generate Tax Statement</field>
|
||||
<field name="res_model">it.tax.statement.wizard</field>
|
||||
<field name="path">tax-statement</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new employment type
|
||||
|
|
@ -96,8 +96,9 @@
|
|||
</record>
|
||||
|
||||
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
action="action_it_tax_statement_wizard" sequence="99"/>
|
||||
parent="employee_it_declaration.menu_hr_payroll_emp_root"
|
||||
groups="base.group_user"
|
||||
action="action_it_tax_statement_wizard" sequence="3"/>
|
||||
|
||||
<record id="it_statement_paper_format" model="report.paperformat">
|
||||
<field name="name">A4 - statement</field>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,158 @@
|
|||
<odoo>
|
||||
<template id="report_it_tax_statement">
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>IT Tax Statement</h2>
|
||||
<p><strong>Employee:</strong> <t t-esc="doc.employee_id.name"/></p>
|
||||
<p><strong>Period:</strong> <t t-esc="doc.period_id.name"/></p>
|
||||
<p><strong>Tax Regime:</strong> <t t-esc="dict(doc._fields['tax_regime'].selection).get(doc.tax_regime)"/></p>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>Section</th><th>Amount</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.investment_costing_ids" t-as="line">
|
||||
<tr>
|
||||
<td><t t-esc="line.investment_type_id.name"/></td>
|
||||
<td><t t-esc="line.amount"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Total:</strong> <t t-esc="sum(doc.investment_costing_ids.mapped('amount'))"/></p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="report_it_tax_statement">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<style>
|
||||
.it-title { text-align: center; margin-bottom: 18px; }
|
||||
.it-title h2 { margin: 0; font-size: 21px; font-weight: 700; }
|
||||
.it-title p { margin: 4px 0 0; color: #555; }
|
||||
.it-section { margin-top: 18px; }
|
||||
.it-section h4 { font-size: 14px; font-weight: 700; border-bottom: 1px solid #999; padding-bottom: 4px; margin-bottom: 8px; }
|
||||
.it-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||
.it-table th { background: #f2f2f2; font-weight: 700; }
|
||||
.it-table th, .it-table td { border: 1px solid #ddd; padding: 5px; vertical-align: top; }
|
||||
.it-right { text-align: right; }
|
||||
.it-muted { color: #777; }
|
||||
</style>
|
||||
|
||||
<div class="it-title">
|
||||
<h2>IT Declaration Submission</h2>
|
||||
<p>
|
||||
<span t-esc="doc.employee_id.company_id.name"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<table class="it-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Employee</th>
|
||||
<td><span t-esc="doc.employee_id.name"/></td>
|
||||
<th>Payroll Period</th>
|
||||
<td><span t-esc="doc.period_id.name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Tax Regime</th>
|
||||
<td><span t-esc="doc._get_regime_label()"/></td>
|
||||
<th>Status</th>
|
||||
<td><span t-esc="dict(doc._fields['state'].selection).get(doc.state)"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Submitted On</th>
|
||||
<td><span t-esc="doc.write_date"/></td>
|
||||
<th>Total Investment</th>
|
||||
<td class="it-right"><span t-esc="'{:,.2f}'.format(doc.total_investment or 0.0)"/></td>
|
||||
</tr>
|
||||
<tr t-if="doc.return_reason">
|
||||
<th>Return Reason</th>
|
||||
<td colspan="3"><span t-esc="doc.return_reason"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<t t-set="sections" t-value="doc._get_report_sections()"/>
|
||||
<t t-if="not sections">
|
||||
<div class="it-section">
|
||||
<p class="it-muted">No declaration lines are available for the selected regime.</p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-foreach="sections" t-as="section">
|
||||
<div class="it-section">
|
||||
<h4><span t-esc="section.get('title')"/></h4>
|
||||
<t t-if="section.get('house_rent')">
|
||||
<table class="it-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Rent Amount</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Landlord PAN</th>
|
||||
<th>Remarks</th>
|
||||
<th>Proof</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="section.get('lines')" t-as="line">
|
||||
<td><span t-esc="line.get('hra_exemption_type')"/></td>
|
||||
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('rent_amount') or 0.0)"/></td>
|
||||
<td><span t-esc="line.get('from_date')"/></td>
|
||||
<td><span t-esc="line.get('to_date')"/></td>
|
||||
<td>
|
||||
<span t-esc="line.get('landlord_pan_status')"/>
|
||||
<br t-if="line.get('landlord_pan_no')"/>
|
||||
<span t-if="line.get('landlord_pan_no')" t-esc="line.get('landlord_pan_no')"/>
|
||||
</td>
|
||||
<td><span t-esc="line.get('remarks')"/></td>
|
||||
<td>
|
||||
<a t-if="line.get('attachment_url')" t-att-href="line.get('attachment_url')">
|
||||
<span t-esc="line.get('attachment_name')"/>
|
||||
</a>
|
||||
<span t-if="not line.get('attachment_url')" class="it-muted">No proof</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<table class="it-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Declaration Amount</th>
|
||||
<th>Proof Amount</th>
|
||||
<th>Limit</th>
|
||||
<th>Remarks</th>
|
||||
<th>Proof</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="section.get('lines')" t-as="line">
|
||||
<td><span t-esc="line.get('name')"/></td>
|
||||
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('declaration_amount') or 0.0)"/></td>
|
||||
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('proof_amount') or 0.0)"/></td>
|
||||
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('limit') or 0.0)"/></td>
|
||||
<td><span t-esc="line.get('remarks')"/></td>
|
||||
<td>
|
||||
<a t-if="line.get('attachment_url')" t-att-href="line.get('attachment_url')">
|
||||
<span t-esc="line.get('attachment_name')"/>
|
||||
</a>
|
||||
<span t-if="not line.get('attachment_url')" class="it-muted">No proof</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-foreach="doc._get_report_extra_sections()" t-as="extra_section">
|
||||
<div class="it-section">
|
||||
<h4><span t-esc="extra_section.get('title')"/></h4>
|
||||
<table class="it-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th t-foreach="extra_section.get('headers')" t-as="header">
|
||||
<span t-esc="header"/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="extra_section.get('rows')" t-as="row">
|
||||
<td t-foreach="row" t-as="cell">
|
||||
<span t-esc="cell"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
class ChildrenEducation(models.Model):
|
||||
_name = "children.education"
|
||||
_description = "Children Education"
|
||||
class ChildrenEducation(models.Model):
|
||||
_name = "children.education"
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = "Children Education"
|
||||
_rec_name = 'it_declaration_id'
|
||||
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration')
|
||||
|
|
@ -36,9 +37,10 @@ class ChildrenEducation(models.Model):
|
|||
# self.us80c_id.declaration_amount = self.total_count if self.us80c_id.limit > self.total_count else self.us80c_id.limit
|
||||
# return super().write(vals)
|
||||
|
||||
class ChildrenEducationCosting(models.Model):
|
||||
_name = 'children.education.costing'
|
||||
_description = "Children Education Costing"
|
||||
class ChildrenEducationCosting(models.Model):
|
||||
_name = 'children.education.costing'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = "Children Education Costing"
|
||||
|
||||
child_id = fields.Char('Child ID')
|
||||
name = fields.Char('Name of Child')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class US80CInsuranceLine(models.Model):
|
||||
_name = 'us80c.insurance.line'
|
||||
_description = 'US80C Insurance Line'
|
||||
class US80CInsuranceLine(models.Model):
|
||||
_name = 'us80c.insurance.line'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'US80C Insurance Line'
|
||||
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||
us80c_id = fields.Many2one('us80c.costing.type', string="80C Costing Type")
|
||||
|
|
@ -28,9 +29,10 @@ class US80CInsuranceLine(models.Model):
|
|||
|
||||
|
||||
|
||||
class EmployeeLifeInsurance(models.Model):
|
||||
_name = 'employee.life.insurance'
|
||||
_description = 'Employee Life Insurance'
|
||||
class EmployeeLifeInsurance(models.Model):
|
||||
_name = 'employee.life.insurance'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'Employee Life Insurance'
|
||||
|
||||
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from odoo import models, fields, api
|
||||
import math
|
||||
|
||||
class LetoutHouseProperty(models.Model):
|
||||
_name = 'letout.house.property'
|
||||
_description = 'Letout House Property Details'
|
||||
class LetoutHouseProperty(models.Model):
|
||||
_name = 'letout.house.property'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'Letout House Property Details'
|
||||
|
||||
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")
|
||||
|
|
@ -40,4 +41,4 @@ class LetoutHouseProperty(models.Model):
|
|||
print(income_loss)
|
||||
record.net_annual_value = net_annual_value
|
||||
record.deduction_for_repairs = round(deduction)
|
||||
record.income_loss = round(income_loss)
|
||||
record.income_loss = round(income_loss)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class NSCDeclarationLine(models.Model):
|
||||
_name = 'nsc.declaration.line'
|
||||
_description = 'NSC Declaration Line'
|
||||
class NSCDeclarationLine(models.Model):
|
||||
_name = 'nsc.declaration.line'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'NSC Declaration Line'
|
||||
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
||||
us80c_id = fields.Many2one('us80c.costing.type', string="80C Costing Type", required=True)
|
||||
|
|
@ -17,9 +18,10 @@ class NSCDeclarationLine(models.Model):
|
|||
rec.total_nsc_amount = sum(entry.nsc_amount for entry in rec.nsc_entry_ids)
|
||||
|
||||
|
||||
class NSCEntry(models.Model):
|
||||
_name = 'nsc.entry'
|
||||
_description = 'NSC Entry'
|
||||
class NSCEntry(models.Model):
|
||||
_name = 'nsc.entry'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'NSC Entry'
|
||||
|
||||
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
|
||||
nsc_number = fields.Char(string="NSC Number")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class NSCInterestLine(models.Model):
|
||||
_name = 'nsc.interest.line'
|
||||
_description = 'NSC Interest Line'
|
||||
class NSCInterestLine(models.Model):
|
||||
_name = 'nsc.interest.line'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'NSC Interest Line'
|
||||
|
||||
|
||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
||||
|
|
@ -21,9 +22,10 @@ class NSCInterestLine(models.Model):
|
|||
|
||||
|
||||
|
||||
class NSCEntry(models.Model):
|
||||
_name = 'nsc.interest.entry'
|
||||
_description = 'NSC Entry'
|
||||
class NSCEntry(models.Model):
|
||||
_name = 'nsc.interest.entry'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'NSC Entry'
|
||||
|
||||
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
|
||||
nsc_number = fields.Char(string="NSC Number")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SelfOccupiedProperty(models.Model):
|
||||
_name = 'self.occupied.property'
|
||||
_description = 'Self Occupied House Property Details'
|
||||
class SelfOccupiedProperty(models.Model):
|
||||
_name = 'self.occupied.property'
|
||||
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||
_description = 'Self Occupied House Property Details'
|
||||
|
||||
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")
|
||||
|
|
@ -25,4 +26,4 @@ class SelfOccupiedProperty(models.Model):
|
|||
@api.onchange('interest_paid_to')
|
||||
def onchange_interest_paid_to(self):
|
||||
for rec in self:
|
||||
rec.income_loss = -(rec.interest_paid_to)
|
||||
rec.income_loss = -(rec.interest_paid_to)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class website_hr_recruitment_applications_extended(website_hr_recruitment_applic
|
|||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found()
|
||||
if applicant and applicant.send_post_onboarding_form:
|
||||
if applicant:
|
||||
if applicant.post_onboarding_form_status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
||||
'applicant': applicant
|
||||
|
|
@ -38,7 +38,7 @@ class website_hr_recruitment_applications_extended(website_hr_recruitment_applic
|
|||
return f"Error: Applicant with ID {applicant_id} not found"
|
||||
|
||||
# Business logic check
|
||||
if not applicant.send_post_onboarding_form or applicant.post_onboarding_form_status != 'done':
|
||||
if applicant.post_onboarding_form_status != 'done':
|
||||
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
|
||||
|
||||
# Get the template
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from odoo import fields, api, models, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
|
|
@ -31,8 +31,7 @@ class HREmployee(models.Model):
|
|||
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
||||
})
|
||||
rec.applicant_id = application.id
|
||||
rec.sudo().applicant_id.send_post_onboarding_form = True
|
||||
return rec.sudo().applicant_id.send_post_onboarding_form_to_candidate()
|
||||
return rec.sudo().applicant_id.send_jod_form_to_employee()
|
||||
|
||||
|
||||
class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||
|
|
@ -44,11 +43,12 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
def _onchange_template_id(self):
|
||||
""" Update the email body and recipients based on the selected template. """
|
||||
if self.template_id:
|
||||
record_id = self.env.context.get('active_id')
|
||||
record_id = self.env.context.get('applicant_id')
|
||||
model = self.env.context.get('active_model')
|
||||
|
||||
if model == 'hr.applicant':
|
||||
if model == 'applicant.request.forms':
|
||||
applicant = self.env['hr.applicant'].browse(record_id)
|
||||
elif model == 'hr.applicant':
|
||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||
else:
|
||||
if model == 'hr.employee':
|
||||
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
||||
|
|
@ -74,14 +74,30 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
for rec in self:
|
||||
self.ensure_one()
|
||||
context = self.env.context
|
||||
active_id = context.get('active_id')
|
||||
active_id = context.get('applicant_id')
|
||||
model = context.get('active_model')
|
||||
request_token = False
|
||||
request_upload_url = False
|
||||
|
||||
if model == 'hr.applicant':
|
||||
if model == 'applicant.request.forms':
|
||||
applicant = self.env['hr.applicant'].browse(active_id)
|
||||
elif model == 'hr.applicant':
|
||||
applicant = self.env['hr.applicant'].browse(context.get('active_id'))
|
||||
elif model == 'hr.employee':
|
||||
applicant = self.env['hr.employee'].browse(active_id).applicant_id
|
||||
else:
|
||||
if model == 'hr.employee':
|
||||
applicant = self.env['hr.employee'].browse(active_id).applicant_id
|
||||
applicant = self.env['hr.applicant'].browse(active_id)
|
||||
|
||||
if rec.is_pre_onboarding_attachment_request and not rec.request_form_id:
|
||||
raise UserError("A document request form is required before sending this email.")
|
||||
|
||||
if rec.request_form_id:
|
||||
request_token = rec.request_form_id._issue_new_access_token()
|
||||
base_url = self.get_base_url()
|
||||
request_upload_url = (
|
||||
f"{base_url}/FTPROTECH/DocRequests/"
|
||||
f"{applicant.id}/{rec.request_form_id.id}?token={request_token}"
|
||||
)
|
||||
|
||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in rec.req_attachment_ids]
|
||||
|
||||
|
|
@ -93,22 +109,29 @@ 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')
|
||||
|
||||
# Prepare context for the template
|
||||
email_context = {
|
||||
'applicant_request_form_id': rec.request_form_id.id,
|
||||
'applicant_request_form_token': request_token,
|
||||
'applicant_request_form_url': request_upload_url,
|
||||
'personal_docs': personal_docs,
|
||||
'education_docs': education_docs,
|
||||
'previous_employer_docs': previous_employer_docs,
|
||||
'other_docs': other_docs,
|
||||
}
|
||||
rendered_subject = template.with_context(**email_context)._render_field(
|
||||
'subject', [applicant.id]
|
||||
)[applicant.id]
|
||||
rendered_body_html = template.with_context(**email_context)._render_field(
|
||||
'body_html', [applicant.id], compute_lang=True
|
||||
)[applicant.id]
|
||||
email_values = {
|
||||
'email_from': rec.email_from,
|
||||
'email_to': rec.email_to,
|
||||
'email_cc': rec.email_cc,
|
||||
'subject': rec.email_subject,
|
||||
'subject': rendered_subject or rec.email_subject,
|
||||
'body_html': rendered_body_html,
|
||||
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
||||
|
||||
}
|
||||
# Use 'with_context' to override the email template fields dynamically
|
||||
if rec.send_mail:
|
||||
template.sudo().with_context(default_body_html=rec.email_body,
|
||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
||||
|
|
@ -116,7 +139,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
base_url = self.get_base_url()
|
||||
|
||||
if rec.is_pre_onboarding_attachment_request:
|
||||
applicant.doc_requests_form_status = 'email_sent_to_candidate'
|
||||
rec.request_form_id.status = 'email_sent_to_candidate'
|
||||
else:
|
||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||
applicant.joining_form_link = '%s/FTPROTECH/JoiningForm/%s'%(base_url,applicant.id)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='post_onboarding_form_status']" position="after">
|
||||
<xpath expr="//field[@name='candidate_id']" position="after">
|
||||
<field name="joining_form_link" force_save="1" readonly="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@
|
|||
'views/res_config_settings.xml',
|
||||
'views/hr_employee.xml',
|
||||
'views/bank_details.xml',
|
||||
'wizards/work_location_wizard.xml'
|
||||
'wizards/work_location_wizard.xml',
|
||||
'wizards/html_preview_wizard.xml'
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,3 +10,18 @@ class EmployerHistory(models.Model):
|
|||
last_working_day = fields.Date(string='Last Working Day')
|
||||
ctc = fields.Char(string='CTC')
|
||||
employee_id = fields.Many2one('hr.employee')
|
||||
summary = fields.Html(string='Summary')
|
||||
|
||||
def action_open_html_popup(self):
|
||||
for rec in self:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Summary',
|
||||
'res_model': 'html.preview.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_html_content': rec.summary,
|
||||
'active_id': rec.id,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
from odoo import models, fields
|
||||
|
||||
class HtmlPreviewWizard(models.TransientModel):
|
||||
_name = 'html.preview.wizard'
|
||||
_description = 'Edit HTML Popup'
|
||||
|
||||
html_content = fields.Html()
|
||||
|
||||
def action_save_html(self):
|
||||
active_id = self.env.context.get('active_id')
|
||||
|
||||
record = self.env['employer.history'].browse(active_id)
|
||||
|
||||
record.summary = self.html_content
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_html_preview_wizard_form" model="ir.ui.view">
|
||||
<field name="name">html.preview.wizard.form</field>
|
||||
<field name="model">html.preview.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Summary">
|
||||
<sheet>
|
||||
<field name="html_content" widget="html"/>
|
||||
</sheet>
|
||||
|
||||
<footer>
|
||||
<button name="action_save_html"
|
||||
string="Save"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
|
||||
<button string="Cancel"
|
||||
special="cancel"
|
||||
class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
from calendar import monthrange
|
||||
from datetime import datetime
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
|
|
@ -232,6 +233,29 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"degree": {"type": "string", "description": "Highest degree or main qualification"},
|
||||
"skills": {"type": "list", "description": "All explicit technical and functional skills"},
|
||||
"summary": {"type": "string", "description": "Short professional summary from the resume"},
|
||||
"education_history": {
|
||||
"type": "list",
|
||||
"description": (
|
||||
"List of education entries if present in the resume. Each item must be an object with "
|
||||
"education_type (10/inter/graduation/post_graduation/additional), specialization, "
|
||||
"university, start_year, end_year, and marks_or_grade."
|
||||
),
|
||||
},
|
||||
"employer_history": {
|
||||
"type": "list",
|
||||
"description": (
|
||||
"List of previous employers if present in the resume. Each item must be an object with "
|
||||
"company_name, designation, date_of_joining, last_working_day, ctc, and work_description. "
|
||||
"work_description should capture the candidate's responsibilities, projects, and achievements for that employer."
|
||||
),
|
||||
},
|
||||
"family_details": {
|
||||
"type": "list",
|
||||
"description": (
|
||||
"List of family members only if explicitly present in the resume. Each item must be an object with "
|
||||
"relation_type (father/mother/spouse/kid1/kid2), name, contact_no, dob, and location."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_jd_required_fields(self):
|
||||
|
|
@ -255,7 +279,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"Return precise contact details, experience, location, and skills. "
|
||||
"Do not guess missing values. "
|
||||
"Normalize skills into clean individual names. "
|
||||
"For experience values, return numeric years when clearly inferable."
|
||||
"For experience values, return numeric years when clearly inferable. "
|
||||
"If the resume contains education details, previous employer details, or family details, return them as structured arrays of objects using the requested field names. "
|
||||
"For each employer entry, extract the role-specific work description into work_description. "
|
||||
"Only include entries that are explicitly present in the document. "
|
||||
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
|
||||
)
|
||||
|
||||
|
|
@ -283,12 +310,16 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
if self.update_existing_candidates:
|
||||
candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals))
|
||||
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
||||
if self.target_model == "candidate":
|
||||
self._sync_candidate_resume_histories(candidate, parsed_data)
|
||||
message = _("Matched existing candidate: %s") % candidate.display_name
|
||||
return candidate, "updated", message
|
||||
|
||||
self._ensure_resume_creation_allowed(parsed_data, line.file_name)
|
||||
candidate = self.env["hr.candidate"].create(candidate_vals)
|
||||
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
||||
if self.target_model == "candidate":
|
||||
self._sync_candidate_resume_histories(candidate, parsed_data)
|
||||
message = _("Created candidate: %s") % candidate.display_name
|
||||
return candidate, "created", message
|
||||
|
||||
|
|
@ -298,6 +329,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
("hr_job_recruitment", "=", self.job_recruitment_id.id),
|
||||
], limit=1)
|
||||
if existing_applicant:
|
||||
self._sync_applicant_resume_histories(existing_applicant, parsed_data)
|
||||
return existing_applicant, "existing", _("Existing application reused for this job request.")
|
||||
|
||||
applicant_vals = {
|
||||
|
|
@ -319,6 +351,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
applicant_vals["total_exp_type"] = "year"
|
||||
applicant_vals = {key: value for key, value in applicant_vals.items() if value not in (False, None, "")}
|
||||
applicant = self.env["hr.applicant"].create(applicant_vals)
|
||||
self._sync_applicant_resume_histories(applicant, parsed_data)
|
||||
return applicant, "created", _("Created application for job request %s.") % self.job_recruitment_id.display_name
|
||||
|
||||
def _find_existing_candidate(self, parsed_data):
|
||||
|
|
@ -368,6 +401,9 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
||||
|
||||
data["skills"] = self._merge_resume_skills(data.get("skills") or [], extracted_text)
|
||||
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
||||
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
||||
data["family_details"] = self._normalize_resume_list(data.get("family_details"))
|
||||
return data
|
||||
|
||||
def _post_process_jd_data(self, parsed_data, extracted_text):
|
||||
|
|
@ -691,6 +727,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"resume_name": line.file_name,
|
||||
"resume_type": resume_mimetype,
|
||||
"type_id": degree_id,
|
||||
"candidate_sequence" : self.env['ir.sequence'].next_by_code('hr.job.candidate.sequence') or '/'
|
||||
}
|
||||
return {key: value for key, value in vals.items() if value not in (False, None, "")}
|
||||
|
||||
|
|
@ -726,6 +763,351 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
update_vals[field_name] = values[field_name]
|
||||
return update_vals
|
||||
|
||||
def _sync_candidate_resume_histories(self, candidate, parsed_data):
|
||||
applicant = candidate.with_context(active_test=False).applicant_ids[:1]
|
||||
history_vals = self._prepare_resume_history_write_vals(candidate, parsed_data)
|
||||
candidate.write(history_vals)
|
||||
if applicant:
|
||||
self._sync_applicant_resume_histories(applicant, parsed_data)
|
||||
|
||||
def _sync_applicant_resume_histories(self, applicant, parsed_data):
|
||||
history_vals = self._prepare_resume_history_write_vals(applicant, parsed_data)
|
||||
if history_vals:
|
||||
applicant.write(history_vals)
|
||||
|
||||
def _prepare_resume_history_write_vals(self, record, parsed_data):
|
||||
vals = {}
|
||||
education_commands = self._prepare_education_history_commands(parsed_data.get("education_history"))
|
||||
employer_commands = self._prepare_employer_history_commands(parsed_data.get("employer_history"))
|
||||
family_commands = self._prepare_family_details_commands(parsed_data.get("family_details"))
|
||||
education_commands = self._filter_new_history_commands(
|
||||
education_commands,
|
||||
record.education_history,
|
||||
self._education_signature_from_command,
|
||||
self._education_signature_from_record,
|
||||
)
|
||||
employer_commands = self._filter_new_history_commands(
|
||||
employer_commands,
|
||||
record.employer_history,
|
||||
self._employer_signature_from_command,
|
||||
self._employer_signature_from_record,
|
||||
)
|
||||
family_commands = self._filter_new_history_commands(
|
||||
family_commands,
|
||||
record.family_details,
|
||||
self._family_signature_from_command,
|
||||
self._family_signature_from_record,
|
||||
)
|
||||
|
||||
if education_commands:
|
||||
vals["education_history"] = education_commands
|
||||
if employer_commands:
|
||||
vals["employer_history"] = employer_commands
|
||||
if family_commands:
|
||||
vals["family_details"] = family_commands
|
||||
return vals
|
||||
|
||||
def _normalize_resume_list(self, value):
|
||||
if isinstance(value, list):
|
||||
return [item for item in value if isinstance(item, dict)]
|
||||
if isinstance(value, dict):
|
||||
return [value]
|
||||
return []
|
||||
|
||||
def _prepare_education_history_commands(self, education_history):
|
||||
commands = []
|
||||
for education in self._normalize_resume_list(education_history):
|
||||
specialization = self._clean_text(
|
||||
education.get("specialization")
|
||||
or education.get("name")
|
||||
or education.get("degree")
|
||||
)
|
||||
university = self._clean_text(
|
||||
education.get("university")
|
||||
or education.get("institution")
|
||||
or education.get("college")
|
||||
)
|
||||
start_year = self._normalize_year(education.get("start_year"))
|
||||
end_year = self._normalize_year(education.get("end_year"))
|
||||
education_type = self._map_education_type(
|
||||
education.get("education_type")
|
||||
or education.get("degree")
|
||||
or specialization
|
||||
)
|
||||
marks_or_grade = self._clean_text(
|
||||
education.get("marks_or_grade")
|
||||
or education.get("marks")
|
||||
or education.get("grade")
|
||||
or education.get("cgpa")
|
||||
or education.get("percentage")
|
||||
) or "N/A"
|
||||
|
||||
if not all([education_type, specialization, university, start_year, end_year]):
|
||||
continue
|
||||
|
||||
commands.append((0, 0, {
|
||||
"education_type": education_type,
|
||||
"name": specialization,
|
||||
"university": university,
|
||||
"start_year": start_year,
|
||||
"end_year": end_year,
|
||||
"marks_or_grade": marks_or_grade,
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_employer_history_commands(self, employer_history):
|
||||
commands = []
|
||||
for employer in self._normalize_resume_list(employer_history):
|
||||
company_name = self._clean_text(employer.get("company_name") or employer.get("employer"))
|
||||
designation = self._clean_text(employer.get("designation") or employer.get("role") or employer.get("job_title"))
|
||||
date_of_joining = self._normalize_history_date(employer.get("date_of_joining") or employer.get("start_date"))
|
||||
last_working_day = self._normalize_history_date(
|
||||
employer.get("last_working_day") or employer.get("end_date"),
|
||||
prefer_month_end=True,
|
||||
)
|
||||
ctc = self._clean_text(employer.get("ctc") or employer.get("salary"))
|
||||
summary_html = self._build_employer_summary_html(
|
||||
employer.get("work_description")
|
||||
or employer.get("summary")
|
||||
or employer.get("description")
|
||||
or employer.get("responsibilities")
|
||||
)
|
||||
|
||||
if not all([company_name, designation, date_of_joining]):
|
||||
continue
|
||||
|
||||
commands.append((0, 0, {
|
||||
"company_name": company_name,
|
||||
"designation": designation,
|
||||
"date_of_joining": date_of_joining,
|
||||
"last_working_day": last_working_day,
|
||||
"ctc": ctc,
|
||||
"summary": summary_html,
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _prepare_family_details_commands(self, family_details):
|
||||
commands = []
|
||||
used_relations = set()
|
||||
for member in self._normalize_resume_list(family_details):
|
||||
relation_type = self._map_family_relation(member.get("relation_type") or member.get("relation"), used_relations)
|
||||
name = self._clean_text(member.get("name"))
|
||||
contact_no = self._clean_text(member.get("contact_no") or member.get("contact") or member.get("phone"))
|
||||
dob = self._normalize_history_date(member.get("dob"))
|
||||
location = self._clean_text(member.get("location") or member.get("address"))
|
||||
|
||||
if not relation_type or not name:
|
||||
continue
|
||||
|
||||
used_relations.add(relation_type)
|
||||
commands.append((0, 0, {
|
||||
"relation_type": relation_type,
|
||||
"name": name,
|
||||
"contact_no": contact_no,
|
||||
"dob": dob,
|
||||
"location": location,
|
||||
}))
|
||||
return commands
|
||||
|
||||
def _clean_text(self, value):
|
||||
if value in (False, None):
|
||||
return False
|
||||
cleaned = re.sub(r"\s+", " ", str(value)).strip(" -,:;\n\t")
|
||||
return cleaned or False
|
||||
|
||||
def _build_employer_summary_html(self, value):
|
||||
if value in (False, None, "", []):
|
||||
return False
|
||||
|
||||
if isinstance(value, list):
|
||||
items = [self._clean_text(item) for item in value]
|
||||
items = [item for item in items if item]
|
||||
else:
|
||||
raw_text = str(value).strip()
|
||||
if not raw_text:
|
||||
return False
|
||||
normalized_text = raw_text.replace("\r", "\n")
|
||||
bullet_like = re.split(r"(?:\n+|[•\u2022]|(?<=\.)\s+(?=[A-Z]))", normalized_text)
|
||||
items = [self._clean_text(item) for item in bullet_like]
|
||||
items = [item for item in items if item]
|
||||
if len(items) <= 1:
|
||||
items = [self._clean_text(part) for part in re.split(r"[;|]", normalized_text)]
|
||||
items = [item for item in items if item]
|
||||
|
||||
if not items:
|
||||
return False
|
||||
|
||||
if len(items) == 1:
|
||||
return f"<div><p>{escape(items[0])}</p></div>"
|
||||
|
||||
bullet_items = "".join(f"<li>{escape(item)}</li>" for item in items)
|
||||
return f"<div><p>Key responsibilities and contributions:</p><ul>{bullet_items}</ul></div>"
|
||||
|
||||
def _filter_new_history_commands(self, commands, existing_records, command_signature_getter, record_signature_getter):
|
||||
if not commands:
|
||||
return []
|
||||
existing_signatures = {
|
||||
signature for signature in (record_signature_getter(record) for record in existing_records) if signature
|
||||
}
|
||||
filtered_commands = []
|
||||
for command in commands:
|
||||
signature = command_signature_getter(command)
|
||||
if not signature or signature in existing_signatures:
|
||||
continue
|
||||
existing_signatures.add(signature)
|
||||
filtered_commands.append(command)
|
||||
return filtered_commands
|
||||
|
||||
def _education_signature_from_command(self, command):
|
||||
vals = command[2] if len(command) > 2 else {}
|
||||
return (
|
||||
vals.get("education_type"),
|
||||
(vals.get("name") or "").strip().lower(),
|
||||
(vals.get("university") or "").strip().lower(),
|
||||
vals.get("start_year"),
|
||||
vals.get("end_year"),
|
||||
)
|
||||
|
||||
def _education_signature_from_record(self, record):
|
||||
return (
|
||||
record.education_type,
|
||||
(record.name or "").strip().lower(),
|
||||
(record.university or "").strip().lower(),
|
||||
record.start_year,
|
||||
record.end_year,
|
||||
)
|
||||
|
||||
def _employer_signature_from_command(self, command):
|
||||
vals = command[2] if len(command) > 2 else {}
|
||||
return (
|
||||
(vals.get("company_name") or "").strip().lower(),
|
||||
(vals.get("designation") or "").strip().lower(),
|
||||
vals.get("date_of_joining"),
|
||||
vals.get("last_working_day"),
|
||||
)
|
||||
|
||||
def _employer_signature_from_record(self, record):
|
||||
return (
|
||||
(record.company_name or "").strip().lower(),
|
||||
(record.designation or "").strip().lower(),
|
||||
record.date_of_joining,
|
||||
record.last_working_day,
|
||||
)
|
||||
|
||||
def _family_signature_from_command(self, command):
|
||||
vals = command[2] if len(command) > 2 else {}
|
||||
return (
|
||||
vals.get("relation_type"),
|
||||
(vals.get("name") or "").strip().lower(),
|
||||
vals.get("dob"),
|
||||
)
|
||||
|
||||
def _family_signature_from_record(self, record):
|
||||
return (
|
||||
record.relation_type,
|
||||
(record.name or "").strip().lower(),
|
||||
record.dob,
|
||||
)
|
||||
|
||||
def _normalize_year(self, value):
|
||||
if value in (False, None, ""):
|
||||
return False
|
||||
if isinstance(value, int):
|
||||
return value if 1900 <= value <= 2100 else False
|
||||
match = re.search(r"\b(19|20)\d{2}\b", str(value))
|
||||
return int(match.group(0)) if match else False
|
||||
|
||||
def _map_education_type(self, value):
|
||||
normalized = (self._clean_text(value) or "").lower()
|
||||
if not normalized:
|
||||
return False
|
||||
mapping = {
|
||||
"10": "10",
|
||||
"10th": "10",
|
||||
"ssc": "10",
|
||||
"secondary": "10",
|
||||
"inter": "inter",
|
||||
"12": "inter",
|
||||
"12th": "inter",
|
||||
"intermediate": "inter",
|
||||
"hsc": "inter",
|
||||
"higher secondary": "inter",
|
||||
"puc": "inter",
|
||||
"graduation": "graduation",
|
||||
"graduate": "graduation",
|
||||
"bachelor": "graduation",
|
||||
"bachelors": "graduation",
|
||||
"post_graduation": "post_graduation",
|
||||
"post graduation": "post_graduation",
|
||||
"postgraduate": "post_graduation",
|
||||
"post graduate": "post_graduation",
|
||||
"master": "post_graduation",
|
||||
"masters": "post_graduation",
|
||||
"additional": "additional",
|
||||
"additional qualification": "additional",
|
||||
"diploma": "additional",
|
||||
"certification": "additional",
|
||||
}
|
||||
if normalized in mapping:
|
||||
return mapping[normalized]
|
||||
if any(token in normalized for token in ["master", "mba", "m.tech", "mtech", "mca", "m.sc", "msc", "pgdm"]):
|
||||
return "post_graduation"
|
||||
if any(token in normalized for token in ["bachelor", "b.tech", "btech", "b.e", "be ", "bca", "b.sc", "bsc", "b.com", "bcom", "ba", "bba"]):
|
||||
return "graduation"
|
||||
if any(token in normalized for token in ["12", "inter", "intermediate", "hsc", "higher secondary", "puc"]):
|
||||
return "inter"
|
||||
if any(token in normalized for token in ["10", "10th", "ssc", "secondary"]):
|
||||
return "10"
|
||||
return "additional"
|
||||
|
||||
def _map_family_relation(self, value, used_relations=None):
|
||||
normalized = (self._clean_text(value) or "").lower()
|
||||
used_relations = used_relations or set()
|
||||
if not normalized:
|
||||
return False
|
||||
if normalized in {"father", "dad"}:
|
||||
return "father"
|
||||
if normalized in {"mother", "mom"}:
|
||||
return "mother"
|
||||
if normalized in {"spouse", "wife", "husband"}:
|
||||
return "spouse"
|
||||
if normalized in {"kid1", "child1"}:
|
||||
return "kid1"
|
||||
if normalized in {"kid2", "child2"}:
|
||||
return "kid2"
|
||||
if normalized in {"son", "daughter", "child", "children", "kid"}:
|
||||
if "kid1" not in used_relations:
|
||||
return "kid1"
|
||||
if "kid2" not in used_relations:
|
||||
return "kid2"
|
||||
return False
|
||||
|
||||
def _normalize_history_date(self, value, prefer_month_end=False):
|
||||
cleaned = self._clean_text(value)
|
||||
if not cleaned:
|
||||
return False
|
||||
for fmt in ("%Y-%m-%d", "%d-%m-%Y", "%d/%m/%Y", "%Y/%m/%d"):
|
||||
try:
|
||||
return datetime.strptime(cleaned, fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
month_year_patterns = ("%Y-%m", "%m-%Y", "%m/%Y", "%b %Y", "%B %Y")
|
||||
for fmt in month_year_patterns:
|
||||
try:
|
||||
parsed = datetime.strptime(cleaned, fmt)
|
||||
last_day = monthrange(parsed.year, parsed.month)[1] if prefer_month_end else 1
|
||||
return parsed.replace(day=last_day).date()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
year = self._normalize_year(cleaned)
|
||||
if year:
|
||||
month = 12 if prefer_month_end else 1
|
||||
day = 31 if prefer_month_end else 1
|
||||
return datetime(year, month, day).date()
|
||||
return False
|
||||
|
||||
def _sync_candidate_skills(self, candidate, skills):
|
||||
if not skills:
|
||||
return
|
||||
|
|
@ -827,7 +1209,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
if self._is_valid_resume_skill(value, text):
|
||||
result.append(value)
|
||||
return self._deduplicate_skill_names(result)[:25]
|
||||
|
||||
|
||||
|
||||
def _detect_resume_domain(self, text):
|
||||
text = text.lower()
|
||||
|
|
@ -839,7 +1221,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
return "it"
|
||||
|
||||
return "general"
|
||||
|
||||
|
||||
def _resolve_resume_skill(self, skill_name,
|
||||
extracted_text):
|
||||
normalized = self._normalize_skill_name(skill_name)
|
||||
|
|
|
|||
|
|
@ -41,26 +41,30 @@
|
|||
'views/recruitment_attachments.xml',
|
||||
'views/hr_employee_education_employer_family.xml',
|
||||
'views/hr_recruitment_source.xml',
|
||||
'views/requisitions.xml',
|
||||
'views/skills.xml',
|
||||
'views/recruitment_matching_views.xml',
|
||||
'wizards/post_onboarding_attachment_wizard.xml',
|
||||
'wizards/applicant_refuse_reason.xml',
|
||||
'wizards/ats_invite_mail_template_wizard.xml',
|
||||
'wizards/client_submission_mail_template_wizard.xml',
|
||||
# 'views/resume_pearser.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_recruitment_extended/static/src/img/pdf_icon.png',
|
||||
'hr_recruitment_extended/static/src/js/recruitment_match_panel.js',
|
||||
'hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
'views/requisitions.xml',
|
||||
'views/skills.xml',
|
||||
'views/recruitment_matching_views.xml',
|
||||
'wizards/post_onboarding_attachment_wizard.xml',
|
||||
'wizards/applicant_refuse_reason.xml',
|
||||
'wizards/ats_invite_mail_template_wizard.xml',
|
||||
'wizards/client_submission_mail_template_wizard.xml',
|
||||
'wizards/applicant_stage_comment_wizard.xml',
|
||||
# 'views/resume_pearser.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_recruitment_extended/static/src/img/pdf_icon.png',
|
||||
'hr_recruitment_extended/static/src/js/recruitment_match_panel.js',
|
||||
# 'hr_recruitment_extended/static/src/js/stage_comment_statusbar.js',
|
||||
# 'hr_recruitment_extended/static/src/xml/stage_comment_statusbar.xml',
|
||||
'hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss',
|
||||
'hr_recruitment_extended/static/src/scss/hr_applicant_hold.scss',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +1,58 @@
|
|||
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from operator import itemgetter
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.addons.website_hr_recruitment.controllers.main import WebsiteHrRecruitment
|
||||
from odoo.osv.expression import AND
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools import email_normalize
|
||||
from odoo.tools.misc import groupby
|
||||
import base64
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
|
||||
class website_hr_recruitment_applications(http.Controller):
|
||||
|
||||
@http.route(['/hr_recruitment/second_application_form/<int:applicant_id>'], type='http', auth="public", website=True)
|
||||
def second_application_form(self, applicant_id, **kwargs):
|
||||
def _get_request_form(self, applicant_id, applicant_request_id, token=None, form_type=None, require_token=False):
|
||||
applicant_request = request.env['applicant.request.forms'].sudo().browse(applicant_request_id)
|
||||
if not applicant_request.exists():
|
||||
return request.env['applicant.request.forms']
|
||||
if applicant_request.applicant_id.id != applicant_id:
|
||||
return request.env['applicant.request.forms']
|
||||
if form_type and applicant_request.form_type != form_type:
|
||||
return request.env['applicant.request.forms']
|
||||
if require_token and not token:
|
||||
return request.env['applicant.request.forms']
|
||||
if token and applicant_request.access_token != token:
|
||||
return request.env['applicant.request.forms']
|
||||
return applicant_request
|
||||
|
||||
@staticmethod
|
||||
def _is_submitted_for_token(applicant_request, token):
|
||||
return bool(
|
||||
applicant_request
|
||||
and applicant_request.status == 'done'
|
||||
and token
|
||||
and applicant_request.submitted_token == token
|
||||
)
|
||||
|
||||
@http.route(['/hr_recruitment/second_application_form/<int:applicant_id>/<int:applicant_request_id>'], type='http', auth="public", website=True)
|
||||
def second_application_form(self, applicant_id, applicant_request_id, **kwargs):
|
||||
"""Renders the website form for applicants to submit additional details."""
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
applicant_request_id = request.env['applicant.request.forms'].sudo().browse(applicant_request_id)
|
||||
if not applicant_request_id.exists():
|
||||
return request.not_found()
|
||||
if applicant and applicant.send_second_application_form:
|
||||
if applicant.second_application_form_status == 'done':
|
||||
if applicant_request_id :
|
||||
if applicant_request_id.status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
else:
|
||||
return request.render("hr_recruitment_extended.applicant_form_template", {
|
||||
'applicant': applicant
|
||||
'applicant': applicant_request_id.applicant_id,
|
||||
'applicant_request_id': applicant_request_id
|
||||
})
|
||||
else:
|
||||
return request.not_found()
|
||||
|
||||
|
||||
|
||||
@http.route(['/hr_recruitment/submit_second_application/<int:applicant_id>/submit'], type='http', auth="public",
|
||||
@http.route(['/hr_recruitment/submit_second_application/<int:applicant_id>/<int:applicant_request_id>/submit'], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
def process_application_form(self, applicant_id, **kwargs):
|
||||
def process_application_form(self, applicant_id, applicant_request_id, **kwargs):
|
||||
# Get the applicant
|
||||
candidate_image_base64 = kwargs.pop('candidate_image_base64')
|
||||
candidate_image = kwargs.pop('candidate_image')
|
||||
|
|
@ -62,10 +71,11 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
kwargs['total_exp_type'] = 'year'
|
||||
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
applicant_request_id = request.env['applicant.request.forms'].sudo().browse(applicant_request_id)
|
||||
if not applicant_request_id.exists() or not applicant.exists():
|
||||
return request.not_found() # Return 404 if applicant doesn't exist
|
||||
|
||||
if applicant.second_application_form_status == 'done':
|
||||
if applicant_request_id.status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
|
||||
|
|
@ -78,65 +88,68 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
if template and applicant.user_id.email:
|
||||
template.sudo().send_mail(applicant.id, force_send=True)
|
||||
|
||||
applicant.second_application_form_status = 'done'
|
||||
applicant_request_id.status = 'done'
|
||||
# Redirect to a Thank You page
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
|
||||
|
||||
@http.route(['/FTPROTECH/DocRequests/<int:applicant_id>'], type='http', auth="public",
|
||||
@http.route(['/FTPROTECH/DocRequests/<int:applicant_id>/<int:applicant_request_id>'], type='http', auth="public",
|
||||
website=True)
|
||||
def doc_request_form(self, applicant_id, **kwargs):
|
||||
"""Renders the website form for applicants to submit additional details."""
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found()
|
||||
if applicant:
|
||||
if applicant.doc_requests_form_status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
else:
|
||||
return request.render("hr_recruitment_extended.doc_request_form_template", {
|
||||
'applicant': applicant
|
||||
})
|
||||
else:
|
||||
return request.not_found()
|
||||
def doc_request_form(self, applicant_id, applicant_request_id, **kwargs):
|
||||
request_token = kwargs.get('token')
|
||||
applicant_request = self._get_request_form(
|
||||
applicant_id,
|
||||
applicant_request_id,
|
||||
token=request_token,
|
||||
form_type='documents_request',
|
||||
require_token=True,
|
||||
)
|
||||
if not applicant_request:
|
||||
raise request.not_found()
|
||||
if self._is_submitted_for_token(applicant_request, request_token):
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
@http.route(['/FTPROTECH/submit/<int:applicant_id>/docRequest'], type='http', auth="public",
|
||||
return request.render("hr_recruitment_extended.doc_request_form_template", {
|
||||
'applicant': applicant_request.applicant_id,
|
||||
'applicant_request_id': applicant_request,
|
||||
'request_token': request_token,
|
||||
})
|
||||
|
||||
@http.route(['/FTPROTECH/submit/<int:applicant_id>/<int:applicant_request_id>/docRequest'], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
def process_applicant_doc_submission_form(self, applicant_id, **post):
|
||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found() # Return 404 if applicant doesn't exist
|
||||
def process_applicant_doc_submission_form(self, applicant_id, applicant_request_id, **post):
|
||||
request_token = post.get('request_token')
|
||||
applicant_request = self._get_request_form(
|
||||
applicant_id,
|
||||
applicant_request_id,
|
||||
token=request_token,
|
||||
form_type='documents_request',
|
||||
require_token=True,
|
||||
)
|
||||
if not applicant_request:
|
||||
raise request.not_found()
|
||||
|
||||
if applicant.doc_requests_form_status == 'done':
|
||||
applicant = applicant_request.applicant_id
|
||||
if self._is_submitted_for_token(applicant_request, request_token):
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
applicant_data = {
|
||||
'applicant_id': int(post.get('applicant_id', 0)),
|
||||
'candidate_image': post.get('candidate_image_base64', ''),
|
||||
'doc_requests_form_status': 'done'
|
||||
}
|
||||
|
||||
applicant_data = {k: v for k, v in applicant_data.items() if v != '' and v != 0}
|
||||
# attachments
|
||||
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
||||
|
||||
if attachments_data:
|
||||
applicant_data['joining_attachment_ids'] = [
|
||||
(4, existing_id) for existing_id in
|
||||
(applicant.joining_attachment_ids).ids
|
||||
] + [
|
||||
(0, 0, {
|
||||
'name': attachment.get('file_name', ''),
|
||||
'recruitment_attachment_id': attachment.get(
|
||||
'attachment_rec_id', ''),
|
||||
'file': attachment.get('file_content', '')
|
||||
}) for attachment in attachments_data if
|
||||
attachment.get('attachment_rec_id')
|
||||
]
|
||||
|
||||
applicant.write(applicant_data)
|
||||
applicant.replace_joining_attachments(attachments_data)
|
||||
applicant_request.write({
|
||||
'status': 'done',
|
||||
'applicant_submitted_date': datetime.now(),
|
||||
'submitted_token': request_token,
|
||||
})
|
||||
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
|
||||
|
|
@ -150,7 +163,7 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||
if not applicant.exists():
|
||||
return request.not_found()
|
||||
if applicant and applicant.send_post_onboarding_form:
|
||||
if applicant:
|
||||
if applicant.post_onboarding_form_status == 'done':
|
||||
return request.render("hr_recruitment_extended.thank_you_template")
|
||||
else:
|
||||
|
|
@ -219,7 +232,6 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
|
||||
applicant_data = {k: v for k, v in applicant_data.items() if v != '' and v != 0}
|
||||
|
||||
# Get family details data from JSON
|
||||
family_data_json = post.get('family_data_json', '[]')
|
||||
family_data = json.loads(family_data_json) if family_data_json else []
|
||||
|
||||
|
|
@ -231,9 +243,8 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
'contact_no': member.get('contact', ''),
|
||||
'dob': datetime.strptime(member.get('dob'), '%Y-%m-%d').date() if member.get('dob') else None,
|
||||
'location': member.get('location', ''),
|
||||
}) for member in family_data if member.get('name') and member.get('relation') # Optional filter to avoid empty members
|
||||
}) for member in family_data if member.get('name') and member.get('relation')
|
||||
]
|
||||
# education details
|
||||
education_data_json = post.get('education_data_json', '[]')
|
||||
education_data = json.loads(education_data_json) if education_data_json else []
|
||||
if education_data:
|
||||
|
|
@ -262,28 +273,13 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
}) for company in employer_data
|
||||
]
|
||||
|
||||
#attachments
|
||||
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
||||
|
||||
if attachments_data:
|
||||
applicant_data['joining_attachment_ids'] = [
|
||||
(4, existing_id) for existing_id in
|
||||
(applicant.joining_attachment_ids).ids
|
||||
] + [
|
||||
(0, 0, {
|
||||
'name': attachment.get('file_name', ''),
|
||||
'recruitment_attachment_id': attachment.get(
|
||||
'attachment_rec_id', ''),
|
||||
'file': attachment.get('file_content', '')
|
||||
}) for attachment in attachments_data if
|
||||
attachment.get('attachment_rec_id')
|
||||
]
|
||||
|
||||
applicant.write(applicant_data)
|
||||
applicant.replace_joining_attachments(attachments_data)
|
||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
||||
raise_if_not_found=False)
|
||||
# Get HR managers with HR department
|
||||
group = request.env.ref('hr.group_hr_manager')
|
||||
users = request.env['res.users'].sudo().search([
|
||||
('groups_id', 'in', group.ids),
|
||||
|
|
@ -292,27 +288,20 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
('employee_id.department_id.name', '=', 'Human Resource')
|
||||
])
|
||||
|
||||
# Extract emails and join them into a comma-separated string
|
||||
email_cc = ','.join([user.email for user in users])
|
||||
|
||||
# Prepare email values
|
||||
email_values = {
|
||||
'email_from': applicant.email_from,
|
||||
'email_to': 'hr@ftprotech.com',
|
||||
'email_cc': email_cc
|
||||
}
|
||||
|
||||
# Debug: Print the email_cc value to verify
|
||||
print(f"Email CC value: {email_values['email_cc']}")
|
||||
|
||||
# Send email
|
||||
template.sudo().send_mail(
|
||||
applicant.id,
|
||||
email_values=email_values,
|
||||
force_send=True
|
||||
)
|
||||
|
||||
# Render thank you page
|
||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
||||
'applicant': applicant
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- employee.recruitment.attachments-->
|
||||
<record model="ir.attachment" id="employee_recruitment_attachments_preview">
|
||||
<field name="name">Attachment Preview</field>
|
||||
<field name="type">binary</field>
|
||||
|
|
@ -44,4 +43,4 @@
|
|||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -88,9 +88,10 @@
|
|||
Request to employees to provide salary expectations, experience, and current offers.
|
||||
</field>
|
||||
<field name="body_html" type="html">
|
||||
<t t-set="request_form_id" t-value="ctx.get('applicant_request_form_id')"/>
|
||||
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
|
||||
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
|
||||
<t t-set="form_url" t-value="base_url + '/hr_recruitment/second_application_form/%s' % object.id"/>
|
||||
<t t-set="form_url" t-value="base_url + '/hr_recruitment/second_application_form/%s/%s' % (object.id, request_form_id)"/>
|
||||
|
||||
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
|
||||
<p>Dear
|
||||
|
|
@ -143,7 +144,7 @@
|
|||
<field name="name">Applicant Form Submission Notification</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.user_id.email }}</field> <!-- Recruiter's Email -->
|
||||
<field name="email_to">{{ object.user_id.email }}</field>
|
||||
<field name="subject">New Submission: Applicant Salary & Experience Form</field>
|
||||
<field name="description">
|
||||
Notification sent to recruiter when an applicant submits the form.
|
||||
|
|
@ -202,7 +203,7 @@
|
|||
</field>
|
||||
<field name="body_html" type="html">
|
||||
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
|
||||
|
||||
<t t-set="request_form_id" t-value="ctx.get('applicant_request_form_id')"/>
|
||||
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
|
||||
<p>Dear
|
||||
<strong>
|
||||
|
|
@ -218,7 +219,6 @@
|
|||
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||
<p>Please ensure to provide soft copies of the required documents:</p>
|
||||
|
||||
<!-- Personal Documents -->
|
||||
<t t-if="ctx.get('personal_docs')">
|
||||
<strong>Personal Documents:</strong>
|
||||
<ul>
|
||||
|
|
@ -228,7 +228,6 @@
|
|||
</ul>
|
||||
</t>
|
||||
|
||||
<!-- Education Documents -->
|
||||
<t t-if="ctx.get('education_docs')">
|
||||
<strong>Education Documents:</strong>
|
||||
<ul>
|
||||
|
|
@ -238,7 +237,6 @@
|
|||
</ul>
|
||||
</t>
|
||||
|
||||
<!-- Previous Employer Documents -->
|
||||
<t t-if="ctx.get('previous_employer_docs')">
|
||||
<strong>Previous Employer Documents:</strong>
|
||||
<ul>
|
||||
|
|
@ -248,7 +246,6 @@
|
|||
</ul>
|
||||
</t>
|
||||
|
||||
<!-- Additional Documents -->
|
||||
<t t-if="ctx.get('other_docs')">
|
||||
<strong>Additional Documents:</strong>
|
||||
<ul>
|
||||
|
|
@ -261,8 +258,8 @@
|
|||
|
||||
<p>Please upload your documents via the following link:</p>
|
||||
|
||||
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
|
||||
<t t-set="upload_url" t-value="base_url + '/FTPROTECH/DocRequests/%s' % object.id"/>
|
||||
<t t-set="upload_url" t-value="ctx.get('applicant_request_form_url')"/>
|
||||
<t t-esc="upload_url"/>
|
||||
|
||||
<p style="text-align: center; margin-top: 20px;">
|
||||
<a t-att-href="upload_url" target="_blank"
|
||||
|
|
@ -635,31 +632,219 @@
|
|||
</record>
|
||||
|
||||
<record id="application_client_submission_email_template" model="mail.template">
|
||||
<field name="name">Applicant Client Submissions</field>
|
||||
<field name="name">Applicant Share Template</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.hr_job_recruitment.requested_by.email }}</field>
|
||||
<field name="subject">Applicant Submission</field>
|
||||
<field name="subject">{{ object.candidate_id.partner_name or object.partner_name or object.display_name }} -
|
||||
{{ object.hr_job_recruitment.job_id.name or object.job_id.name }}
|
||||
</field>
|
||||
<field name="description">
|
||||
Submitting the Applicant Details to Client.
|
||||
Share applicant and job requisition details by email.
|
||||
</field>
|
||||
<field name="body_html" type="html">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <t t-esc="ctx['client_name']">Sir/Madam</t>,
|
||||
<br/>
|
||||
<br/>
|
||||
Submitting new applicant.
|
||||
<br/>
|
||||
Kindly review the Applicant.
|
||||
<br/>
|
||||
<br/>
|
||||
Regards,
|
||||
<br/>
|
||||
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
|
||||
</p>
|
||||
<t t-set="applicant_name"
|
||||
t-value="object.candidate_id.partner_name or object.partner_name or object.display_name"/>
|
||||
<t t-set="job_name">
|
||||
<t t-if="object.hr_job_recruitment.job_id">
|
||||
<t t-set="job_name" t-value="object.hr_job_recruitment.job_id.display_name"/>
|
||||
</t>
|
||||
<t t-elif="object.job_id">
|
||||
<t t-set="job_name" t-value="object.job_id.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="job_name" t-value="''"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="recruitment_name"
|
||||
t-value="object.hr_job_recruitment.recruitment_sequence or object.hr_job_recruitment.display_name or ''"/>
|
||||
<t t-set="requested_by">
|
||||
<t t-if="object.hr_job_recruitment.requested_by">
|
||||
<t t-set="requested_by" t-value="object.hr_job_recruitment.requested_by.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="requested_by" t-value="''"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set="locations">
|
||||
<t t-if="object.hr_job_recruitment.locations">
|
||||
<t t-set="locations_str"
|
||||
t-value="', '.join(object.hr_job_recruitment.locations.mapped('name'))"/>
|
||||
<t t-set="locations" t-value="locations_str"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="locations" t-value="''"/>
|
||||
</t>
|
||||
</t>
|
||||
<div style="margin: 0; padding: 0; font-size: 13px; line-height: 1.6;">
|
||||
<p>Dear Sir/Madam,</p>
|
||||
<p>Please find the applicant details below for your review.</p>
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Applicant</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="applicant_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Email</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="object.email_from or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Phone</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="object.partner_phone or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Current Organization</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="object.current_organization or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Total Experience</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="object.total_exp or ''"/>
|
||||
<t t-esc="object.total_exp_type or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Relevant Experience</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="object.relevant_exp or ''"/>
|
||||
<t t-esc="object.relevant_exp_type or ''"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Job Requisition</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="recruitment_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Job Position</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="job_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Requested By</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="requested_by"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<strong>Location</strong>
|
||||
</td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;">
|
||||
<t t-esc="locations"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>The applicant resume is attached for reference. Please review and let us know your feedback.</p>
|
||||
<p>Regards,
|
||||
<br/>
|
||||
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="job_recruitment_share_email_template" model="mail.template">
|
||||
<field name="name">Job Recruitment Share Template</field>
|
||||
<field name="model_id" ref="hr_recruitment_extended.model_hr_job_recruitment"/>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.requested_by.email or object.address_id.email or '' }}</field>
|
||||
<field name="subject">{{ object.job_id.name or object.display_name }} - Job Description</field>
|
||||
<field name="description">
|
||||
Share job description and recruitment details by email.
|
||||
</field>
|
||||
<field name="body_html" type="html">
|
||||
<t t-set="job_name" t-value="object.job_id.name or object.name or object.display_name or ''"/>
|
||||
<t t-set="recruitment_name" t-value="object.recruitment_sequence or object.display_name or ''"/>
|
||||
<t t-set="locations" t-value="', '.join(object.locations.mapped('name')) if object.locations else ''"/>
|
||||
<t t-set="primary_skills" t-value="', '.join(object.skill_ids.mapped('name')) if object.skill_ids else ''"/>
|
||||
<t t-set="secondary_skills" t-value="', '.join(object.secondary_skill_ids.mapped('name')) if object.secondary_skill_ids else ''"/>
|
||||
<div style="margin: 0; padding: 0; font-size: 13px; line-height: 1.6;">
|
||||
<p>Dear Sir/Madam,</p>
|
||||
<p>Please find the job description and hiring details below for your review and sourcing support.</p>
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Job Requisition</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="recruitment_name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Job Position</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="job_name"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Recruitment Type</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.recruitment_type or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Requested By</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.requested_by.name or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Primary Recruiter</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.user_id.name or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Open Positions</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.no_of_recruitment or 0"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Experience</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.experience.display_name or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Budget</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="object.budget or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Locations</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="locations"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Primary Skills</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="primary_skills"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><strong>Secondary Skills</strong></td>
|
||||
<td style="padding: 8px; border: 1px solid #d9d9d9;"><t t-esc="secondary_skills"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div>
|
||||
<strong>Job Description</strong>
|
||||
<div t-out="object.description or ''"/>
|
||||
</div>
|
||||
<p>Please review and share suitable profiles at the earliest convenience.</p>
|
||||
<p>Regards,<br/><t t-out="user.name or 'Hiring Manager'">Hiring Manager</t></p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import hr_recruitment
|
||||
from . import hr_job_recruitment
|
||||
from . import stages
|
||||
from . import hr_applicant
|
||||
from . import applicant_request_forms
|
||||
from . import hr_applicant_stage_comment
|
||||
from . import hr_applicant
|
||||
from . import hr_job
|
||||
from . import res_partner
|
||||
from . import candidate_experience
|
||||
from . import hr_employee_education_employer_family
|
||||
# from . import resume_pearser
|
||||
from . import recruitment_attachments
|
||||
from . import hr_recruitment_source
|
||||
from . import requisitions
|
||||
from . import skills
|
||||
from . import skills
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import uuid
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ApplicantRequestForms(models.Model):
|
||||
_name = 'applicant.request.forms'
|
||||
_description = 'Applicant Request Forms'
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
'hr.applicant',
|
||||
string='Applicant',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
form_type = fields.Selection([
|
||||
('experience_request_form', 'Experience Request Form'),
|
||||
('documents_request', 'Documents Request'),
|
||||
], string='Form Type', required=True)
|
||||
|
||||
status = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('email_sent_to_candidate', 'Email Sent to Candidate'),
|
||||
('done', 'Done'),
|
||||
], string='Status', default='draft')
|
||||
|
||||
send_date = fields.Datetime(string='Sent Date')
|
||||
applicant_submitted_date = fields.Datetime(string='Applicant Submitted Date')
|
||||
access_token = fields.Char(string='Access Token', copy=False, readonly=True)
|
||||
submitted_token = fields.Char(string='Submitted Token', copy=False, readonly=True)
|
||||
|
||||
def _generate_access_token(self):
|
||||
self.ensure_one()
|
||||
return uuid.uuid4().hex
|
||||
|
||||
def _issue_new_access_token(self):
|
||||
self.ensure_one()
|
||||
token = self._generate_access_token()
|
||||
self.write({
|
||||
'access_token': token,
|
||||
'submitted_token': False,
|
||||
'status': 'email_sent_to_candidate',
|
||||
'send_date': fields.Datetime.now(),
|
||||
'applicant_submitted_date': False,
|
||||
})
|
||||
return token
|
||||
|
||||
def action_send_form(self):
|
||||
self.ensure_one()
|
||||
|
||||
applicant = self.applicant_id
|
||||
|
||||
if self.form_type == 'experience_request_form':
|
||||
template = self.env.ref(
|
||||
'hr_recruitment_extended.email_template_second_application_form',
|
||||
raise_if_not_found=False
|
||||
)
|
||||
|
||||
if template and applicant.email_from:
|
||||
template.with_context(
|
||||
applicant_request_form_id=self.id
|
||||
).send_mail(applicant.id, force_send=True)
|
||||
|
||||
self.write({
|
||||
'status': 'email_sent_to_candidate',
|
||||
'send_date': fields.Datetime.now()
|
||||
})
|
||||
|
||||
elif self.form_type == 'documents_request':
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Select Attachments',
|
||||
'res_model': 'post.onboarding.attachment.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_req_attachment_ids': [],
|
||||
'default_is_pre_onboarding_attachment_request': True,
|
||||
'default_request_form_id': self.id,
|
||||
'applicant_id': self.applicant_id.id,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +1,141 @@
|
|||
from email.policy import default
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
import warnings
|
||||
from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
|
||||
|
||||
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
_track_duration_field = 'recruitment_stage_id'
|
||||
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
primary_skill_match_percentage = fields.Float(
|
||||
string="Primary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
secondary_skill_match_percentage = fields.Float(
|
||||
string="Secondary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
overall_skill_match_percentage = fields.Float(
|
||||
string="Overall Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
|
||||
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, readonly=True, tracking=True)
|
||||
client_submission_date = fields.Datetime(string="Submission Date")
|
||||
submitted_stage = fields.Many2one('hr.recruitment.stage')
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
_track_duration_field = 'recruitment_stage_id'
|
||||
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
primary_skill_match_percentage = fields.Float(
|
||||
string="Primary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
secondary_skill_match_percentage = fields.Float(
|
||||
string="Secondary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
overall_skill_match_percentage = fields.Float(
|
||||
string="Overall Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
|
||||
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)
|
||||
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')
|
||||
|
||||
@api.depends('is_on_hold')
|
||||
def _compute_hold_state(self):
|
||||
for applicant in self:
|
||||
applicant.hold_state = 'hold' if applicant.is_on_hold else False
|
||||
|
||||
def hold_unhold_button(self):
|
||||
for rec in self:
|
||||
if rec.is_on_hold:
|
||||
rec.is_on_hold = False
|
||||
else:
|
||||
rec.is_on_hold = True
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def hold_unhold_button(self):
|
||||
for rec in self:
|
||||
if rec.is_on_hold:
|
||||
rec.is_on_hold = False
|
||||
else:
|
||||
rec.is_on_hold = True
|
||||
|
||||
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('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
|
||||
def _compute_skill_match_percentages(self):
|
||||
@api.depends('stage_comment_ids')
|
||||
def _compute_stage_comment_count(self):
|
||||
for applicant in self:
|
||||
percentages = {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
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()
|
||||
}
|
||||
if applicant.hr_job_recruitment and applicant.candidate_id:
|
||||
percentages = applicant.hr_job_recruitment._get_skill_match_percentages(applicant.candidate_id.skill_ids)
|
||||
percentages = {
|
||||
key: value for key, value in percentages.items()
|
||||
if key in {'primary_skill_match_percentage', 'secondary_skill_match_percentage', 'overall_skill_match_percentage'}
|
||||
}
|
||||
applicant.update(percentages)
|
||||
|
||||
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):
|
||||
for applicant in self:
|
||||
percentages = {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
}
|
||||
if applicant.hr_job_recruitment and applicant.candidate_id:
|
||||
percentages = applicant.hr_job_recruitment._get_skill_match_percentages(applicant.candidate_id.skill_ids)
|
||||
percentages = {
|
||||
key: value for key, value in percentages.items()
|
||||
if key in {'primary_skill_match_percentage', 'secondary_skill_match_percentage', 'overall_skill_match_percentage'}
|
||||
}
|
||||
applicant.update(percentages)
|
||||
@api.constrains('candidate_id','hr_job_recruitment')
|
||||
def hr_applicant_constrains(self):
|
||||
for rec in self:
|
||||
|
|
@ -82,13 +150,38 @@ class HRApplicant(models.Model):
|
|||
search_domain = []
|
||||
if job_recruitment_id:
|
||||
search_domain = [('job_recruitment_ids', '=', job_recruitment_id)] + search_domain
|
||||
# if stages:
|
||||
# search_domain = [('id', 'in', stages.ids)] + search_domain
|
||||
|
||||
stage_ids = stages.sudo()._search(search_domain, order=stages._order)
|
||||
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:
|
||||
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:
|
||||
blocked_records = self.filtered(
|
||||
lambda applicant: applicant.is_on_hold and applicant.stage_id.id != vals.get('stage_id')
|
||||
)
|
||||
if blocked_records:
|
||||
raise ValidationError(_("You cannot change the stage of an applicant while it is on hold. Please unhold it first."))
|
||||
# user_id change: update date_open
|
||||
res = super().write(vals)
|
||||
if vals.get('user_id'):
|
||||
|
|
@ -150,16 +243,15 @@ class HRApplicant(models.Model):
|
|||
copy=False, index=True,
|
||||
group_expand='_read_group_recruitment_stage_ids')
|
||||
stage_color = fields.Char(related="recruitment_stage_id.stage_color")
|
||||
|
||||
send_second_application_form = fields.Boolean(related='recruitment_stage_id.second_application_form')
|
||||
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
|
||||
request_form_ids = fields.One2many(
|
||||
'applicant.request.forms',
|
||||
'applicant_id',
|
||||
string='Request Forms'
|
||||
)
|
||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||
doc_requests_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
||||
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
||||
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
||||
# holding_offer = fields.HTML()
|
||||
employee_code = fields.Char(related="employee_id.employee_id")
|
||||
|
||||
recruitment_attachments = fields.Many2many(
|
||||
|
|
@ -193,37 +285,50 @@ class HRApplicant(models.Model):
|
|||
|
||||
def preview_resume(self):
|
||||
pass
|
||||
# for record in self:
|
||||
# if record.resume:
|
||||
# attachment = self.env.ref("hr_recruitment_extended.employee_recruitment_attachments_preview")
|
||||
# attachment.datas = record.resume
|
||||
# return {
|
||||
# 'name': "File Preview",
|
||||
# 'type': 'ir.actions.act_url',
|
||||
# 'url': f'/web/content/{attachment.id}?download=false',
|
||||
# 'target': 'current', # Opens in a new tab
|
||||
# }
|
||||
|
||||
def submit_to_client(self):
|
||||
for rec in self:
|
||||
submitted_count = len(self.sudo().search([('id','!=',rec.id),('submitted_to_client','=',True)]).ids)
|
||||
if submitted_count >= rec.hr_job_recruitment.no_of_eligible_submissions:
|
||||
warnings.warn(
|
||||
"Max no of submissions for this JD has been reached",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Submission',
|
||||
'res_model': 'client.submission.mails.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {'default_template_id': self.env.ref(
|
||||
"hr_recruitment_extended.application_client_submission_email_template").id,
|
||||
},
|
||||
def replace_joining_attachments(self, attachments_data):
|
||||
self.ensure_one()
|
||||
|
||||
latest_attachments = {}
|
||||
for attachment in attachments_data or []:
|
||||
attachment_id = attachment.get('attachment_rec_id')
|
||||
file_content = attachment.get('file_content')
|
||||
if not attachment_id or not file_content:
|
||||
continue
|
||||
latest_attachments[int(attachment_id)] = {
|
||||
'name': attachment.get('file_name', ''),
|
||||
'recruitment_attachment_id': int(attachment_id),
|
||||
'file': file_content,
|
||||
}
|
||||
|
||||
if not latest_attachments:
|
||||
return
|
||||
|
||||
existing_lines = self.joining_attachment_ids.filtered(
|
||||
lambda line: line.recruitment_attachment_id.id in latest_attachments
|
||||
)
|
||||
if existing_lines:
|
||||
existing_lines.unlink()
|
||||
|
||||
self.write({
|
||||
'joining_attachment_ids': [(0, 0, values) for values in latest_attachments.values()]
|
||||
})
|
||||
|
||||
def action_share_applicant(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Share Applicant'),
|
||||
'res_model': 'client.submission.mails.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_template_id': self.env.ref(
|
||||
'hr_recruitment_extended.application_client_submission_email_template'
|
||||
).id,
|
||||
},
|
||||
}
|
||||
|
||||
def submit_for_approval(self):
|
||||
for rec in self:
|
||||
|
|
@ -231,7 +336,6 @@ class HRApplicant(models.Model):
|
|||
if not manager_id:
|
||||
raise ValidationError(_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
||||
mail_template = self.env.ref('hr_recruitment_extended.email_template_candidate_approval')
|
||||
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
|
||||
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
||||
render_ctx = dict(recruitment_manager=manager_id)
|
||||
mail_template.with_context(render_ctx).send_mail(
|
||||
|
|
@ -247,7 +351,6 @@ class HRApplicant(models.Model):
|
|||
raise ValidationError(
|
||||
_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
||||
mail_template = self.env.ref('hr_recruitment_extended.email_template_stage_approved')
|
||||
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
|
||||
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
||||
render_ctx = dict(recruitment_manager=manager_id)
|
||||
mail_template.with_context(render_ctx).send_mail(
|
||||
|
|
@ -280,7 +383,7 @@ class HRApplicant(models.Model):
|
|||
template.send_mail(applicant.id, force_send=True)
|
||||
applicant.second_application_form_status = 'email_sent_to_candidate'
|
||||
|
||||
def send_post_onboarding_form_to_candidate(self):
|
||||
def send_jod_form_to_employee(self):
|
||||
for rec in self:
|
||||
if not rec.employee_id:
|
||||
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrApplicantStageComment(models.Model):
|
||||
_name = 'hr.applicant.stage.comment'
|
||||
_description = 'Applicant Stage Comment'
|
||||
_order = 'create_date desc, id desc'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', required=True, ondelete='cascade', index=True)
|
||||
stage_id = fields.Many2one('hr.recruitment.stage', required=True, index=True)
|
||||
comment = fields.Text(required=True)
|
||||
user_id = fields.Many2one('res.users', default=lambda self: self.env.user, required=True)
|
||||
comment_date = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||
|
||||
@api.depends('applicant_id', 'stage_id')
|
||||
def _compute_display_name(self):
|
||||
for comment in self:
|
||||
comment.display_name = '%s - %s' % (
|
||||
comment.applicant_id.display_name or '',
|
||||
comment.stage_id.display_name or '',
|
||||
)
|
||||
|
|
@ -1,34 +1,36 @@
|
|||
from odoo import models, fields, api, _
|
||||
from datetime import date
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
import re
|
||||
import unicodedata
|
||||
from odoo import models, fields, api, _
|
||||
from datetime import date
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
class HRJobRecruitment(models.Model):
|
||||
class HRJobRecruitment(models.Model):
|
||||
_name = 'hr.job.recruitment'
|
||||
_description = 'Recruitment'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_inherits = {'hr.job': 'job_id'}
|
||||
_rec_name = 'recruitment_sequence'
|
||||
_order = 'id desc'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_recruitment_sequence', 'UNIQUE(recruitment_sequence)', 'Recruitment sequence must be unique!')
|
||||
]
|
||||
_SKILL_ALIAS_GROUPS = {
|
||||
'python': {'python', 'python3', 'py'},
|
||||
'postgresql': {'postgresql', 'postgres', 'postgre', 'pgsql', 'psql', 'pgadmin'},
|
||||
'javascript': {'javascript', 'js', 'nodejs', 'node'},
|
||||
'typescript': {'typescript', 'ts'},
|
||||
'react': {'react', 'reactjs'},
|
||||
'vue': {'vue', 'vuejs'},
|
||||
'angular': {'angular', 'angularjs'},
|
||||
}
|
||||
active = fields.Boolean(default=True)
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_recruitment_sequence', 'UNIQUE(recruitment_sequence)', 'Recruitment sequence must be unique!')
|
||||
]
|
||||
_SKILL_ALIAS_GROUPS = {
|
||||
'python': {'python', 'python3', 'py'},
|
||||
'postgresql': {'postgresql', 'postgres', 'postgre', 'pgsql', 'psql', 'pgadmin'},
|
||||
'javascript': {'javascript', 'js', 'nodejs', 'node'},
|
||||
'typescript': {'typescript', 'ts'},
|
||||
'react': {'react', 'reactjs'},
|
||||
'vue': {'vue', 'vuejs'},
|
||||
'angular': {'angular', 'angularjs'},
|
||||
}
|
||||
|
||||
def _get_first_stage(self):
|
||||
"""This function is used to fetch the starting stage"""
|
||||
|
|
@ -148,7 +150,6 @@ class HRJobRecruitment(models.Model):
|
|||
target_from = fields.Date(string="This is the date in which we starting the recruitment process",
|
||||
default=fields.Date.today, tracking=True)
|
||||
target_to = fields.Date(string='This is the target end date', tracking=True)
|
||||
# hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
|
||||
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite',store=True, tracking=True)
|
||||
department_id = fields.Many2one('hr.department', string='Department', check_company=True, tracking=True)
|
||||
description = fields.Html(string='Job Description', sanitize_attributes=False)
|
||||
|
|
@ -159,238 +160,253 @@ class HRJobRecruitment(models.Model):
|
|||
help='Number of employees currently occupying this job position.')
|
||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company, tracking=True, exportable=False)
|
||||
contract_type_id = fields.Many2one('hr.contract.type', string='Employment Type', tracking=True)
|
||||
# active = fields.Boolean(default=True)
|
||||
user_id = fields.Many2one('res.users', "Recruiter",
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True, help="The Recruiter will be the default value for all Applicants in this job \
|
||||
position. The Recruiter is automatically added to all meetings with the Applicant.")
|
||||
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True, help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
|
||||
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
||||
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
||||
address_id = fields.Many2one(
|
||||
'res.partner', "Job Location", default=_default_address_id,
|
||||
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
|
||||
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False, tracking=True)
|
||||
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal', tracking=True)
|
||||
requested_by = fields.Many2one('res.partner', string="Requested By",
|
||||
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]", tracking=True)
|
||||
|
||||
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'}
|
||||
|
||||
def _normalize_skill_name(self, skill_name):
|
||||
normalized_name = unicodedata.normalize('NFKD', skill_name or '')
|
||||
normalized_name = normalized_name.encode('ascii', 'ignore').decode('ascii').lower()
|
||||
normalized_name = re.sub(r'[^a-z0-9]+', '', normalized_name)
|
||||
if not normalized_name:
|
||||
return ''
|
||||
for canonical_name, aliases in self._SKILL_ALIAS_GROUPS.items():
|
||||
if normalized_name in aliases or any(alias in normalized_name for alias in aliases if len(alias) > 3):
|
||||
return canonical_name
|
||||
return normalized_name
|
||||
|
||||
def _get_normalized_skill_name_map(self, skill_names):
|
||||
normalized_map = {}
|
||||
for skill_name in skill_names:
|
||||
normalized_name = self._normalize_skill_name(skill_name)
|
||||
if normalized_name and normalized_name not in normalized_map:
|
||||
normalized_map[normalized_name] = skill_name
|
||||
return normalized_map
|
||||
|
||||
def _get_skill_match_percentages_from_names(self, primary_skill_names, secondary_skill_names, candidate_skill_names):
|
||||
self.ensure_one()
|
||||
candidate_skill_map = self._get_normalized_skill_name_map(candidate_skill_names)
|
||||
primary_skill_map = self._get_normalized_skill_name_map(primary_skill_names)
|
||||
secondary_skill_map = self._get_normalized_skill_name_map(secondary_skill_names)
|
||||
all_skill_map = {**primary_skill_map, **secondary_skill_map}
|
||||
|
||||
def _percentage(required_skill_map):
|
||||
if not required_skill_map:
|
||||
return 0.0
|
||||
return round(
|
||||
(len(set(required_skill_map) & set(candidate_skill_map)) / len(required_skill_map)) * 100,
|
||||
2,
|
||||
)
|
||||
|
||||
matching_skill_keys = set(all_skill_map) & set(candidate_skill_map)
|
||||
missing_skill_keys = set(all_skill_map) - set(candidate_skill_map)
|
||||
|
||||
return {
|
||||
'primary_skill_match_percentage': _percentage(primary_skill_map),
|
||||
'secondary_skill_match_percentage': _percentage(secondary_skill_map),
|
||||
'overall_skill_match_percentage': _percentage(all_skill_map),
|
||||
'matching_skill_names': [all_skill_map[key] for key in matching_skill_keys],
|
||||
'missing_skill_names': [all_skill_map[key] for key in missing_skill_keys],
|
||||
}
|
||||
|
||||
def _get_skill_match_percentages(self, candidate_skills, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
candidate_skill_names = candidate_skills.mapped('name')
|
||||
return self._get_skill_match_percentages_from_names(
|
||||
primary_skill_names,
|
||||
secondary_skill_names,
|
||||
candidate_skill_names,
|
||||
)
|
||||
|
||||
def _prepare_candidate_pool_match_payload(self, candidate, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
)
|
||||
return {
|
||||
'candidate_id': candidate.id,
|
||||
'candidate_name': candidate.partner_name or candidate.display_name,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False),
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def _prepare_applicant_match_payload(self, applicant, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
candidate = applicant.candidate_id
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
) if candidate else {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
'matching_skill_names': [],
|
||||
'missing_skill_names': [],
|
||||
}
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'applicant_name': applicant.partner_name or applicant.display_name,
|
||||
'candidate_id': candidate.id if candidate else False,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False) if candidate else False,
|
||||
'email_from': applicant.email_from or (candidate.email_from if candidate else False),
|
||||
'partner_phone': applicant.partner_phone or (candidate.partner_phone if candidate else False),
|
||||
'recruitment_stage_name': applicant.recruitment_stage_id.display_name,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def get_candidate_pool_matches_data(self, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
existing_candidate_ids = self.application_ids.candidate_id.ids
|
||||
candidates = self.env['hr.candidate'].search([
|
||||
('id', 'not in', existing_candidate_ids),
|
||||
])
|
||||
|
||||
ranked_candidates = []
|
||||
for candidate in candidates:
|
||||
ranked_candidates.append(
|
||||
self._prepare_candidate_pool_match_payload(candidate, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_applicants = []
|
||||
for applicant in self.application_ids.sorted(lambda rec: (
|
||||
-(rec.overall_skill_match_percentage or 0.0),
|
||||
-(rec.primary_skill_match_percentage or 0.0),
|
||||
-(rec.secondary_skill_match_percentage or 0.0),
|
||||
rec.partner_name or rec.display_name or '',
|
||||
)):
|
||||
ranked_applicants.append(
|
||||
self._prepare_applicant_match_payload(applicant, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_candidates.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['candidate_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
ranked_applicants.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['applicant_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'job_recruitment_id': self.id,
|
||||
'job_recruitment_name': self.display_name,
|
||||
'primary_skill_names': list(self._get_normalized_skill_name_map(primary_skill_names).values()),
|
||||
'secondary_skill_names': list(self._get_normalized_skill_name_map(secondary_skill_names).values()),
|
||||
'candidate_count': len(ranked_candidates),
|
||||
'applicant_count': len(ranked_applicants),
|
||||
'candidates': ranked_candidates,
|
||||
'applicants': ranked_applicants,
|
||||
}
|
||||
|
||||
def action_add_candidate_to_recruitment(self, candidate_id):
|
||||
self.ensure_one()
|
||||
candidate = self.env['hr.candidate'].browse(candidate_id).exists()
|
||||
if not candidate:
|
||||
raise ValidationError(_("The selected candidate no longer exists."))
|
||||
|
||||
existing_applicant = self.application_ids.filtered(lambda applicant: applicant.candidate_id == candidate)[:1]
|
||||
if existing_applicant:
|
||||
return {
|
||||
'applicant_id': existing_applicant.id,
|
||||
'already_exists': True,
|
||||
}
|
||||
|
||||
applicant_vals = {
|
||||
'candidate_id': candidate.id,
|
||||
'partner_name': candidate.partner_name or candidate.display_name,
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'hr_job_recruitment': self.id,
|
||||
'user_id': self.user_id.id,
|
||||
'company_id': candidate.company_id.id or self.company_id.id,
|
||||
}
|
||||
applicant = self.env['hr.applicant'].create(applicant_vals)
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'already_exists': False,
|
||||
}
|
||||
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal', tracking=True)
|
||||
requested_by = fields.Many2one('res.partner', string="Requested By",
|
||||
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]", tracking=True)
|
||||
|
||||
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'}
|
||||
|
||||
def _normalize_skill_name(self, skill_name):
|
||||
normalized_name = unicodedata.normalize('NFKD', skill_name or '')
|
||||
normalized_name = normalized_name.encode('ascii', 'ignore').decode('ascii').lower()
|
||||
normalized_name = re.sub(r'[^a-z0-9]+', '', normalized_name)
|
||||
if not normalized_name:
|
||||
return ''
|
||||
for canonical_name, aliases in self._SKILL_ALIAS_GROUPS.items():
|
||||
if normalized_name in aliases or any(alias in normalized_name for alias in aliases if len(alias) > 3):
|
||||
return canonical_name
|
||||
return normalized_name
|
||||
|
||||
def _get_normalized_skill_name_map(self, skill_names):
|
||||
normalized_map = {}
|
||||
for skill_name in skill_names:
|
||||
normalized_name = self._normalize_skill_name(skill_name)
|
||||
if normalized_name and normalized_name not in normalized_map:
|
||||
normalized_map[normalized_name] = skill_name
|
||||
return normalized_map
|
||||
|
||||
def _get_skill_match_percentages_from_names(self, primary_skill_names, secondary_skill_names, candidate_skill_names):
|
||||
self.ensure_one()
|
||||
candidate_skill_map = self._get_normalized_skill_name_map(candidate_skill_names)
|
||||
primary_skill_map = self._get_normalized_skill_name_map(primary_skill_names)
|
||||
secondary_skill_map = self._get_normalized_skill_name_map(secondary_skill_names)
|
||||
all_skill_map = {**primary_skill_map, **secondary_skill_map}
|
||||
|
||||
def _percentage(required_skill_map):
|
||||
if not required_skill_map:
|
||||
return 0.0
|
||||
return round(
|
||||
(len(set(required_skill_map) & set(candidate_skill_map)) / len(required_skill_map)) * 100,
|
||||
2,
|
||||
)
|
||||
|
||||
matching_skill_keys = set(all_skill_map) & set(candidate_skill_map)
|
||||
missing_skill_keys = set(all_skill_map) - set(candidate_skill_map)
|
||||
|
||||
return {
|
||||
'primary_skill_match_percentage': _percentage(primary_skill_map),
|
||||
'secondary_skill_match_percentage': _percentage(secondary_skill_map),
|
||||
'overall_skill_match_percentage': _percentage(all_skill_map),
|
||||
'matching_skill_names': [all_skill_map[key] for key in matching_skill_keys],
|
||||
'missing_skill_names': [all_skill_map[key] for key in missing_skill_keys],
|
||||
}
|
||||
|
||||
def _get_skill_match_percentages(self, candidate_skills, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
candidate_skill_names = candidate_skills.mapped('name')
|
||||
return self._get_skill_match_percentages_from_names(
|
||||
primary_skill_names,
|
||||
secondary_skill_names,
|
||||
candidate_skill_names,
|
||||
)
|
||||
|
||||
def _prepare_candidate_pool_match_payload(self, candidate, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
)
|
||||
return {
|
||||
'candidate_id': candidate.id,
|
||||
'candidate_name': candidate.partner_name or candidate.display_name,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False),
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def _prepare_applicant_match_payload(self, applicant, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
candidate = applicant.candidate_id
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
) if candidate else {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
'matching_skill_names': [],
|
||||
'missing_skill_names': [],
|
||||
}
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'applicant_name': applicant.partner_name or applicant.display_name,
|
||||
'candidate_id': candidate.id if candidate else False,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False) if candidate else False,
|
||||
'email_from': applicant.email_from or (candidate.email_from if candidate else False),
|
||||
'partner_phone': applicant.partner_phone or (candidate.partner_phone if candidate else False),
|
||||
'recruitment_stage_name': applicant.recruitment_stage_id.display_name,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def get_candidate_pool_matches_data(self, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
existing_candidate_ids = self.application_ids.candidate_id.ids
|
||||
candidates = self.env['hr.candidate'].search([
|
||||
('id', 'not in', existing_candidate_ids),
|
||||
])
|
||||
|
||||
ranked_candidates = []
|
||||
for candidate in candidates:
|
||||
ranked_candidates.append(
|
||||
self._prepare_candidate_pool_match_payload(candidate, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_applicants = []
|
||||
for applicant in self.application_ids.sorted(lambda rec: (
|
||||
-(rec.overall_skill_match_percentage or 0.0),
|
||||
-(rec.primary_skill_match_percentage or 0.0),
|
||||
-(rec.secondary_skill_match_percentage or 0.0),
|
||||
rec.partner_name or rec.display_name or '',
|
||||
)):
|
||||
ranked_applicants.append(
|
||||
self._prepare_applicant_match_payload(applicant, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_candidates.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['candidate_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
ranked_applicants.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['applicant_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'job_recruitment_id': self.id,
|
||||
'job_recruitment_name': self.display_name,
|
||||
'primary_skill_names': list(self._get_normalized_skill_name_map(primary_skill_names).values()),
|
||||
'secondary_skill_names': list(self._get_normalized_skill_name_map(secondary_skill_names).values()),
|
||||
'candidate_count': len(ranked_candidates),
|
||||
'applicant_count': len(ranked_applicants),
|
||||
'candidates': ranked_candidates,
|
||||
'applicants': ranked_applicants,
|
||||
}
|
||||
|
||||
def action_add_candidate_to_recruitment(self, candidate_id):
|
||||
self.ensure_one()
|
||||
candidate = self.env['hr.candidate'].browse(candidate_id).exists()
|
||||
if not candidate:
|
||||
raise ValidationError(_("The selected candidate no longer exists."))
|
||||
|
||||
existing_applicant = self.application_ids.filtered(lambda applicant: applicant.candidate_id == candidate)[:1]
|
||||
if existing_applicant:
|
||||
return {
|
||||
'applicant_id': existing_applicant.id,
|
||||
'already_exists': True,
|
||||
}
|
||||
|
||||
applicant_vals = {
|
||||
'candidate_id': candidate.id,
|
||||
'partner_name': candidate.partner_name or candidate.display_name,
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'hr_job_recruitment': self.id,
|
||||
'user_id': self.user_id.id,
|
||||
'company_id': candidate.company_id.id or self.company_id.id,
|
||||
}
|
||||
applicant = self.env['hr.applicant'].create(applicant_vals)
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'already_exists': False,
|
||||
}
|
||||
|
||||
@api.onchange('recruitment_type')
|
||||
def _onchange_recruitment_type(self):
|
||||
self.requested_by = False
|
||||
self.address_id = False
|
||||
|
||||
def send_mail_to_recruiters(self):
|
||||
for rec in self:
|
||||
users = (rec.interviewer_ids | rec.user_id).filtered(lambda user: user.email)
|
||||
if not users:
|
||||
raise ValidationError(_("Please add at least one recruiter with an email address before sending notifications."))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Send Email',
|
||||
'res_model': 'ats.invite.mail.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_ats_invite_mail_template_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_partner_ids': [(6, 0, users.ids)],
|
||||
'default_template_id': self.env.ref("hr_recruitment_extended.email_template_recruiter_assignment_template").id,
|
||||
},
|
||||
}
|
||||
def send_mail_to_recruiters(self):
|
||||
for rec in self:
|
||||
users = (rec.interviewer_ids | rec.user_id).filtered(lambda user: user.email)
|
||||
if not users:
|
||||
raise ValidationError(_("Please add at least one recruiter with an email address before sending notifications."))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Send Email',
|
||||
'res_model': 'ats.invite.mail.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_ats_invite_mail_template_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_partner_ids': [(6, 0, users.ids)],
|
||||
'default_template_id': self.env.ref("hr_recruitment_extended.email_template_recruiter_assignment_template").id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_share_job_recruitment(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Share Job Description'),
|
||||
'res_model': 'client.submission.mails.template.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_template_id': self.env.ref(
|
||||
'hr_recruitment_extended.job_recruitment_share_email_template'
|
||||
).id,
|
||||
},
|
||||
}
|
||||
|
||||
@api.onchange('requested_by')
|
||||
def _onchange_requested_by(self):
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ import datetime
|
|||
# hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
|
||||
|
||||
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'),
|
||||
]
|
||||
#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)
|
||||
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)
|
||||
|
||||
first_name = fields.Char(string='First Name',required=False, help="This is the person's first name, given at birth or during a naming ceremony. It’s the name people use to address you.")
|
||||
middle_name = fields.Char(string='Middle Name', help="This is an extra name that comes between the first name and last name. Not everyone has a middle name")
|
||||
|
|
@ -31,13 +31,13 @@ class HrCandidate(models.Model):
|
|||
resume_type = fields.Char()
|
||||
resume_name = fields.Char()
|
||||
|
||||
applications_stages_stat = fields.Many2many('application.stage.status',string="Applications History", compute="_compute_applications_stages_stat")
|
||||
# availability_status = fields.Selection([('available','Available'),('not_available','Not Available'),('hired','Hired'),('abscond','Abscond')])
|
||||
|
||||
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'}
|
||||
applications_stages_stat = fields.Many2many('application.stage.status',string="Applications History", compute="_compute_applications_stages_stat")
|
||||
# availability_status = fields.Selection([('available','Available'),('not_available','Not Available'),('hired','Hired'),('abscond','Abscond')])
|
||||
|
||||
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.onchange('resume')
|
||||
|
|
@ -161,71 +161,6 @@ class HrCandidate(models.Model):
|
|||
employee.write({
|
||||
'image_1920': self.candidate_image})
|
||||
return action
|
||||
#
|
||||
# doj = fields.Date(tracking=True)
|
||||
# gender = fields.Selection([
|
||||
# ('male', 'Male'),
|
||||
# ('female', 'Female'),
|
||||
# ('other', 'Other')
|
||||
# ], tracking=True)
|
||||
# birthday = fields.Date(tracking=True)
|
||||
#
|
||||
# blood_group = fields.Selection([
|
||||
# ('A+', 'A+'),
|
||||
# ('A-', 'A-'),
|
||||
# ('B+', 'B+'),
|
||||
# ('B-', 'B-'),
|
||||
# ('O+', 'O+'),
|
||||
# ('O-', 'O-'),
|
||||
# ('AB+', 'AB+'),
|
||||
# ('AB-', 'AB-'),
|
||||
# ], string="Blood Group")
|
||||
#
|
||||
# private_street = fields.Char(string="Private Street", groups="hr.group_hr_user")
|
||||
# private_street2 = fields.Char(string="Private Street2", groups="hr.group_hr_user")
|
||||
# private_city = fields.Char(string="Private City", groups="hr.group_hr_user")
|
||||
# private_state_id = fields.Many2one(
|
||||
# "res.country.state", string="Private State",
|
||||
# domain="[('country_id', '=?', private_country_id)]",
|
||||
# groups="hr.group_hr_user")
|
||||
# private_zip = fields.Char(string="Private Zip", groups="hr.group_hr_user")
|
||||
# private_country_id = fields.Many2one("res.country", string="Private Country", groups="hr.group_hr_user")
|
||||
#
|
||||
# permanent_street = fields.Char(string="permanent Street", groups="hr.group_hr_user")
|
||||
# permanent_street2 = fields.Char(string="permanent Street2", groups="hr.group_hr_user")
|
||||
# permanent_city = fields.Char(string="permanent City", groups="hr.group_hr_user")
|
||||
# permanent_state_id = fields.Many2one(
|
||||
# "res.country.state", string="permanent State",
|
||||
# domain="[('country_id', '=?', private_country_id)]",
|
||||
# groups="hr.group_hr_user")
|
||||
# permanent_zip = fields.Char(string="permanent Zip", groups="hr.group_hr_user")
|
||||
# permanent_country_id = fields.Many2one("res.country", string="permanent Country", groups="hr.group_hr_user")
|
||||
#
|
||||
# marital = fields.Selection(
|
||||
# selection='_get_marital_status_selection',
|
||||
# string='Marital Status',
|
||||
# groups="hr.group_hr_user",
|
||||
# default='single',
|
||||
# required=True,
|
||||
# tracking=True)
|
||||
#
|
||||
# marriage_anniversary_date = fields.Date(string='Anniversary Date' ,tracking=True)
|
||||
#
|
||||
# #bank Details:
|
||||
#
|
||||
# full_name_as_in_bank = fields.Char(string='Full Name (as per bank)' ,tracking=True)
|
||||
# bank_name = fields.Char(string='Bank Name' ,tracking=True)
|
||||
# bank_branch = fields.Char(string='Bank Branch' ,tracking=True)
|
||||
# bank_account_no = fields.Char(string='Bank Account N0' ,tracking=True)
|
||||
# bank_ifsc_code = fields.Char(string='Bank IFSC Code' ,tracking=True)
|
||||
#
|
||||
#
|
||||
# #passport details:
|
||||
# passport_no = fields.Char(string="Passport No",tracking=True)
|
||||
# passport_start_date = fields.Date(string="Start Date",tracking=True)
|
||||
# passport_end_date = fields.Date(string="End Date",tracking=True)
|
||||
# passport_issued_location = fields.Char(string="Start Date",tracking=True)
|
||||
#
|
||||
# #authotentication Details
|
||||
# pan_no = fields.Char(string='PAN No',tracking=True)
|
||||
# identification_id = fields.Char(string='Aadhar No',tracking=True)
|
||||
|
|
@ -505,29 +440,6 @@ class Location(models.Model):
|
|||
if record.zip_code and not record.zip_code.isdigit(): # Check if zip_code exists and is not digit
|
||||
raise ValidationError("Zip Code should contain only numeric characters. Please enter a valid zip code.")
|
||||
|
||||
#
|
||||
# class RecruitmentHistory(models.Model):
|
||||
# _name='recruitment.status.history'
|
||||
#
|
||||
# date_from = fields.Date(string='Date From')
|
||||
# date_end = fields.Date(string='Date End')
|
||||
# target = fields.Integer(string='Target')
|
||||
# job_id = fields.Many2one('hr.job', string='Job Position') # Ensure this field exists
|
||||
# hired = fields.Many2many('hr.applicant')
|
||||
#
|
||||
# @api.depends('date_from', 'date_end', 'job_id')
|
||||
# def _total_hired_users(self):
|
||||
# for rec in self:
|
||||
# if rec.date_from:
|
||||
# # Use `date_end` or today's date if `date_end` is not provided
|
||||
# date_end = rec.date_end or date.today()
|
||||
#
|
||||
# # Search for applicants matching the conditions
|
||||
# hired_applicants = self.env['hr.applicant'].search([
|
||||
# ('date_closed', '>=', rec.date_from),
|
||||
# ('date_closed', '<=', date_end),
|
||||
# ('job_id', '=', rec.job_id.id)
|
||||
# ])
|
||||
# rec.hired = hired_applicants
|
||||
# else:
|
||||
# rec.hired = False
|
||||
|
|
@ -592,4 +504,4 @@ class ApplicationsStageStatus(models.Model):
|
|||
WHERE
|
||||
a.active = 't' or a.active = 'f'
|
||||
);
|
||||
""" % (self._table))
|
||||
""" % (self._table))
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ class RecruitmentStage(models.Model):
|
|||
job_recruitment_ids = fields.Many2many(
|
||||
'hr.job.recruitment', string='Job Specific',
|
||||
help='Specific jobs that use this stage. Other jobs will not use this stage.')
|
||||
second_application_form = fields.Boolean(default=False)
|
||||
post_onboarding_form = fields.Boolean(default=False)
|
||||
# second_application_form = fields.Boolean(default=False)
|
||||
# post_onboarding_form = fields.Boolean(default=False)
|
||||
require_approval = fields.Boolean(default=False)
|
||||
stage_color = fields.Char('Stage Color', default='#FFFFFF', help="Choose a color for the recruitment stage", widget='color')
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,13 @@ hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recru
|
|||
hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0
|
||||
access_hr_recruitment_stage_hr,hr.recruitment.stage.hr,hr_recruitment.model_hr_recruitment_stage,hr.group_hr_manager,1,0,0,0
|
||||
|
||||
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
|
||||
access_hr_applicant_stage_comment_user,hr.applicant.stage.comment.user,model_hr_applicant_stage_comment,base.group_user,1,1,1,0
|
||||
|
||||
|
||||
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
|
||||
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
|
||||
access_applicant_stage_comment_wizard,applicant.stage.comment.wizard.user,model_applicant_stage_comment_wizard,base.group_user,1,1,1,1
|
||||
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
|
||||
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||
access_applicant_request_forms_hr_user,access.applicant.request.forms.hr.user,model_applicant_request_forms,hr.group_hr_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -38,21 +38,6 @@
|
|||
<value eval=" {'noupdate': True} "/>
|
||||
</function>
|
||||
|
||||
|
||||
<!-- <record id="hr_job_recruitment_rule" model="ir.rule">-->
|
||||
<!-- <field name="name">Applicant Interviewer</field>-->
|
||||
<!-- <field name="model_id" ref="model_hr_applicant"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- '|',-->
|
||||
<!-- ('job_id.interviewer_ids', 'in', user.id),-->
|
||||
<!-- ('interviewer_ids', 'in', user.id),-->
|
||||
<!-- ]</field>-->
|
||||
<!-- <field name="perm_create" eval="False"/>-->
|
||||
<!-- <field name="perm_unlink" eval="False"/>-->
|
||||
<!-- <field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
|
||||
<record id="hr_recruitment.hr_applicant_interviewer_rule" model="ir.rule">
|
||||
<field name="name">Applicant Interviewer</field>
|
||||
<field name="domain_force">[
|
||||
|
|
@ -189,4 +174,4 @@
|
|||
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -26,16 +26,14 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
|||
addUploadedFileRow(attachmentId, file, base64String) {
|
||||
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||
|
||||
if (!this.uploadedFiles[attachmentId]) {
|
||||
this.uploadedFiles[attachmentId] = [];
|
||||
}
|
||||
this.uploadedFiles[attachmentId] = [];
|
||||
tableBody.empty();
|
||||
|
||||
// Generate a unique file ID using attachmentId and a timestamp
|
||||
const fileId = `${attachmentId}-${Date.now()}`;
|
||||
|
||||
const fileRecord = {
|
||||
attachment_rec_id : attachmentId,
|
||||
id: fileId, // Unique file ID
|
||||
attachment_rec_id: parseInt(attachmentId, 10),
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
base64: base64String,
|
||||
type: file.type,
|
||||
|
|
@ -43,36 +41,33 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
|||
|
||||
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||
|
||||
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
||||
const previewImageId = `preview_image_${fileId}`;
|
||||
const fileNameInputId = `file_name_input_${fileId}`;
|
||||
const previewWrapperId = `preview_wrapper_${fileId}`;
|
||||
let previewContent = '';
|
||||
let previewClickHandler = '';
|
||||
|
||||
// Check if the file is an image or PDF and set preview content accordingly
|
||||
if (file.type.startsWith('image/')) {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
previewContent = `<div id="${previewWrapperId}" class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||
this.$('#modal_attachment_photo_preview').show();
|
||||
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
|
||||
this.$('#modal_attachment_pdf_preview').hide();
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
} else if (file.type === 'application/pdf') {
|
||||
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
previewContent = `<div id="${previewWrapperId}" class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||
</div>`;
|
||||
previewClickHandler = () => {
|
||||
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||
this.$('#modal_attachment_pdf_preview').show();
|
||||
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
|
||||
this.$('#modal_attachment_photo_preview').hide();
|
||||
this.$('#attachmentPreviewModal').modal('show');
|
||||
};
|
||||
}
|
||||
|
||||
// Append new row to the table with a preview and buttons
|
||||
tableBody.append(`
|
||||
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||
<td>
|
||||
|
|
@ -96,10 +91,7 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
|||
|
||||
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||
|
||||
// Attach click handler for preview (image or PDF)
|
||||
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
||||
|
||||
// Attach click handler for the preview button (to trigger modal)
|
||||
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
|
||||
},
|
||||
|
||||
|
|
@ -108,17 +100,14 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
|||
const attachmentId = $(button).data('attachment-id');
|
||||
const fileId = $(button).data('file-id');
|
||||
|
||||
// Find the index of the file to delete based on unique file ID
|
||||
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
|
||||
this.uploadedFiles[attachmentId].splice(fileIndex, 1);
|
||||
}
|
||||
|
||||
// Remove the row from DOM
|
||||
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||
|
||||
// Hide table if no files left
|
||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||
}
|
||||
|
|
@ -130,15 +119,15 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
|||
const attachmentId = $(input).data('attachment-id');
|
||||
|
||||
if (input.files.length > 0) {
|
||||
Array.from(input.files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
const file = input.files[input.files.length - 1];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
input.value = '';
|
||||
},
|
||||
|
||||
handleUploadNewFile(ev) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
statusBarDurationField,
|
||||
StatusBarDurationField,
|
||||
} from "@mail/views/fields/statusbar_duration/statusbar_duration_field";
|
||||
|
||||
export class RecruitmentStatusBarDurationField extends StatusBarDurationField {
|
||||
getItemTooltip(item) {
|
||||
const tooltip = super.getItemTooltip(item);
|
||||
const comments = this.props.record.data.stage_comment_tooltips || {};
|
||||
const comment = comments[item.value];
|
||||
return comment ? `${tooltip}\n\n${comment}` : tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
export const recruitmentStatusBarDurationField = {
|
||||
...statusBarDurationField,
|
||||
component: RecruitmentStatusBarDurationField,
|
||||
fieldDependencies: [
|
||||
...statusBarDurationField.fieldDependencies,
|
||||
{ name: "stage_comment_tooltips", type: "JSON" },
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("fields").add("recruitment_statusbar_duration", recruitmentStatusBarDurationField);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
.o_kanban_record .o_hr_applicant_kanban_card_hold {
|
||||
background: linear-gradient(180deg, #f5f5f5 0%, #ececec 100%);
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 0 0 1px rgba(108, 117, 125, 0.18);
|
||||
filter: grayscale(0.2);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.o_kanban_record .o_hr_applicant_kanban_card_hold .badge,
|
||||
.o_kanban_record .o_hr_applicant_kanban_card_hold .o_tag {
|
||||
filter: saturate(0.7);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_recruitment_extended.RecruitmentStatusBarDurationField.Dropdown" t-inherit="mail.StatusBarDurationField.Dropdown" t-inherit-mode="extension">
|
||||
<xpath expr="//DropdownItem" position="attributes">
|
||||
<attribute name="attrs">{ title: getItemTooltip(item) }</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_recruitment_extended.RecruitmentStatusBarDurationField" t-inherit="mail.StatusBarDurationField" t-inherit-mode="extension">
|
||||
<xpath expr="//button[@role='radio']" position="attributes">
|
||||
<attribute name="t-att-title">getItemTooltip(item)</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -10,7 +10,6 @@
|
|||
<field name="experience_code" placeholder = "E1" required="1" width="30%"/>
|
||||
<field name="experience_from" required="1" placeholder="0" />
|
||||
<field name="experience_to" required="1" placeholder="2" />
|
||||
<!-- <field name="active"/>-->
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -4,18 +4,28 @@
|
|||
<field name="name">hr.applicant.view.list</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="column_invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="recruitment_stage_id"/>
|
||||
<field name="primary_skill_match_percentage" optional="hide"/>
|
||||
<field name="secondary_skill_match_percentage" optional="hide"/>
|
||||
<field name="overall_skill_match_percentage" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="decoration-muted">hold_state == 'hold'</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='message_needaction']" position="after">
|
||||
<field name="is_on_hold" column_invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="column_invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="recruitment_stage_id"/>
|
||||
<field name="hold_state" widget="badge" decoration-muted="hold_state == 'hold'" optional="show" nolabel="1"/>
|
||||
<field name="submitted_to_client" optional="show"/>
|
||||
<field name="client_submission_date" optional="show"/>
|
||||
<field name="submitted_stage" optional="hide"/>
|
||||
<field name="primary_skill_match_percentage" optional="hide"/>
|
||||
<field name="secondary_skill_match_percentage" optional="hide"/>
|
||||
<field name="overall_skill_match_percentage" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_applicant_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -23,6 +33,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='archive_applicant']" position="attributes">
|
||||
<attribute name="string">Reject</attribute>
|
||||
<attribute name="invisible">not active or is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='archive_applicant']" position="before">
|
||||
<field name="is_on_hold" invisible="1" force_save="1"/>
|
||||
|
|
@ -33,71 +44,77 @@
|
|||
groups="hr_recruitment.group_hr_recruitment_user"
|
||||
invisible="application_status in ['refused'] or not is_on_hold"/>
|
||||
<button string="Submit" name="submit_for_approval" type="object" class="oe_stat_button"
|
||||
invisible="not approval_required or application_submitted" groups="base.group_user"/>
|
||||
invisible="is_on_hold or not approval_required or application_submitted" groups="base.group_user"/>
|
||||
<button string="Approve" name="approve_applicant" type="object" class="oe_stat_button"
|
||||
invisible="not approval_required or not application_submitted"
|
||||
invisible="is_on_hold or not approval_required or not application_submitted"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<button name="submit_to_client" string="Send to Client" type="object" class="btn-primary"
|
||||
<button name="action_share_applicant" string="Share" type="object" class="btn-primary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"
|
||||
invisible="submitted_to_client or application_status in ['refused']"/>
|
||||
invisible="not id"/>
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='create_employee_from_applicant']" position="attributes">
|
||||
<attribute name="invisible">employee_id or not active or not date_closed or is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_create_meeting']" position="attributes">
|
||||
<attribute name="invisible">not id or is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='toggle_active']" position="attributes">
|
||||
<attribute name="invisible">active or is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//sheet" position="attributes">
|
||||
<attribute name="readonly">is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='kanban_state']" position="after">
|
||||
<div class="o_employee_avatar m-0 p-0">
|
||||
<field name="candidate_image" widget="image" class="oe_avatar m-0"
|
||||
options="{"zoom": true, "preview_image":"candidate_image"}"/>
|
||||
|
||||
</div>
|
||||
<widget name="web_ribbon" title="ON HOLD" bg_color="text-bg-secondary" invisible="not is_on_hold"/>
|
||||
<widget name="web_ribbon" title="Awaiting Approval" bg_color="text-bg-warning" invisible="not approval_required or not application_submitted"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='job_id']" position="before">
|
||||
<field name="hr_job_recruitment"/>
|
||||
<field name="approval_required" invisible="1"/>
|
||||
<field name="application_submitted" invisible="1"/>
|
||||
<field name="stage_color" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="employee_id" invisible="1"/>
|
||||
<field name="send_second_application_form"/>
|
||||
<field name="second_application_form_status" readonly="not send_second_application_form"/>
|
||||
|
||||
<field name="send_post_onboarding_form"/>
|
||||
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
||||
<field name="doc_requests_form_status" readonly="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='application_details']/group[1]" position="after">
|
||||
<group string="Skill Matching">
|
||||
<field name="primary_skill_match_percentage" readonly="1"/>
|
||||
<field name="secondary_skill_match_percentage" readonly="1"/>
|
||||
<field name="overall_skill_match_percentage" readonly="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="before">
|
||||
<field name="hr_job_recruitment"/>
|
||||
<field name="approval_required" invisible="1"/>
|
||||
<field name="application_submitted" invisible="1"/>
|
||||
<field name="stage_color" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="employee_id" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_stage_comment_wizard" type="object" class="oe_stat_button" icon="fa-comments">
|
||||
<field name="stage_comment_count" widget="statinfo" string="Stage Notes"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='application_details']/group[1]" position="after">
|
||||
<group string="Client Submission">
|
||||
<field name="submitted_to_client"/>
|
||||
<field name="client_submission_date" readonly="not submitted_to_client"/>
|
||||
<field name="submitted_stage" readonly="not submitted_to_client"/>
|
||||
</group>
|
||||
<group string="Skill Matching">
|
||||
<field name="primary_skill_match_percentage" readonly="1"/>
|
||||
<field name="secondary_skill_match_percentage" readonly="1"/>
|
||||
<field name="overall_skill_match_percentage" readonly="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="before">
|
||||
<field name="stage_comment_tooltips" invisible="1"/>
|
||||
<field name="recruitment_stage_id" widget="statusbar_duration"
|
||||
options="{'clickable': '1', 'fold_field': 'fold'}" invisible="not active and not employee_id"
|
||||
readonly="approval_required or is_on_hold" force_save="1"/>
|
||||
</xpath>
|
||||
<!-- <xpath expr="//form" position="after">-->
|
||||
<!-- </xpath>-->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button string="Send Second Application Form" name="send_second_application_form_to_candidate"
|
||||
type="object" groups="hr.group_hr_user"
|
||||
invisible="not send_second_application_form or second_application_form_status in ['email_sent_to_candidate','done']"/>
|
||||
<button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/>
|
||||
<button string="Request Documents" name="send_pre_onboarding_doc_request_form_to_candidate" type="object"
|
||||
groups="hr.group_hr_user"
|
||||
invisible="doc_requests_form_status in ['email_sent_to_candidate'] or send_post_onboarding_form"/>
|
||||
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<xpath expr="//page[@name='application_details']" position="after">
|
||||
<page name="Attachments" id="attachment_ids_page">
|
||||
<field name="recruitment_attachments" widget="many2many_tags"/>
|
||||
|
||||
|
|
@ -107,7 +124,7 @@
|
|||
name="action_validate_attachments"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="attachments_validation_status == 'pending'"
|
||||
invisible="attachments_validation_status == 'pending' or not employee_id"
|
||||
style="width: 100%;">
|
||||
<div>
|
||||
Click here to save & Update Employee Data (attachments)
|
||||
|
|
@ -121,7 +138,7 @@
|
|||
name="action_validate_attachments"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="attachments_validation_status == 'validated'"
|
||||
invisible="attachments_validation_status == 'validated' or not employee_id"
|
||||
style="width: 100%;">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data (attachments)
|
||||
|
|
@ -152,21 +169,42 @@
|
|||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="after">
|
||||
<sheet invisible="application_status != 'refused'">
|
||||
<group invisible="application_status != 'refused'">
|
||||
<field name="refused_stage" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<xpath expr="//notebook" position="after">
|
||||
<sheet invisible="application_status != 'refused'">
|
||||
<group invisible="application_status != 'refused'">
|
||||
<field name="refused_stage" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refused_comments" invisible="application_status != 'refused'"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</xpath>
|
||||
<xpath expr="//chatter" position="attributes">
|
||||
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</group>
|
||||
</sheet>
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='additional_info']" position="after">
|
||||
<page string="Request Forms" name="request_forms">
|
||||
<div class="oe_title" style="display:inline-flex; align-items:center; gap:4px;">
|
||||
<label for="post_onboarding_form_status" string="JOD Status:"/>
|
||||
<field name="post_onboarding_form_status" class="oe_inline"/>
|
||||
</div>
|
||||
<field name="request_form_ids">
|
||||
<list editable="bottom">
|
||||
<field name="form_type"/>
|
||||
<button name="action_send_form"
|
||||
string="Send / Resend"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
|
||||
<field name="status" readonly="1" force_save="1"/>
|
||||
<field name="send_date" readonly="1" force_save="1" optional="hide"/>
|
||||
<field name="applicant_submitted_date" readonly="1" force_save="1" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//chatter" position="attributes">
|
||||
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_applicant_view_search_bis_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.search</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -181,10 +219,13 @@
|
|||
<xpath expr="//search/field[@name='job_id']" position="after">
|
||||
<field name="hr_job_recruitment"/>
|
||||
<field name="recruitment_stage_id" domain="[]"/>
|
||||
<field name="submitted_stage" domain="[]"/>
|
||||
<field name="approval_required"/>
|
||||
</xpath>
|
||||
<xpath expr="//filter[@name='my_applications']" position="after">
|
||||
<filter name="to_approve" string="To Approve" domain="[('approval_required', '=', True),('application_submitted', '=', True)]"/>
|
||||
<filter name="submitted_to_client" string="Submitted to Client" domain="[('submitted_to_client', '=', True)]"/>
|
||||
<filter name="not_submitted_to_client" string="Not Submitted to Client" domain="[('submitted_to_client', '=', False)]"/>
|
||||
</xpath>
|
||||
<xpath expr="//search/group" position="inside">
|
||||
<filter string="Job Recruitment" name="job_recruitment" domain="[]"
|
||||
|
|
@ -192,6 +233,10 @@
|
|||
|
||||
<filter string="Job Recruitment Stage" name="job_recruitment_stage" domain="[]"
|
||||
context="{'group_by': 'recruitment_stage_id'}"/>
|
||||
<filter string="Client Submission" name="client_submission_group" domain="[]"
|
||||
context="{'group_by': 'submitted_to_client'}"/>
|
||||
<filter string="Submitted Stage" name="submitted_stage_group" domain="[]"
|
||||
context="{'group_by': 'submitted_stage'}"/>
|
||||
</xpath>
|
||||
<xpath expr="//search/group/filter[@name='stage']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
|
|
@ -207,6 +252,43 @@
|
|||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="default_group_by">recruitment_stage_id</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='application_status']" position="after">
|
||||
<field name="is_on_hold" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='menu']/a[@name='action_create_meeting']" position="attributes">
|
||||
<attribute name="t-if">!record.is_on_hold.raw_value</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='menu']/a[@name='archive_applicant']" position="attributes">
|
||||
<attribute name="t-if">!record.is_on_hold.raw_value</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='menu']/a[@name='archive_applicant']" position="before">
|
||||
<a t-if="!record.is_on_hold.raw_value and record.application_status.raw_value != 'refused'"
|
||||
role="menuitem" name="hold_unhold_button" type="object" class="dropdown-item">Hold</a>
|
||||
<a t-if="record.is_on_hold.raw_value and record.application_status.raw_value != 'refused'"
|
||||
role="menuitem" name="hold_unhold_button" type="object" class="dropdown-item">Un Hold</a>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='menu']/a[@type='archive']" position="attributes">
|
||||
<attribute name="t-if">record.active.raw_value and !record.is_on_hold.raw_value</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='menu']/a[@type='unarchive']" position="attributes">
|
||||
<attribute name="t-if">!record.active.raw_value and !record.is_on_hold.raw_value</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']/widget[@name='web_ribbon'][1]" position="before">
|
||||
<widget name="web_ribbon" title="HOLD" bg_color="text-bg-secondary" invisible="not is_on_hold"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']/field[@name='partner_name']" position="replace">
|
||||
<div t-attf-class="#{record.is_on_hold.raw_value ? 'o_hr_applicant_kanban_card_hold' : ''}">
|
||||
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']/field[@name='job_id']" position="before">
|
||||
<div class="mb-2" invisible="not is_on_hold">
|
||||
<span class="badge text-bg-secondary">On Hold</span>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='kanban_state']" position="attributes">
|
||||
<attribute name="invisible">is_on_hold</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
|
||||
|
|
@ -225,19 +307,22 @@
|
|||
<field name="user_id"/>
|
||||
<field name="active"/>
|
||||
<field name="application_status"/>
|
||||
<field name="submitted_to_client"/>
|
||||
<field name="is_on_hold" invisible="1"/>
|
||||
<field name="company_id"
|
||||
invisible="1"/> <!-- We need to keep this field as it is used in the domain of user_id in the model -->
|
||||
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger"}'/>
|
||||
<templates>
|
||||
<t t-name="menu">
|
||||
<a role="menuitem" name="action_create_meeting" type="object" class="dropdown-item">Schedule
|
||||
<a t-if="!record.is_on_hold.raw_value" role="menuitem" name="action_create_meeting" type="object" class="dropdown-item">Schedule
|
||||
Interview
|
||||
</a>
|
||||
<a role="menuitem" name="archive_applicant" type="object" class="dropdown-item">Reject</a>
|
||||
<a t-if="record.active.raw_value" role="menuitem" type="archive" class="dropdown-item">Archive
|
||||
<a t-if="!record.is_on_hold.raw_value and record.application_status.raw_value != 'refused'" role="menuitem" name="hold_unhold_button" type="object" class="dropdown-item">Hold</a>
|
||||
<a t-if="record.is_on_hold.raw_value and record.application_status.raw_value != 'refused'" role="menuitem" name="hold_unhold_button" type="object" class="dropdown-item">Un Hold</a>
|
||||
<a t-if="!record.is_on_hold.raw_value" role="menuitem" name="archive_applicant" type="object" class="dropdown-item">Reject</a>
|
||||
<a t-if="record.active.raw_value and !record.is_on_hold.raw_value" role="menuitem" type="archive" class="dropdown-item">Archive
|
||||
</a>
|
||||
<a t-if="!record.active.raw_value" role="menuitem" type="unarchive" class="dropdown-item">
|
||||
<a t-if="!record.active.raw_value and !record.is_on_hold.raw_value" role="menuitem" type="unarchive" class="dropdown-item">
|
||||
Unarchive
|
||||
</a>
|
||||
<t t-if="widget.deletable">
|
||||
|
|
@ -245,54 +330,57 @@
|
|||
</t>
|
||||
</t>
|
||||
<t t-name="card">
|
||||
<widget name="web_ribbon" title="HOLD" bg_color="text-bg-success" invisible="not is_on_hold"/>
|
||||
<widget name="web_ribbon" title="Hired" bg_color="text-bg-success" invisible="not date_closed"/>
|
||||
<widget name="web_ribbon" title="Refused" bg_color="text-bg-danger"
|
||||
invisible="application_status != 'refused'"/>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
|
||||
invisible="application_status != 'archived'"/>
|
||||
<div class="d-flex align-items-baseline gap-1 ms-2">
|
||||
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
|
||||
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
|
||||
</div>
|
||||
<div class="row g-0 mt-0 mt-sm-3 ms-2">
|
||||
<div class="col-7">
|
||||
<!-- Categories and applicant properties -->
|
||||
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="applicant_properties" widget="properties"/>
|
||||
<div t-attf-class="o_hr_applicant_kanban_card #{record.is_on_hold.raw_value ? 'o_hr_applicant_kanban_card_hold' : ''}">
|
||||
<widget name="web_ribbon" title="HOLD" bg_color="text-bg-secondary" invisible="not is_on_hold"/>
|
||||
<widget name="web_ribbon" title="Hired" bg_color="text-bg-success" invisible="not date_closed"/>
|
||||
<widget name="web_ribbon" title="Refused" bg_color="text-bg-danger"
|
||||
invisible="application_status != 'refused'"/>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
|
||||
invisible="application_status != 'archived'"/>
|
||||
<div class="d-flex align-items-baseline gap-1 ms-2">
|
||||
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
|
||||
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<!-- Refused information, visible when status is 'refused' -->
|
||||
<div invisible="application_status != 'refused'">
|
||||
<sheet>
|
||||
<!-- Refused Stage -->
|
||||
<div class="form-group">
|
||||
<label for="refuse_reason_id"><strong>Refuse Details</strong></label>
|
||||
<field name="refuse_reason_id" readonly="1" force_save="1"
|
||||
invisible="application_status != 'refused'"/>
|
||||
</div>
|
||||
|
||||
<!-- Refuse Date -->
|
||||
<div class="form-group">
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1"
|
||||
invisible="application_status != 'refused'" widget="date"/>
|
||||
</div>
|
||||
</sheet>
|
||||
<div class="ms-2 mb-2" invisible="not is_on_hold">
|
||||
<span class="badge text-bg-secondary">On Hold</span>
|
||||
</div>
|
||||
<div class="ms-2 mb-2" invisible="not submitted_to_client">
|
||||
<span class="badge text-bg-info">Submitted to Client</span>
|
||||
</div>
|
||||
<div class="row g-0 mt-0 mt-sm-3 ms-2">
|
||||
<div class="col-7">
|
||||
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="applicant_properties" widget="properties"/>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<div invisible="application_status != 'refused'">
|
||||
<sheet>
|
||||
<div class="form-group">
|
||||
<label for="refuse_reason_id"><strong>Refuse Details</strong></label>
|
||||
<field name="refuse_reason_id" readonly="1" force_save="1"
|
||||
invisible="application_status != 'refused'"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1"
|
||||
invisible="application_status != 'refused'" widget="date"/>
|
||||
</div>
|
||||
</sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<field name="priority" widget="priority"/>
|
||||
<field class="ms-1 align-items-center" name="activity_ids" widget="kanban_activity"/>
|
||||
<div class="d-flex ms-auto align-items-center">
|
||||
<a name="action_open_attachments" type="object">
|
||||
<i class='fa fa-paperclip' role="img" aria-label="Documents"/>
|
||||
<field name="attachment_number"/>
|
||||
</a>
|
||||
<field name="kanban_state" class="mx-1" widget="state_selection" invisible="is_on_hold"/>
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<footer>
|
||||
<field name="priority" widget="priority"/>
|
||||
<field class="ms-1 align-items-center" name="activity_ids" widget="kanban_activity"/>
|
||||
<div class="d-flex ms-auto align-items-center">
|
||||
<a name="action_open_attachments" type="object">
|
||||
<i class='fa fa-paperclip' role="img" aria-label="Documents"/>
|
||||
<field name="attachment_number"/>
|
||||
</a>
|
||||
<field name="kanban_state" class="mx-1" widget="state_selection"/>
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
|
@ -428,6 +516,17 @@
|
|||
<field name="orientation">Portrait</field>
|
||||
</record>
|
||||
|
||||
<record id="action_send_jod_to_emp" model="ir.actions.server">
|
||||
<field name="name">Send JOD to EMPLOYEE</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="groups_id" eval="[(4, ref('hr_recruitment.group_hr_recruitment_manager'))]"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.send_jod_form_to_employee()</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_download_joining_form" model="ir.actions.report">
|
||||
<field name="name">Download Joining Form</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -442,4 +541,4 @@
|
|||
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
name="action_validate_personal_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="personal_details_status == 'pending' or not employee_id">
|
||||
invisible="not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data
|
||||
<field name="personal_details_status"
|
||||
<field name="contact_details_status"
|
||||
widget="badge"
|
||||
options="{'pending': 'danger', 'validated': 'success'}"/>
|
||||
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
name="action_validate_personal_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="personal_details_status == 'validated' or not employee_id">
|
||||
invisible="not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="personal_details_status"
|
||||
|
|
@ -110,13 +110,12 @@
|
|||
</div>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Bank Details" invisible="not employee_id">
|
||||
<group string="Bank Details">
|
||||
<button string="Validate/update"
|
||||
name="action_validate_bank_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="bank_details_status == 'pending'">
|
||||
invisible="bank_details_status == 'pending' or not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data
|
||||
<field name="bank_details_status"
|
||||
|
|
@ -131,7 +130,7 @@
|
|||
name="action_validate_bank_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="bank_details_status == 'validated'">
|
||||
invisible="bank_details_status == 'validated' or not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="bank_details_status"
|
||||
|
|
@ -149,13 +148,12 @@
|
|||
<field name="bank_ifsc_code"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Passport Details" invisible="not employee_id">
|
||||
<group string="Passport Details">
|
||||
<button string="Validate/update"
|
||||
name="action_validate_passport_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="passport_details_status == 'pending'">
|
||||
invisible="passport_details_status == 'pending' or not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data
|
||||
<field name="passport_details_status"
|
||||
|
|
@ -170,7 +168,7 @@
|
|||
name="action_validate_passport_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="passport_details_status == 'validated'">
|
||||
invisible="passport_details_status == 'validated' or not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="passport_details_status"
|
||||
|
|
@ -187,13 +185,12 @@
|
|||
<field name="passport_issued_location" string="Issued Location"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Authentication Details" invisible="not employee_id">
|
||||
<group string="Authentication Details">
|
||||
<button string="Validate/update"
|
||||
name="action_validate_authentication_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="authentication_details_status == 'pending'">
|
||||
invisible="authentication_details_status == 'pending' or not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data
|
||||
<field name="authentication_details_status"
|
||||
|
|
@ -208,7 +205,7 @@
|
|||
name="action_validate_authentication_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="authentication_details_status == 'validated'">
|
||||
invisible="authentication_details_status == 'validated' or not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="authentication_details_status"
|
||||
|
|
@ -226,13 +223,12 @@
|
|||
</group>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group invisible="not employee_id">
|
||||
<group>
|
||||
<button string="Validate/update"
|
||||
name="action_validate_family_education_employer_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="family_education_employer_details_status == 'pending'">
|
||||
invisible="family_education_employer_details_status == 'pending' or not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data (Education, Employer, Family Details)
|
||||
<field name="family_education_employer_details_status"
|
||||
|
|
@ -247,7 +243,7 @@
|
|||
name="action_validate_family_education_employer_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="family_education_employer_details_status == 'validated'">
|
||||
invisible="family_education_employer_details_status == 'validated' or not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data (Education, Employer, Family Details)
|
||||
<field name="family_education_employer_details_status"
|
||||
|
|
@ -262,6 +258,9 @@
|
|||
<list string="Employer Details">
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
<button string="Summary" name="action_open_html_popup" type="object"
|
||||
class="oe_highlight" icon="fa-eye"/>
|
||||
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
|
|
@ -286,7 +285,7 @@
|
|||
</field>
|
||||
</group>
|
||||
|
||||
<group string="Education History" colspan="2">
|
||||
<group string="Education Education" colspan="2">
|
||||
<field mode="list" nolabel="1" name="education_history" class="mt-2">
|
||||
<list string="Education Details">
|
||||
<field name="education_type"/>
|
||||
|
|
@ -350,6 +349,8 @@
|
|||
<list string="Employer Details">
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
<button string="Summary" name="action_open_html_popup" type="object" class="oe_highlight" icon="fa-eye"/>
|
||||
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
|
|
@ -386,7 +387,6 @@
|
|||
<field name="start_year"/>
|
||||
<field name="end_year"/>
|
||||
<field name="marks_or_grade"/>
|
||||
<!-- <field name="attachments"/>-->
|
||||
<field name="employee_id" column_invisible="1"/>
|
||||
</list>
|
||||
<form string="Education Details">
|
||||
|
|
@ -425,4 +425,4 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -43,29 +43,17 @@
|
|||
<field name="name">hr.job.recruitment.form</field>
|
||||
<field name="model">hr.job.recruitment</field>
|
||||
<field name="arch" type="xml">
|
||||
<!-- <form string="job">-->
|
||||
<!-- <sheet>-->
|
||||
<!-- <group>-->
|
||||
|
||||
<!-- <div class="oe_title">-->
|
||||
<!-- <field name="recruitment_sequence" readonly="1" force_save="1"/>-->
|
||||
<!-- <field name="job_id"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </group>-->
|
||||
|
||||
<!-- </sheet>-->
|
||||
<!-- </form>-->
|
||||
<!-- Add the recruitment_sequence field into the form -->
|
||||
|
||||
<form string="Job" js_class="recruitment_form_view">
|
||||
<header>
|
||||
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<field name="recruitment_status" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
</header> <!-- inherited in other module -->
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||
<sheet>
|
||||
<form string="Job" js_class="recruitment_form_view">
|
||||
<header>
|
||||
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<button name="action_share_job_recruitment" type="object" string="Share JD" class="btn-primary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user" invisible="not id"/>
|
||||
<field name="recruitment_status" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
</header> <!-- inherited in other module -->
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||
<sheet>
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
icon="fa-pencil"
|
||||
|
|
@ -109,12 +97,12 @@
|
|||
<button name="buttion_view_applicants" type="object" class="oe_stat_button"
|
||||
string="Candidates" widget="statinfo" icon="fa-th-large"/>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="float-end">
|
||||
<field name="website_published" widget="boolean_toggle_labeled" nolabel="1"
|
||||
options="{'false_label': 'Not Published', 'true_label': 'Published'}" on_change="1"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="float-end">
|
||||
<field name="website_published" widget="boolean_toggle_labeled" nolabel="1"
|
||||
options="{'false_label': 'Not Published', 'true_label': 'Published'}" on_change="1"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
|
||||
<field name="recruitment_sequence" readonly="0" force_save="1"/>
|
||||
<group>
|
||||
|
|
@ -215,10 +203,10 @@
|
|||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter open_attachments="True" invisible="hide_chatter_suggestion"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<chatter open_attachments="True" invisible="hide_chatter_suggestion"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_job_recruitment_filter" model="ir.ui.view">
|
||||
|
|
@ -427,30 +415,30 @@
|
|||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_job_recruitment" model="ir.actions.act_window">
|
||||
<field name="name">Job Positions Recruitment</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
</p>
|
||||
<p>
|
||||
Let's create a job position recruitment request.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_hr_job_recruitment_awaiting_published" model="ir.actions.act_window">
|
||||
<field name="name">UnPublished Recruitments</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_job_recruitment" model="ir.actions.act_window">
|
||||
<field name="name">Job Positions Recruitment</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
</p>
|
||||
<p>
|
||||
Let's create a job position recruitment request.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="action_hr_job_recruitment_awaiting_published" model="ir.actions.act_window">
|
||||
<field name="name">JD</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form,search</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="context">{"search_default_open_status":1,"search_default_my_assignments":1,"search_default_unpublished_records":1,'no_of_eligible_submissions': 0}</field>
|
||||
<field name="context">{"search_default_open_status":1,"search_default_my_assignments":1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
|
|
@ -461,21 +449,21 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_job_recruitment_published" model="ir.actions.act_window">
|
||||
<field name="name">Published Recruitments</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form,search</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="context">{"search_default_open_status":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
</p>
|
||||
<p>
|
||||
Let's create a job position Recruitment Requests.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="action_hr_job_recruitment_published" model="ir.actions.act_window">-->
|
||||
<!-- <field name="name">Published</field>-->
|
||||
<!-- <field name="res_model">hr.job.recruitment</field>-->
|
||||
<!-- <field name="view_mode">kanban,list,form,search</field>-->
|
||||
<!-- <field name="search_view_id" ref="view_job_recruitment_filter"/>-->
|
||||
<!-- <field name="context">{"search_default_open_status":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}</field>-->
|
||||
<!-- <field name="help" type="html">-->
|
||||
<!-- <p class="o_view_nocontent_smiling_face">-->
|
||||
<!-- Ready to recruit more efficiently?-->
|
||||
<!-- </p>-->
|
||||
<!-- <p>-->
|
||||
<!-- Let's create a job position Recruitment Requests.-->
|
||||
<!-- </p>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
|
||||
|
||||
|
|
@ -489,23 +477,24 @@
|
|||
<menuitem name="JD"
|
||||
id="menu_hr_job_descriptions"
|
||||
parent="hr_recruitment.menu_hr_recruitment_root"
|
||||
action="action_hr_job_recruitment_awaiting_published"
|
||||
sequence="1"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem
|
||||
name="Awaiting Publication"
|
||||
id="menu_hr_job_recruitment_awaiting_publication"
|
||||
parent="menu_hr_job_descriptions"
|
||||
action="action_hr_job_recruitment_awaiting_published"
|
||||
sequence="1"
|
||||
groups="base.group_user"/>
|
||||
<menuitem
|
||||
name="Published"
|
||||
id="menu_hr_job_recruitment_published"
|
||||
parent="menu_hr_job_descriptions"
|
||||
action="action_hr_job_recruitment_published"
|
||||
sequence="2"
|
||||
groups="base.group_user"/>
|
||||
<!-- <menuitem-->
|
||||
<!-- name="Awaiting Publication"-->
|
||||
<!-- id="menu_hr_job_recruitment_awaiting_publication"-->
|
||||
<!-- parent="menu_hr_job_descriptions"-->
|
||||
<!-- action="action_hr_job_recruitment_awaiting_published"-->
|
||||
<!-- sequence="1"-->
|
||||
<!-- groups="base.group_user"/>-->
|
||||
<!-- <menuitem-->
|
||||
<!-- name="Published"-->
|
||||
<!-- id="menu_hr_job_recruitment_published"-->
|
||||
<!-- parent="menu_hr_job_descriptions"-->
|
||||
<!-- action="action_hr_job_recruitment_published"-->
|
||||
<!-- sequence="2"-->
|
||||
<!-- groups="base.group_user"/>-->
|
||||
|
||||
<menuitem
|
||||
name="Job Positions"
|
||||
|
|
@ -514,15 +503,5 @@
|
|||
action="hr_recruitment.action_hr_job"
|
||||
sequence="0"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
|
||||
<!-- <menuitem-->
|
||||
<!-- name="Candidates"-->
|
||||
<!-- parent="hr_recruitment.menu_crm_case_categ0_act_job"-->
|
||||
<!-- id="menu_hr_candidate_interviewer"-->
|
||||
<!-- action="hr_recruitment.action_hr_candidate"-->
|
||||
<!-- sequence="29"-->
|
||||
<!-- groups="base.group_user"/>-->
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@
|
|||
<attribute name="class" add=""/>
|
||||
</xpath>
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<!-- action="%(action_hr_job_recruitment_applications)d" type="action"-->
|
||||
<attribute name="action">%(hr_recruitment_extended.action_hr_job_recruitment_requests)d</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='no_of_recruitment']" position="attributes">
|
||||
|
|
@ -124,41 +123,11 @@
|
|||
<xpath expr="//notebook" position="inside">
|
||||
<page string="All Recruitments" name="hr_job_recruitments_page">
|
||||
<field name="hr_job_recruitments"/>
|
||||
<!-- <field name="hr_job_recruitments">-->
|
||||
<!-- <list editable="bottom">-->
|
||||
<!-- <field name="recruitment_sequence"/>-->
|
||||
<!-- <field name="date_from"/>-->
|
||||
<!-- <field name="date_end"/>-->
|
||||
<!-- <field name="target"/>-->
|
||||
<!-- <field name="application_count"/>-->
|
||||
<!-- <field name="applicant_hired"/>-->
|
||||
<!-- </list>-->
|
||||
<!-- </field>-->
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- <record model="ir.ui.view" id="hr_job_form_extended">-->
|
||||
<!-- <field name="name">hr.job.form.extended</field>-->
|
||||
<!-- <field name="model">hr.job</field>-->
|
||||
<!-- <field name="inherit_id" ref="hr_recruitment_skills.hr_job_form_inherit_hr_recruitment_skills"/>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <xpath expr="//div[hasclass('oe_button_box')]" position="inside">-->
|
||||
<!-- <button name="buttion_view_applicants" type="object" class="oe_stat_button" string="Candidates" widget="statinfo" icon="fa-th-large"/>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- <xpath expr="//field[@name='skill_ids']" position="after">-->
|
||||
<!-- <field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}"-->
|
||||
<!-- context="{'search_default_group_skill_type_id': 1}"/>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- <xpath expr="//group[@name='recruitment2']" position="inside">-->
|
||||
<!-- <field name="locations" widget="many2many_tags"/>-->
|
||||
<!-- <field name="recruitment_stage_ids" widget="many2many_tags"/>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record model="ir.ui.view" id="hr_recruitment_hr_applicant_view_form_extend">
|
||||
<field name="name">hr.applicant.view.form.extended</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -191,12 +160,9 @@
|
|||
|
||||
|
||||
|
||||
<field name="submitted_to_client" force_save="1" readonly="1" invisible="not submitted_to_client"/>
|
||||
<field name="client_submission_date" force_save="1" readonly="1" invisible="not submitted_to_client"/>
|
||||
<field name="submitted_stage" force_save="1" readonly="1" invisible="not submitted_to_client"/>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
|
||||
<field name="current_ctc"/>
|
||||
<field name="current_ctc" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='application_details']" position="inside">
|
||||
<group>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<template id="applicant_form_template" name="Second Application Form">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="applicant" t-value="applicant"/>
|
||||
<t t-set="applicant_request_id" t-value="applicant_request_id"/>
|
||||
|
||||
<section class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
|
|
@ -13,7 +14,7 @@
|
|||
</h2>
|
||||
|
||||
<form id="hr_recruitment_second_form_applicant"
|
||||
t-att-action="'/hr_recruitment/submit_second_application/%s/submit'%(applicant.id)"
|
||||
t-att-action="'/hr_recruitment/submit_second_application/%s/%s/submit'%(applicant.id,applicant_request_id.id)"
|
||||
method="post">
|
||||
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
||||
<input type="hidden" name="candidate_image_base64"/>
|
||||
|
|
@ -1863,24 +1864,21 @@
|
|||
|
||||
<template id="doc_request_form_template" name="FTPROTECH Doc Request Form">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="applicant_request_id" t-value="applicant_request_id"/>
|
||||
<section class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card shadow-lg p-4">
|
||||
<form id="doc_request_form"
|
||||
t-att-action="'/FTPROTECH/submit/%s/docRequest'%(applicant.id)" method="post"
|
||||
t-att-action="'/FTPROTECH/submit/%s/%s/docRequest'%(applicant.id,applicant_request_id.id)" method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<!-- Upload or Capture Photo -->
|
||||
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
||||
<input type="hidden" name="request_token" t-att-value="request_token"/>
|
||||
<input type="hidden" name="candidate_image_base64"/>
|
||||
<input type="hidden" name="attachments_data_json" id="attachments_data_json"/>
|
||||
|
||||
|
||||
<!-- Applicant Photo -->
|
||||
<!-- Profile Picture Upload (Similar to res.users) -->
|
||||
<div class="mb-3 text-center">
|
||||
<!-- Image Preview with Label Click -->
|
||||
<label for="candidate_image" style="cursor: pointer;">
|
||||
<img id="photo_preview"
|
||||
t-att-src="'data:image/png;base64,' + (applicant.candidate_image.decode() if applicant.candidate_image else '')"
|
||||
|
|
@ -1888,11 +1886,9 @@
|
|||
style="display: flex; align-items: center; justify-content: center; width: 150px; height: 150px; object-fit: cover; border: 2px solid #ddd; overflow: hidden;"/>
|
||||
</label>
|
||||
|
||||
<!-- Hidden File Input -->
|
||||
<input type="file" class="d-none" name="candidate_image" id="candidate_image"
|
||||
accept="image/*"/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
onclick="document.getElementById('candidate_image').click();">
|
||||
|
|
@ -1908,7 +1904,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<div class="modal fade" id="photoPreviewModal" tabindex="-1"
|
||||
aria-labelledby="photoPreviewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
|
@ -1956,13 +1951,11 @@
|
|||
<input type="file"
|
||||
class="form-control d-none attachment-input"
|
||||
t-att-data-attachment-id="attachment.id"
|
||||
multiple="multiple"
|
||||
accept="image/*,application/pdf"/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table to display uploaded files -->
|
||||
<div t-att-id="'preview_table_container_%s' % attachment.id"
|
||||
class="uploaded-files-preview d-none">
|
||||
<table class="table table-bordered table-striped">
|
||||
|
|
@ -1981,7 +1974,6 @@
|
|||
<p>No attachments required.</p>
|
||||
</div>
|
||||
|
||||
<!-- Image Preview Modal -->
|
||||
<div class="modal fade" id="attachmentPreviewModal" tabindex="-1"
|
||||
aria-labelledby="attachmentPreviewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
|
|
@ -1993,12 +1985,10 @@
|
|||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<!-- Image Preview -->
|
||||
<img id="modal_attachment_photo_preview" src=""
|
||||
class="img-fluid rounded shadow"
|
||||
style="max-width: 100%; display: none;"/>
|
||||
|
||||
<!-- PDF Preview -->
|
||||
<iframe id="modal_attachment_pdf_preview" src=""
|
||||
width="100%" height="500px"
|
||||
style="border: none; display: none;"></iframe>
|
||||
|
|
@ -2236,4 +2226,4 @@
|
|||
</template>
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<field name="model">recruitment.requisition</field>
|
||||
<field name="inherit_id" ref="requisitions.view_requisition_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- <field name="job_id" invisible="job_id == False" readonly="1" force_save="1"/>-->
|
||||
<xpath expr="//field[@name='job_id']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
<attribute name="readonly">state not in ['draft']</attribute>
|
||||
|
|
@ -26,4 +25,4 @@
|
|||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@
|
|||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='category_id']" position="after">
|
||||
<!-- <group>-->
|
||||
<field name="contact_type"/>
|
||||
<!-- </group>-->
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
|
|
@ -69,4 +67,4 @@
|
|||
sequence="1"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@
|
|||
</xpath>
|
||||
<xpath expr="//field[@name='job_ids']" position="after">
|
||||
<field name="job_recruitment_ids" string="Job Recruitment Ids" widget="many2many_tags"/>
|
||||
<field name="second_application_form"/>
|
||||
<field name="post_onboarding_form"/>
|
||||
<field name="require_approval" string="Approval Required"/>
|
||||
<field name="stage_color" widget="color" string="Select Stage Color"/>
|
||||
</xpath>
|
||||
|
|
@ -29,4 +27,4 @@
|
|||
sequence="1"/>
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ from . import post_onboarding_attachment_wizard
|
|||
from . import applicant_refuse_reason
|
||||
from . import ats_invite_mail_template_wizard
|
||||
from . import client_submission_mail_template_wizard
|
||||
from . import applicant_stage_comment_wizard
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import base64
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ApplicantOfferMailWizard(models.TransientModel):
|
||||
_name = 'applicant.offer.mail.wizard'
|
||||
_description = 'Applicant Offer Mail Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string='Applicant', required=True, readonly=True)
|
||||
offer_letter_id = fields.Many2one('offer.letter', string='Offer Letter', readonly=True)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template', required=True, readonly=True)
|
||||
|
||||
position = fields.Char(string='Position', required=True)
|
||||
salary = fields.Float(string='Annual CTC', required=True)
|
||||
joining_date = fields.Date(string='Joining Date', required=True)
|
||||
pay_struct_id = fields.Many2one('hr.payroll.structure', string='Salary Structure', required=True)
|
||||
manager_id = fields.Many2one('hr.employee', string='Manager')
|
||||
contract_type = fields.Selection([
|
||||
('permanent', 'Permanent'),
|
||||
('contract', 'Fixed Term Contract'),
|
||||
('intern', 'Internship'),
|
||||
], string='Contract Type', default='permanent', required=True)
|
||||
probation_period = fields.Integer(string='Probation Period (months)', default=3, required=True)
|
||||
|
||||
email_from = fields.Char('Email From', required=True)
|
||||
email_to = fields.Char('Email To', required=True)
|
||||
email_cc = fields.Text('Email CC')
|
||||
email_subject = fields.Char('Subject', required=True)
|
||||
email_body = fields.Html(
|
||||
'Body',
|
||||
render_engine='qweb',
|
||||
render_options={'post_process': True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize='email_outgoing',
|
||||
required=True,
|
||||
)
|
||||
attachment_ids = fields.Many2many('ir.attachment', string='Attachments')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
applicant = self._get_applicant()
|
||||
template = self.env.ref(
|
||||
'hr_recruitment_extended.applicant_offer_email_template',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
offer_letter = self._create_offer_letter(applicant)
|
||||
|
||||
defaults.update({
|
||||
'applicant_id': applicant.id,
|
||||
'offer_letter_id': offer_letter.id,
|
||||
'position': offer_letter.position,
|
||||
'salary': offer_letter.salary,
|
||||
'joining_date': offer_letter.joining_date,
|
||||
'pay_struct_id': offer_letter.pay_struct_id.id,
|
||||
'manager_id': offer_letter.manager_id.id,
|
||||
'contract_type': offer_letter.contract_type,
|
||||
'probation_period': offer_letter.probation_period,
|
||||
})
|
||||
if template:
|
||||
defaults['template_id'] = template.id
|
||||
defaults.update(self._prepare_mail_defaults(template, offer_letter))
|
||||
return defaults
|
||||
|
||||
def _get_applicant(self):
|
||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||
if not applicant.exists():
|
||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
||||
return applicant
|
||||
|
||||
def _get_default_pay_structure(self, applicant):
|
||||
company = applicant.company_id or self.env.company
|
||||
return self.env['hr.payroll.structure'].search([
|
||||
'|',
|
||||
('company_id', '=', company.id),
|
||||
('company_id', '=', False),
|
||||
], limit=1)
|
||||
|
||||
def _get_default_manager(self, applicant):
|
||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
||||
|
||||
def _create_offer_letter(self, applicant):
|
||||
pay_structure = self._get_default_pay_structure(applicant)
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before sending an offer."))
|
||||
|
||||
offer_letter = self.env['offer.letter'].create({
|
||||
'candidate_id': applicant.id,
|
||||
'position': applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.partner_name or applicant.display_name,
|
||||
'salary': applicant.salary_expected or applicant.current_ctc or 0.0,
|
||||
'joining_date': fields.Date.today(),
|
||||
'pay_struct_id': pay_structure.id,
|
||||
'manager_id': self._get_default_manager(applicant).id,
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
return offer_letter
|
||||
|
||||
def _update_offer_letter(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
'candidate_id': self.applicant_id.id,
|
||||
'position': self.position,
|
||||
'salary': self.salary,
|
||||
'joining_date': self.joining_date,
|
||||
'pay_struct_id': self.pay_struct_id.id,
|
||||
'manager_id': self.manager_id.id,
|
||||
'contract_type': self.contract_type,
|
||||
'probation_period': self.probation_period,
|
||||
}
|
||||
self.offer_letter_id.write(vals)
|
||||
self.offer_letter_id.get_paydetailed_lines()
|
||||
return self.offer_letter_id
|
||||
|
||||
def _generate_offer_attachment(self, offer_letter):
|
||||
report = self.env.ref('offer_letters.hr_offer_letters_employee_print')
|
||||
pdf_content, _ = report.sudo()._render_qweb_pdf(report, offer_letter.id)
|
||||
attachment_name = _('Offer Letter - %s.pdf') % (offer_letter.candidate_id.partner_name or offer_letter.name)
|
||||
existing_attachment = self.attachment_ids.filtered(
|
||||
lambda attachment: attachment.name == attachment_name and attachment.mimetype == 'application/pdf'
|
||||
)[:1]
|
||||
|
||||
attachment_vals = {
|
||||
'name': attachment_name,
|
||||
'datas': base64.b64encode(pdf_content),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id or 0,
|
||||
'type': 'binary',
|
||||
}
|
||||
if existing_attachment:
|
||||
existing_attachment.write(attachment_vals)
|
||||
return existing_attachment
|
||||
return self.env['ir.attachment'].create(attachment_vals)
|
||||
|
||||
def _prepare_mail_defaults(self, template, offer_letter):
|
||||
render_results = template._generate_template(
|
||||
[offer_letter.id],
|
||||
['subject', 'body_html', 'email_from', 'email_to', 'email_cc', 'reply_to'],
|
||||
find_or_create_partners=False,
|
||||
)
|
||||
generated_values = render_results.get(offer_letter.id, {})
|
||||
attachment = self._generate_offer_attachment(offer_letter)
|
||||
attachment_ids = [attachment.id] if attachment else []
|
||||
|
||||
return {
|
||||
'email_from': generated_values.get('email_from') or offer_letter.candidate_id.user_id.email or self.env.user.email,
|
||||
'email_to': generated_values.get('email_to') or offer_letter.candidate_id.email_from or '',
|
||||
'email_cc': generated_values.get('email_cc', ''),
|
||||
'email_subject': generated_values.get('subject', ''),
|
||||
'email_body': generated_values.get('body_html', ''),
|
||||
'attachment_ids': [(6, 0, attachment_ids)],
|
||||
}
|
||||
|
||||
def action_send_offer(self):
|
||||
self.ensure_one()
|
||||
offer_letter = self._update_offer_letter()
|
||||
offer_attachment = self._generate_offer_attachment(offer_letter)
|
||||
attachment_ids = self.attachment_ids.ids
|
||||
if offer_attachment.id not in attachment_ids:
|
||||
attachment_ids.append(offer_attachment.id)
|
||||
|
||||
mail = self.env['mail.mail'].create({
|
||||
'email_from': self.email_from,
|
||||
'email_to': self.email_to,
|
||||
'email_cc': self.email_cc,
|
||||
'subject': self.email_subject,
|
||||
'body_html': self.email_body,
|
||||
'attachment_ids': [(6, 0, attachment_ids)],
|
||||
'auto_delete': False,
|
||||
'model': 'offer.letter',
|
||||
'res_id': offer_letter.id,
|
||||
})
|
||||
mail.send()
|
||||
offer_letter.action_send_offer()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_applicant_offer_mail_wizard_form" model="ir.ui.view">
|
||||
<field name="name">applicant.offer.mail.wizard.form</field>
|
||||
<field name="model">applicant.offer.mail.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Send Offer">
|
||||
<group>
|
||||
<field name="applicant_id" readonly="1"/>
|
||||
<field name="template_id" options="{'no_create': True}" readonly="1"/>
|
||||
</group>
|
||||
<group string="Offer Details" col="2">
|
||||
<field name="position"/>
|
||||
<field name="salary"/>
|
||||
<field name="joining_date"/>
|
||||
<field name="pay_struct_id" options="{'no_create': True}"/>
|
||||
<field name="manager_id"/>
|
||||
<field name="contract_type"/>
|
||||
<field name="probation_period"/>
|
||||
</group>
|
||||
<group string="Email Details">
|
||||
<field name="email_from" placeholder="Sender email"/>
|
||||
<field name="email_to" placeholder="Recipient email"/>
|
||||
<field name="email_cc" placeholder="Comma-separated CC recipients"/>
|
||||
<field name="email_subject" options="{'dynamic_placeholder': true}" placeholder="Email subject"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Body">
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor"
|
||||
options="{'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
</page>
|
||||
<page string="Attachments">
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button name="action_send_offer" type="object" string="Send Offer" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ApplicantStageCommentWizard(models.TransientModel):
|
||||
_name = 'applicant.stage.comment.wizard'
|
||||
_description = 'Applicant Stage Comment Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', required=True, readonly=True)
|
||||
stage_id = fields.Many2one('hr.recruitment.stage', required=True)
|
||||
comment = fields.Text(string='Comment')
|
||||
stage_comment_ids = fields.Many2many(
|
||||
'hr.applicant.stage.comment',
|
||||
compute='_compute_stage_comment_ids',
|
||||
string='Stage Comments',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
values = super().default_get(fields_list)
|
||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||
if applicant:
|
||||
values.setdefault('applicant_id', applicant.id)
|
||||
values.setdefault('stage_id', applicant.recruitment_stage_id.id)
|
||||
return values
|
||||
|
||||
@api.depends('applicant_id', 'stage_id')
|
||||
def _compute_stage_comment_ids(self):
|
||||
Comment = self.env['hr.applicant.stage.comment']
|
||||
for wizard in self:
|
||||
domain = [('applicant_id', '=', wizard.applicant_id.id)]
|
||||
# if wizard.stage_id:
|
||||
# domain.append(('stage_id', '=', wizard.stage_id.id))
|
||||
wizard.stage_comment_ids = Comment.search(domain)
|
||||
|
||||
def action_add_comment(self):
|
||||
self.ensure_one()
|
||||
if not self.comment:
|
||||
raise UserError(_('Please enter a comment before saving.'))
|
||||
self.env['hr.applicant.stage.comment'].create({
|
||||
'applicant_id': self.applicant_id.id,
|
||||
'stage_id': self.stage_id.id,
|
||||
'comment': self.comment,
|
||||
})
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_applicant_stage_comment_wizard_form" model="ir.ui.view">
|
||||
<field name="name">applicant.stage.comment.wizard.form</field>
|
||||
<field name="model">applicant.stage.comment.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Stage Comment">
|
||||
<group>
|
||||
<field name="applicant_id"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="comment" placeholder="Write a comment for this stage..."/>
|
||||
</group>
|
||||
<group string="Existing Comments">
|
||||
<field name="stage_comment_ids" nolabel="1" readonly="1">
|
||||
<list create="0" edit="0" delete="0">
|
||||
<field name="comment_date"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="comment"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_add_comment" type="object" string="Save Comment" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from odoo import models, fields, api
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
'recruitment.attachments',
|
||||
string='Attachments to Request'
|
||||
)
|
||||
request_form_id = fields.Many2one('applicant.request.forms')
|
||||
attachment_ids = fields.Many2many('ir.attachment')
|
||||
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template',compute='_compute_template_id')
|
||||
|
|
@ -36,9 +37,9 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
|
||||
@api.onchange('template_id')
|
||||
def _onchange_template_id(self):
|
||||
""" Update the email body and recipients based on the selected template. """
|
||||
"""Update the email body and recipients based on the selected template."""
|
||||
if self.template_id:
|
||||
record_id = self.env.context.get('active_id')
|
||||
record_id = self.env.context.get('applicant_id')
|
||||
applicant = self.env['hr.applicant'].browse(record_id)
|
||||
|
||||
if record_id:
|
||||
|
|
@ -47,7 +48,6 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
if not record.exists():
|
||||
raise UserError("The record does not exist or is not accessible.")
|
||||
|
||||
# Fetch email template
|
||||
email_template = self.env['mail.template'].sudo().browse(self.template_id.id)
|
||||
|
||||
if not email_template:
|
||||
|
|
@ -55,7 +55,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
|
||||
self.email_from = self.env.company.email
|
||||
self.email_to = applicant.email_from
|
||||
self.email_body = email_template.body_html # Assign the rendered email bodyc
|
||||
self.email_body = email_template.body_html
|
||||
self.email_subject = email_template.subject
|
||||
|
||||
@api.model
|
||||
|
|
@ -72,8 +72,21 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
for rec in self:
|
||||
self.ensure_one()
|
||||
context = self.env.context
|
||||
active_id = context.get('active_id')
|
||||
active_id = context.get('applicant_id')
|
||||
applicant = self.env['hr.applicant'].browse(active_id)
|
||||
request_token = False
|
||||
request_upload_url = False
|
||||
|
||||
if rec.is_pre_onboarding_attachment_request and not rec.request_form_id:
|
||||
raise UserError("A document request form is required before sending this email.")
|
||||
|
||||
if rec.request_form_id:
|
||||
request_token = rec.request_form_id._issue_new_access_token()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
request_upload_url = (
|
||||
f"{base_url}/FTPROTECH/DocRequests/"
|
||||
f"{applicant.id}/{rec.request_form_id.id}?token={request_token}"
|
||||
)
|
||||
|
||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in rec.req_attachment_ids]
|
||||
|
||||
|
|
@ -85,31 +98,36 @@ 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')
|
||||
|
||||
|
||||
# Prepare context for the template
|
||||
print(request_upload_url)
|
||||
email_context = {
|
||||
'applicant_request_form_id': rec.request_form_id.id,
|
||||
'applicant_request_form_token': request_token,
|
||||
'applicant_request_form_url': request_upload_url,
|
||||
'personal_docs': personal_docs,
|
||||
'education_docs': education_docs,
|
||||
'previous_employer_docs': previous_employer_docs,
|
||||
'other_docs': other_docs,
|
||||
}
|
||||
rendered_subject = template.with_context(**email_context)._render_field(
|
||||
'subject', [applicant.id]
|
||||
)[applicant.id]
|
||||
rendered_body_html = template.with_context(**email_context)._render_field(
|
||||
'body_html', [applicant.id], compute_lang=True
|
||||
)[applicant.id]
|
||||
email_values = {
|
||||
'email_from': rec.email_from,
|
||||
'email_to': rec.email_to,
|
||||
'email_cc': rec.email_cc,
|
||||
'subject': rec.email_subject,
|
||||
'subject': rendered_subject or rec.email_subject,
|
||||
'body_html': rendered_body_html,
|
||||
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
||||
|
||||
}
|
||||
# Use 'with_context' to override the email template fields dynamically
|
||||
template.sudo().with_context(default_body_html=rec.email_body,
|
||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
||||
force_send=True)
|
||||
|
||||
|
||||
if rec.is_pre_onboarding_attachment_request:
|
||||
applicant.doc_requests_form_status = 'email_sent_to_candidate'
|
||||
else:
|
||||
if not rec.is_pre_onboarding_attachment_request:
|
||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
'security/ir.model.access.csv',
|
||||
'data/data.xml',
|
||||
'views/masters.xml',
|
||||
# 'views/groups.xml',
|
||||
'views/groups.xml',
|
||||
'views/login.xml',
|
||||
'views/menu_access_control_views.xml',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from odoo.http import request
|
|||
from odoo.addons.web.controllers.home import Home
|
||||
|
||||
|
||||
class CustomMasterLogin(Home):
|
||||
class CustomMasterLogin(Home):
|
||||
|
||||
@http.route()
|
||||
def web_login(self, *args, **kw):
|
||||
|
|
@ -13,23 +13,23 @@ class CustomMasterLogin(Home):
|
|||
response = super(CustomMasterLogin, self).web_login(*args, **kw)
|
||||
|
||||
if response.is_qweb:
|
||||
response.qcontext['masters'] = request.env['master.control'].sudo().search([])
|
||||
response.qcontext['masters'] = request.env['master.control'].sudo().search([], order='sequence asc, id asc')
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
||||
|
||||
if request.session.uid and master_selected:
|
||||
user = request.env.user
|
||||
master = request.env['master.control'].sudo().search(
|
||||
[('code', '=', master_selected)], limit=1
|
||||
)
|
||||
|
||||
if master.exists() and master.user_ids:
|
||||
if user not in master.user_ids:
|
||||
request.session.logout(keep_db=True)
|
||||
|
||||
# Create a response with JavaScript alert
|
||||
html = f"""
|
||||
<html>
|
||||
if request.session.uid and master_selected:
|
||||
user = request.env.user
|
||||
master = request.env['master.control'].sudo().search(
|
||||
[('code', '=', master_selected)], limit=1
|
||||
)
|
||||
|
||||
if master.exists() and master.user_ids:
|
||||
if user not in master.user_ids:
|
||||
request.session.logout(keep_db=True)
|
||||
|
||||
# Create a response with JavaScript alert
|
||||
html = f"""
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
alert("{_("You don't have access to login to '%s'. Please contact the administrator.") % master.display_name}");
|
||||
|
|
|
|||
|
|
@ -152,3 +152,22 @@ class IrUiMenu(models.Model):
|
|||
parent = parent.parent_id
|
||||
|
||||
return visible
|
||||
|
||||
def unlink(self):
|
||||
# Clean up references before deleting menus
|
||||
menu_ids = self.ids
|
||||
|
||||
# Delete master control menu lines
|
||||
self.env['master.control.menu.line'].sudo().search([
|
||||
('menu_id', 'in', menu_ids)
|
||||
]).unlink()
|
||||
|
||||
# Delete menu access lines
|
||||
self.env['menu.access.line'].sudo().search([
|
||||
('menu_id', 'in', menu_ids)
|
||||
]).unlink()
|
||||
|
||||
# Clear caches
|
||||
self.clear_caches()
|
||||
|
||||
return super().unlink()
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<record id="base.action_res_groups" model="ir.actions.act_window">
|
||||
<field name="name">Roles</field>
|
||||
<field name="res_model">res.groups</field>
|
||||
<!-- <field name="domain">[('is_visible_for_master', '=', True)]</field>-->
|
||||
<field name="domain"></field>
|
||||
<!-- <field name="context">{'search_default_filter_no_share': 1}</field>-->
|
||||
<!-- <field name="help">A group is a set of functional areas that will be assigned to the user in order to give-->
|
||||
<!-- them access and rights to specific applications and tasks in the system. You can create custom groups or-->
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<!-- </field>-->
|
||||
</record>
|
||||
|
||||
<menuitem action="base.action_res_groups" name="Roles" id="base.menu_action_res_groups" parent="base.menu_users" groups="base.group_user" sequence="3"/>
|
||||
<!-- <menuitem action="base.action_res_groups" name="Roles" id="base.menu_action_res_groups" parent="base.menu_users" groups="base.group_user" sequence="3"/>-->
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -27,9 +27,13 @@ export class ModuleSelector extends Component {
|
|||
const masters = await this.orm.searchRead(
|
||||
"master.control",
|
||||
[["user_ids", "in", [user.userId]]],
|
||||
["name", "code"]
|
||||
["name", "code", "sequence", "id"],
|
||||
{
|
||||
order: "sequence ASC, id ASC",
|
||||
offset: 0,
|
||||
limit: false
|
||||
}
|
||||
);
|
||||
|
||||
this.state.masters = masters;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
from . import models
|
||||
from . import models
|
||||
from . import wizards
|
||||
from . import controllers
|
||||
|
|
|
|||
|
|
@ -8,12 +8,18 @@
|
|||
""",
|
||||
'author': 'Raman Marikanti',
|
||||
'category': 'Human Resources',
|
||||
'depends': ['base', 'hr_recruitment','hr_payroll','hr_ftp'],
|
||||
'depends': ['base', 'hr_recruitment', 'hr_payroll', 'hr_recruitment_extended'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/mail_template.xml',
|
||||
'views/offer_letter_views.xml',
|
||||
'views/hr_applicant_offer_views.xml',
|
||||
'views/offer_response_templates.xml',
|
||||
# 'views/templates.xml',
|
||||
'views/menu_views.xml',
|
||||
'wizards/offer_release_request_wizard.xml',
|
||||
'wizards/applicant_offer_mail_wizard.xml',
|
||||
'wizards/offer_letter_reject_wizard.xml',
|
||||
'report/offer_letter_report.xml',
|
||||
'report/offer_letter_template.xml',
|
||||
],
|
||||
|
|
@ -27,4 +33,4 @@
|
|||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class OfferLetterResponseController(http.Controller):
|
||||
|
||||
def _get_offer_letter(self, offer_id, token):
|
||||
offer_letter = request.env['offer.letter'].sudo().browse(offer_id)
|
||||
if not offer_letter.exists():
|
||||
raise request.not_found()
|
||||
if not token or offer_letter.response_token != token:
|
||||
raise request.not_found()
|
||||
if not offer_letter._is_latest_offer():
|
||||
raise request.not_found()
|
||||
return offer_letter
|
||||
|
||||
@http.route('/offer_letters/respond/<int:offer_id>/accept', type='http', auth='public')
|
||||
def accept_offer(self, offer_id, token=None, **kwargs):
|
||||
offer_letter = self._get_offer_letter(offer_id, token)
|
||||
if offer_letter.state == 'requested':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Not Released',
|
||||
'message': 'This offer is not yet available for response.',
|
||||
})
|
||||
if offer_letter.state == 'accepted':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Already Accepted',
|
||||
'message': 'Your acceptance has already been recorded.',
|
||||
})
|
||||
if offer_letter.state == 'rejected':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Already Rejected',
|
||||
'message': 'This offer has already been rejected.',
|
||||
})
|
||||
offer_letter.action_accept_offer()
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Accepted',
|
||||
'message': 'Thank you. Your offer acceptance has been recorded successfully.',
|
||||
})
|
||||
|
||||
@http.route('/offer_letters/respond/<int:offer_id>/reject', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
||||
def reject_offer(self, offer_id, token=None, **post):
|
||||
offer_letter = self._get_offer_letter(offer_id, token)
|
||||
if offer_letter.state == 'requested':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Not Released',
|
||||
'message': 'This offer is not yet available for response.',
|
||||
})
|
||||
if request.httprequest.method == 'POST':
|
||||
reason = post.get('rejection_reason', '').strip()
|
||||
if not reason:
|
||||
return request.render('offer_letters.offer_reject_reason_page', {
|
||||
'offer_letter': offer_letter,
|
||||
'token': token,
|
||||
'error': 'Please enter a rejection reason.',
|
||||
})
|
||||
offer_letter.action_reject_offer(reason)
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Rejected',
|
||||
'message': 'Your rejection response has been recorded successfully.',
|
||||
})
|
||||
|
||||
if offer_letter.state == 'accepted':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Already Accepted',
|
||||
'message': 'This offer has already been accepted.',
|
||||
})
|
||||
if offer_letter.state == 'rejected':
|
||||
return request.render('offer_letters.offer_response_message', {
|
||||
'title': 'Offer Already Rejected',
|
||||
'message': 'This offer has already been rejected.',
|
||||
})
|
||||
return request.render('offer_letters.offer_reject_reason_page', {
|
||||
'offer_letter': offer_letter,
|
||||
'token': token,
|
||||
'error': False,
|
||||
})
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="applicant_offer_email_template" model="mail.template">
|
||||
<field name="name">Applicant Offer Email Template</field>
|
||||
<field name="model_id" ref="offer_letters.model_offer_letter"/>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.candidate_id.email_from or '' }}</field>
|
||||
<field name="subject">Offer Letter - {{ object.position or object.candidate_id.job_id.name or '' }}</field>
|
||||
<field name="description">
|
||||
Send applicant offer mail with offer letter attachment.
|
||||
</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0; padding: 0; font-size: 13px; line-height: 1.7;">
|
||||
<p>Dear <t t-esc="object.candidate_id.partner_name or ''"/>,</p>
|
||||
<p>
|
||||
With reference to the interview and subsequent discussions you had with us, we are pleased to select
|
||||
you for the position of "<t t-esc="object.position or ''"/>" in our organization with the following
|
||||
terms and conditions.
|
||||
</p>
|
||||
<p>
|
||||
You will be paid a compensation of
|
||||
<strong>Rs <t t-esc="'{:,.0f}'.format(object.salary or 0.0)"/> LPA</strong>
|
||||
(<t t-esc="env.company.currency_id.amount_to_text(object.salary or 0.0)"/>) on a cost-to-company basis.
|
||||
</p>
|
||||
<p>
|
||||
We would like you to join us at the earliest but not later than
|
||||
<strong><t t-esc="object.joining_date or ''"/></strong>.
|
||||
</p>
|
||||
<p>
|
||||
Enclosed is the Offer Letter cum Appointment with the break-up of salary for your reference.
|
||||
The signed copy of the same will be issued to you on the date of joining.
|
||||
</p>
|
||||
<p>Your salary structure may be revised as per statutory compliances, keeping the CTC intact.</p>
|
||||
<p>
|
||||
You will be required to sign a Code of Conduct, Non-Disclosure Agreement, and Anti-Corruption Policy
|
||||
at the time of your joining. The details are mentioned in the enclosed letter.
|
||||
</p>
|
||||
<p>You would need to travel as per business requirement.</p>
|
||||
<p>
|
||||
Kindly acknowledge this email as a token of acceptance of this offer within 2 days of the email dated,
|
||||
failing which the offer stands withdrawn. This offer is valid subject to the current employer's
|
||||
reference check.
|
||||
</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<a t-att-href="ctx.get('offer_accept_url')" target="_blank"
|
||||
style="background-color: #198754; color: #ffffff; padding: 10px 18px; text-decoration: none; border-radius: 4px; display: inline-block; margin-right: 10px;">
|
||||
Accept Offer
|
||||
</a>
|
||||
<a t-att-href="ctx.get('offer_reject_url')" target="_blank"
|
||||
style="background-color: #dc3545; color: #ffffff; padding: 10px 18px; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||
Reject Offer
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Regards,<br/>
|
||||
HR Team<br/>
|
||||
9703546916
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1 +1,3 @@
|
|||
from . import offer_letter
|
||||
from . import offer_letter
|
||||
from . import hr_applicant
|
||||
from . import hr_candidate
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
|
||||
finalized_ctc = fields.Float(string='Finalized CTC', tracking=True)
|
||||
offer_letter_ids = fields.One2many('offer.letter', 'candidate_id', string='Offer Letters')
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
'offer.letter',
|
||||
string='Current Offer Letter',
|
||||
compute='_compute_current_offer_letter',
|
||||
store=True,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
('requested', 'Requested'),
|
||||
('sent', 'Sent to Applicant'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
string='Offer Status',
|
||||
related='current_offer_letter_id.state',
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('offer_letter_ids', 'offer_letter_ids.create_date', 'offer_letter_ids.state')
|
||||
def _compute_current_offer_letter(self):
|
||||
for applicant in self:
|
||||
offer_letters = applicant.offer_letter_ids.sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string('1970-01-01 00:00:00'), offer.id)
|
||||
)
|
||||
applicant.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
||||
def action_request_offer_release(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Request Offer Release'),
|
||||
'res_model': 'offer.release.request.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('offer_letters.view_offer_release_request_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_applicant_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_send_offer(self):
|
||||
return self.action_request_offer_release()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HRCandidate(models.Model):
|
||||
_inherit = 'hr.candidate'
|
||||
|
||||
current_offer_letter_id = fields.Many2one(
|
||||
'offer.letter',
|
||||
string='Current Offer Letter',
|
||||
compute='_compute_current_offer_letter',
|
||||
store=False,
|
||||
)
|
||||
offer_release_status = fields.Selection(
|
||||
selection=[
|
||||
('requested', 'Requested'),
|
||||
('sent', 'Sent to Applicant'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
string='Offer Status',
|
||||
related='current_offer_letter_id.state',
|
||||
readonly=True,
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('applicant_ids.current_offer_letter_id', 'applicant_ids.current_offer_letter_id.create_date')
|
||||
def _compute_current_offer_letter(self):
|
||||
for candidate in self:
|
||||
offer_letters = candidate.applicant_ids.mapped('current_offer_letter_id').sorted(
|
||||
key=lambda offer: (offer.create_date or fields.Datetime.from_string('1970-01-01 00:00:00'), offer.id)
|
||||
)
|
||||
candidate.current_offer_letter_id = offer_letters[-1] if offer_letters else False
|
||||
|
|
@ -27,6 +27,8 @@ class OfferLetter(models.Model):
|
|||
)
|
||||
candidate_id = fields.Many2one( 'hr.applicant', string='Candidate', required=True,
|
||||
)
|
||||
requested_by_id = fields.Many2one('res.users', string='Requested By', readonly=True, tracking=True)
|
||||
request_date = fields.Datetime(string='Requested On', readonly=True, tracking=True)
|
||||
|
||||
employee_id = fields.Char(
|
||||
string='Employee ID',
|
||||
|
|
@ -68,20 +70,20 @@ class OfferLetter(models.Model):
|
|||
default=lambda self: self._default_terms()
|
||||
)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('requested', 'Requested'),
|
||||
('sent', 'Sent'),
|
||||
('accepted', 'Accepted'),
|
||||
('rejected', 'Rejected'),
|
||||
('expired', 'Expired')],
|
||||
string='Status',
|
||||
default='draft',
|
||||
default='requested',
|
||||
tracking=True
|
||||
)
|
||||
sent_date = fields.Datetime(string='Sent Date')
|
||||
response_date = fields.Datetime(string='Response Date')
|
||||
pay_struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
||||
manager_id = fields.Many2one('hr.employee', string='Manager')
|
||||
|
||||
rejection_reason = fields.Char()
|
||||
|
||||
@api.model
|
||||
def _default_terms(self):
|
||||
|
|
@ -95,13 +97,41 @@ class OfferLetter(models.Model):
|
|||
def create(self, vals):
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('New')
|
||||
if vals.get('candidate_id') and 'salary' in vals:
|
||||
self.env['hr.applicant'].browse(vals['candidate_id']).write({
|
||||
'finalized_ctc': vals['salary'],
|
||||
})
|
||||
return super(OfferLetter, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
result = super(OfferLetter, self).write(vals)
|
||||
if 'salary' in vals:
|
||||
for record in self.filtered('candidate_id'):
|
||||
record.candidate_id.finalized_ctc = record.salary
|
||||
return result
|
||||
|
||||
def action_open_send_offer_wizard(self):
|
||||
self.ensure_one()
|
||||
if not self.env.user.has_group('hr.group_hr_manager'):
|
||||
raise UserError(_("Only HR can release the offer letter to the applicant."))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Send Offer'),
|
||||
'res_model': 'applicant.offer.mail.wizard',
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('offer_letters.view_applicant_offer_mail_wizard_form').id,
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'active_model': 'offer.letter',
|
||||
'active_id': self.id,
|
||||
},
|
||||
}
|
||||
|
||||
def action_send_offer(self):
|
||||
self.ensure_one()
|
||||
# template = self.env.ref('offer_letters.email_template_offer_letter')
|
||||
if not self.env.user.has_group('hr.group_hr_manager'):
|
||||
raise UserError(_("Only HR can mark the offer as sent."))
|
||||
self.write({'state': 'sent', 'sent_date': fields.Datetime.now()})
|
||||
# template.send_mail(self.id, force_send=True)
|
||||
return True
|
||||
|
||||
def action_accept_offer(self):
|
||||
|
|
@ -222,4 +252,4 @@ class OfferLetter(models.Model):
|
|||
}
|
||||
|
||||
def generate_pdf_report(self):
|
||||
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(self)
|
||||
return self.env.ref('offer_letters.hr_offer_letters_employee_print').report_action(self)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,5 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_offer_letter_user,offer.letter.user,model_offer_letter,base.group_user,1,1,1,0
|
||||
access_offer_letter_manager,offer.letter.manager,model_offer_letter,hr.group_hr_manager,1,1,1,1
|
||||
access_applicant_offer_mail_wizard,applicant.offer.mail.wizard.user,offer_letters.model_applicant_offer_mail_wizard,base.group_user,1,1,1,1
|
||||
access_offer_release_request_wizard,offer.release.request.wizard.user,model_offer_release_request_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_applicant_offer_letter_status" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.offer.letter</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//widget[@name='web_ribbon']" position="before">
|
||||
<field name="offer_release_status" invisible="1"/>
|
||||
<widget name="web_ribbon" title="Offer Requested" bg_color="text-bg-warning"
|
||||
invisible="offer_release_status != 'requested'"/>
|
||||
<widget name="web_ribbon" title="Offer Released" bg_color="text-bg-info"
|
||||
invisible="offer_release_status != 'sent'"/>
|
||||
<widget name="web_ribbon" title="Offer Accepted" bg_color="text-bg-success"
|
||||
invisible="offer_release_status != 'accepted'"/>
|
||||
<widget name="web_ribbon" title="Offer Rejected" bg_color="text-bg-danger"
|
||||
invisible="offer_release_status != 'rejected'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_applicant_view_form_offer_letters_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.offer.letters.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_share_applicant']" position="before">
|
||||
<button name="action_request_offer_release" string="Request Offer Release" type="object" class="btn-primary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"
|
||||
invisible="not id"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='request_forms']" position="after">
|
||||
<page string="Offer Letters" name="offer_letters">
|
||||
<group>
|
||||
<field name="finalized_ctc"/>
|
||||
<field name="current_offer_letter_id" readonly="1"/>
|
||||
<field name="offer_release_status" widget="badge" readonly="1"/>
|
||||
</group>
|
||||
<field name="offer_letter_ids" nolabel="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="requested_by_id"/>
|
||||
<field name="request_date"/>
|
||||
<field name="salary"/>
|
||||
<field name="state"/>
|
||||
<field name="sent_date"/>
|
||||
<field name="response_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_view_kanban_offer_letters_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.kanban.offer.letters.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_kanban_view_applicant"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="inside">
|
||||
<field name="offer_release_status"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']/widget[@name='web_ribbon']" position="after">
|
||||
<widget name="web_ribbon" title="Offer Req" bg_color="text-bg-warning"
|
||||
invisible="offer_release_status != 'requested'"/>
|
||||
<widget name="web_ribbon" title="Offer Rel" bg_color="text-bg-info"
|
||||
invisible="offer_release_status != 'sent'"/>
|
||||
<widget name="web_ribbon" title="Offer Accept" bg_color="text-bg-success"
|
||||
invisible="offer_release_status != 'accepted'"/>
|
||||
<widget name="web_ribbon" title="Offer Rej" bg_color="text-bg-danger"
|
||||
invisible="offer_release_status != 'rejected'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_candidate_view_kanban_offer_letters_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.kanban.offer.letters.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_candidate_view_kanban_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="inside">
|
||||
<field name="offer_release_status"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']" position="inside">
|
||||
<div class="mb-2" t-if="record.offer_release_status.raw_value == 'requested'">
|
||||
<span class="badge text-bg-warning">Offer Requested</span>
|
||||
</div>
|
||||
<div class="mb-2" t-if="record.offer_release_status.raw_value == 'sent'">
|
||||
<span class="badge text-bg-info">Offer Released</span>
|
||||
</div>
|
||||
<div class="mb-2" t-if="record.offer_release_status.raw_value == 'accepted'">
|
||||
<span class="badge text-bg-success">Offer Accepted</span>
|
||||
</div>
|
||||
<div class="mb-2" t-if="record.offer_release_status.raw_value == 'rejected'">
|
||||
<span class="badge text-bg-danger">Offer Rejected</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_offer_letters_list" name="Offer Letters" parent="hr_ftp.menu_hr_recruitment_hr" action="action_offer_letters"/>
|
||||
<menuitem id="menu_offer_letters_list" sequence="10" name="Offer Letters" parent="hr_payroll.menu_hr_payroll_root" action="action_offer_letters"/>
|
||||
</odoo>
|
||||
|
|
@ -20,22 +20,26 @@
|
|||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_send_offer" type="object" string="Send Offer" class="oe_highlight" invisble="state != 'draft'"/>
|
||||
<button name="action_accept_offer" type="object" string="Accept Offer" invisble="state != 'sent'" class="oe_highlight"/>
|
||||
<button name="action_reject_offer" type="object" string="Reject Offer" invisble="state != 'sent'" class="oe_danger"/>
|
||||
<button name="get_paydetailed_lines" type="object" string="Get Data" invisble="state != 'sent'" class="oe_danger"/>
|
||||
<button name="action_open_send_offer_wizard" type="object" string="Send Offer" class="oe_highlight"
|
||||
invisible="state != 'requested'" groups="hr.group_hr_manager"/>
|
||||
<button name="action_accept_offer" type="object" string="Accept Offer" invisible="state != 'sent'" class="oe_highlight"/>
|
||||
<!-- <button name="action_open_reject_wizard" type="object" string="Reject Offer" invisible="state != 'sent'" class="oe_danger"/>-->
|
||||
<button name="get_paydetailed_lines" type="object" string="Get Data" class="oe_danger"/>
|
||||
<button name="generate_pdf_report" type="object" string="Generate PDF" class="oe_highlight"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,sent,accepted,rejected,expired"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="requested,sent,accepted,rejected,expired"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" readonly="state != 'draft'"/>
|
||||
<field name="name" readonly="state != 'requested'"/>
|
||||
<field name="candidate_id"/>
|
||||
<field name="requested_by_id" readonly="1"/>
|
||||
<field name="request_date" readonly="1"/>
|
||||
<field name="manager_id"/>
|
||||
<field name="position"/>
|
||||
<field name="salary"/>
|
||||
<field name="mi"/>
|
||||
<field name="rejection_reason" readonly="state != 'rejected'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id"/>
|
||||
|
|
@ -43,6 +47,8 @@
|
|||
<field name="contract_type"/>
|
||||
<field name="probation_period"/>
|
||||
<field name="pay_struct_id"/>
|
||||
<field name="sent_date" readonly="1"/>
|
||||
<field name="response_date" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
|
|
@ -61,4 +67,4 @@
|
|||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="offer_response_message">
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; background: #f5f7fa; margin: 0; padding: 40px;">
|
||||
<div style="max-width: 640px; margin: 0 auto; background: #ffffff; border-radius: 10px; padding: 32px; box-shadow: 0 10px 30px rgba(0,0,0,0.08);">
|
||||
<h2 style="margin-top: 0; color: #143d5d;">
|
||||
<t t-esc="title"/>
|
||||
</h2>
|
||||
<p style="font-size: 16px; color: #333333; line-height: 1.6;">
|
||||
<t t-esc="message"/>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="offer_reject_reason_page">
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; background: #f5f7fa; margin: 0; padding: 40px;">
|
||||
<div style="max-width: 720px; margin: 0 auto; background: #ffffff; border-radius: 10px; padding: 32px; box-shadow: 0 10px 30px rgba(0,0,0,0.08);">
|
||||
<h2 style="margin-top: 0; color: #143d5d;">Reject Offer</h2>
|
||||
<p style="font-size: 15px; color: #333333; line-height: 1.6;">
|
||||
Please let us know the reason for rejecting the offer for
|
||||
<strong><t t-esc="offer_letter.position"/></strong>.
|
||||
</p>
|
||||
<p t-if="error" style="color: #dc3545; font-size: 14px;">
|
||||
<t t-esc="error"/>
|
||||
</p>
|
||||
<form t-att-action="'/offer_letters/respond/%s/reject?token=%s' % (offer_letter.id, token)" method="post">
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label for="rejection_reason" style="display: block; font-weight: 600; margin-bottom: 8px; color: #143d5d;">Rejection Reason</label>
|
||||
<textarea id="rejection_reason" name="rejection_reason" rows="6"
|
||||
style="width: 100%; padding: 12px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px;"
|
||||
placeholder="Please enter your reason here"></textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
style="background: #dc3545; color: #ffffff; border: none; padding: 12px 18px; border-radius: 6px; font-size: 14px; cursor: pointer;">
|
||||
Submit Rejection
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import applicant_offer_mail_wizard
|
||||
from . import offer_release_request_wizard
|
||||
from . import offer_letter_reject_wizard
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import base64
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ApplicantOfferMailWizard(models.TransientModel):
|
||||
_name = 'applicant.offer.mail.wizard'
|
||||
_description = 'Applicant Offer Mail Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string='Applicant', required=True, readonly=True)
|
||||
offer_letter_id = fields.Many2one('offer.letter', string='Offer Letter', readonly=True)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template', required=True, readonly=True)
|
||||
generated_attachment_id = fields.Many2one('ir.attachment', string='Generated Offer Attachment', readonly=True)
|
||||
|
||||
position = fields.Char(string='Position', required=True)
|
||||
salary = fields.Float(string='Annual CTC', required=True)
|
||||
joining_date = fields.Date(string='Joining Date', required=True)
|
||||
pay_struct_id = fields.Many2one('hr.payroll.structure', string='Salary Structure', required=True)
|
||||
manager_id = fields.Many2one('hr.employee', string='Manager')
|
||||
contract_type = fields.Selection([
|
||||
('permanent', 'Permanent'),
|
||||
('contract', 'Fixed Term Contract'),
|
||||
('intern', 'Internship'),
|
||||
], string='Contract Type', default='permanent', required=True)
|
||||
probation_period = fields.Integer(string='Probation Period (months)', default=3, required=True)
|
||||
|
||||
email_from = fields.Char('Email From', required=True)
|
||||
email_to = fields.Char('Email To', required=True)
|
||||
email_cc = fields.Text('Email CC')
|
||||
email_subject = fields.Char('Subject', required=True)
|
||||
email_body = fields.Html(
|
||||
'Body',
|
||||
render_engine='qweb',
|
||||
render_options={'post_process': True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize='email_outgoing',
|
||||
required=True,
|
||||
)
|
||||
attachment_ids = fields.Many2many('ir.attachment', string='Attachments')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
offer_letter = self._get_offer_letter_from_context()
|
||||
applicant = offer_letter.candidate_id if offer_letter else self._get_applicant()
|
||||
template = self.env.ref('offer_letters.applicant_offer_email_template', raise_if_not_found=False)
|
||||
offer_letter = offer_letter or self._create_offer_letter(applicant)
|
||||
|
||||
defaults.update({
|
||||
'applicant_id': applicant.id,
|
||||
'offer_letter_id': offer_letter.id,
|
||||
'position': offer_letter.position,
|
||||
'salary': offer_letter.salary,
|
||||
'joining_date': offer_letter.joining_date,
|
||||
'pay_struct_id': offer_letter.pay_struct_id.id,
|
||||
'manager_id': offer_letter.manager_id.id,
|
||||
'contract_type': offer_letter.contract_type,
|
||||
'probation_period': offer_letter.probation_period,
|
||||
})
|
||||
if template:
|
||||
defaults['template_id'] = template.id
|
||||
defaults.update(self._prepare_mail_defaults(template, offer_letter))
|
||||
return defaults
|
||||
|
||||
def _get_applicant(self):
|
||||
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||
if not applicant.exists():
|
||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
||||
return applicant
|
||||
|
||||
def _get_offer_letter_from_context(self):
|
||||
if self.env.context.get('active_model') != 'offer.letter':
|
||||
return self.env['offer.letter']
|
||||
offer_letter = self.env['offer.letter'].browse(self.env.context.get('active_id'))
|
||||
if not offer_letter.exists():
|
||||
raise UserError(_("The offer letter does not exist or is not accessible."))
|
||||
return offer_letter
|
||||
|
||||
def _get_default_pay_structure(self, applicant):
|
||||
company = applicant.company_id or self.env.company
|
||||
return self.env['hr.payroll.structure'].search([
|
||||
'|',
|
||||
('company_id', '=', company.id),
|
||||
('company_id', '=', False),
|
||||
], limit=1)
|
||||
|
||||
def _get_default_manager(self, applicant):
|
||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
||||
|
||||
def _create_offer_letter(self, applicant):
|
||||
pay_structure = self._get_default_pay_structure(applicant)
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before sending an offer."))
|
||||
|
||||
offer_letter = self.env['offer.letter'].create({
|
||||
'candidate_id': applicant.id,
|
||||
'position': applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.partner_name or applicant.display_name,
|
||||
'salary': applicant.finalized_ctc or applicant.salary_expected or applicant.current_ctc or 0.0,
|
||||
'joining_date': fields.Date.today(),
|
||||
'pay_struct_id': pay_structure.id,
|
||||
'manager_id': self._get_default_manager(applicant).id,
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
return offer_letter
|
||||
|
||||
def _update_offer_letter(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
'candidate_id': self.applicant_id.id,
|
||||
'position': self.position,
|
||||
'salary': self.salary,
|
||||
'joining_date': self.joining_date,
|
||||
'pay_struct_id': self.pay_struct_id.id,
|
||||
'manager_id': self.manager_id.id,
|
||||
'contract_type': self.contract_type,
|
||||
'probation_period': self.probation_period,
|
||||
}
|
||||
self.offer_letter_id.write(vals)
|
||||
self.offer_letter_id.get_paydetailed_lines()
|
||||
return self.offer_letter_id
|
||||
|
||||
def _generate_offer_attachment(self, offer_letter):
|
||||
report = self.env.ref('offer_letters.hr_offer_letters_employee_print')
|
||||
from odoo import _ as translate
|
||||
pdf_content, _ = report.sudo()._render_qweb_pdf(report, offer_letter.id)
|
||||
attachment_name = translate('Offer Letter - %s.pdf') % (offer_letter.candidate_id.partner_name or offer_letter.name)
|
||||
return self.env['ir.attachment'].create({
|
||||
'name': attachment_name,
|
||||
'datas': base64.b64encode(pdf_content),
|
||||
'mimetype': 'application/pdf',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id or 0,
|
||||
'type': 'binary',
|
||||
})
|
||||
|
||||
def _prepare_mail_defaults(self, template, offer_letter):
|
||||
response_token = offer_letter._issue_response_token()
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
accept_url = f"{base_url}/offer_letters/respond/{offer_letter.id}/accept?token={response_token}"
|
||||
reject_url = f"{base_url}/offer_letters/respond/{offer_letter.id}/reject?token={response_token}"
|
||||
render_results = template.with_context(
|
||||
offer_accept_url=accept_url,
|
||||
offer_reject_url=reject_url,
|
||||
)._generate_template(
|
||||
[offer_letter.id],
|
||||
['subject', 'body_html', 'email_from', 'email_to', 'email_cc', 'reply_to'],
|
||||
find_or_create_partners=False,
|
||||
)
|
||||
generated_values = render_results.get(offer_letter.id, {})
|
||||
attachment = self._generate_offer_attachment(offer_letter)
|
||||
other_attachments = self.env['ir.attachment']
|
||||
previous_attachment = self.env['ir.attachment']
|
||||
if self.ids:
|
||||
previous_attachment = self.generated_attachment_id
|
||||
other_attachments = self.attachment_ids - previous_attachment
|
||||
attachment_ids = (other_attachments | attachment).ids if attachment else other_attachments.ids
|
||||
|
||||
return {
|
||||
'email_from': generated_values.get('email_from') or offer_letter.candidate_id.user_id.email or self.env.user.email,
|
||||
'email_to': generated_values.get('email_to') or offer_letter.candidate_id.email_from or '',
|
||||
'email_cc': generated_values.get('email_cc', ''),
|
||||
'email_subject': generated_values.get('subject', ''),
|
||||
'email_body': generated_values.get('body_html', ''),
|
||||
'generated_attachment_id': attachment.id if attachment else False,
|
||||
'attachment_ids': [(6, 0, attachment_ids)],
|
||||
}
|
||||
|
||||
def action_generate_offer(self):
|
||||
self.ensure_one()
|
||||
offer_letter = self._update_offer_letter()
|
||||
previous_attachment = self.generated_attachment_id
|
||||
self.write(self._prepare_mail_defaults(self.template_id, offer_letter))
|
||||
if previous_attachment:
|
||||
previous_attachment.unlink()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Send Offer'),
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'view_id': self.env.ref('offer_letters.view_applicant_offer_mail_wizard_form').id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_preview_offer_attachment(self):
|
||||
self.ensure_one()
|
||||
if not self.generated_attachment_id:
|
||||
raise UserError(_("Please generate the offer first to preview the latest PDF."))
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/web/content/%s?download=false' % self.generated_attachment_id.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_send_offer(self):
|
||||
self.ensure_one()
|
||||
if not self.env.user.has_group('hr.group_hr_manager'):
|
||||
raise UserError(_("Only HR can send the offer to the applicant."))
|
||||
offer_letter = self._update_offer_letter()
|
||||
previous_attachment = self.generated_attachment_id
|
||||
refreshed_values = self._prepare_mail_defaults(self.template_id, offer_letter)
|
||||
self.write(refreshed_values)
|
||||
if previous_attachment:
|
||||
previous_attachment.unlink()
|
||||
attachment_ids = self.attachment_ids.ids
|
||||
offer_attachment = self.generated_attachment_id
|
||||
if offer_attachment.id not in attachment_ids:
|
||||
attachment_ids.append(offer_attachment.id)
|
||||
|
||||
mail = self.env['mail.mail'].create({
|
||||
'email_from': self.email_from,
|
||||
'email_to': self.email_to,
|
||||
'email_cc': self.email_cc,
|
||||
'subject': self.email_subject,
|
||||
'body_html': self.email_body,
|
||||
'attachment_ids': [(6, 0, attachment_ids)],
|
||||
'auto_delete': False,
|
||||
'model': 'offer.letter',
|
||||
'res_id': offer_letter.id,
|
||||
})
|
||||
mail.send()
|
||||
offer_letter.action_send_offer()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_applicant_offer_mail_wizard_form" model="ir.ui.view">
|
||||
<field name="name">applicant.offer.mail.wizard.form</field>
|
||||
<field name="model">applicant.offer.mail.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Send Offer">
|
||||
<group>
|
||||
<field name="applicant_id" readonly="1"/>
|
||||
<field name="template_id" options="{'no_create': True}" readonly="1"/>
|
||||
<field name="generated_attachment_id" invisible="1"/>
|
||||
</group>
|
||||
<group string="Offer Details" col="2">
|
||||
<field name="position"/>
|
||||
<field name="salary"/>
|
||||
<field name="joining_date"/>
|
||||
<field name="pay_struct_id" options="{'no_create': True}"/>
|
||||
<field name="manager_id"/>
|
||||
<field name="contract_type"/>
|
||||
<field name="probation_period"/>
|
||||
</group>
|
||||
<button name="action_generate_offer" type="object" string="Generate Offer" class="btn-secondary"/>
|
||||
<group string="Email Details">
|
||||
<field name="email_from" placeholder="Sender email"/>
|
||||
<field name="email_to" placeholder="Recipient email"/>
|
||||
<field name="email_cc" placeholder="Comma-separated CC recipients"/>
|
||||
<field name="email_subject" options="{'dynamic_placeholder': true}" placeholder="Email subject"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Body">
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor"
|
||||
options="{'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
</page>
|
||||
<page string="Attachments">
|
||||
<field name="attachment_ids" widget="many2many_binary"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button name="action_preview_offer_attachment" type="object" string="Preview Offer Letter"
|
||||
class="btn-secondary" invisible="not generated_attachment_id"/>
|
||||
<button name="action_send_offer" type="object" string="Send Offer" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class OfferLetterRejectWizard(models.TransientModel):
|
||||
_name = 'offer.letter.reject.wizard'
|
||||
_description = 'Offer Letter Reject Wizard'
|
||||
|
||||
offer_letter_id = fields.Many2one('offer.letter', string='Offer Letter', required=True, readonly=True)
|
||||
rejection_reason = fields.Text(string='Rejection Reason', required=True)
|
||||
|
||||
def action_confirm_reject(self):
|
||||
self.ensure_one()
|
||||
self.offer_letter_id.action_reject_offer(self.rejection_reason)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_offer_letter_reject_wizard_form" model="ir.ui.view">
|
||||
<field name="name">offer.letter.reject.wizard.form</field>
|
||||
<field name="model">offer.letter.reject.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reject Offer">
|
||||
<group>
|
||||
<field name="offer_letter_id" readonly="1"/>
|
||||
<field name="rejection_reason" placeholder="Enter rejection reason"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_confirm_reject" type="object" string="Reject Offer" class="btn-danger"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class OfferReleaseRequestWizard(models.TransientModel):
|
||||
_name = 'offer.release.request.wizard'
|
||||
_description = 'Offer Release Request Wizard'
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string='Applicant', required=True, readonly=True)
|
||||
position = fields.Char(string='Position', readonly=True)
|
||||
finalized_ctc = fields.Float(string='Finalized CTC', required=True)
|
||||
email_from = fields.Char(string='Email From', required=True)
|
||||
email_to = fields.Char(string='Mail To', required=True)
|
||||
email_cc = fields.Text(string='Mail CC')
|
||||
email_subject = fields.Char(string='Subject', required=True)
|
||||
email_body = fields.Html(
|
||||
string='Mail Body',
|
||||
render_engine='qweb',
|
||||
render_options={'post_process': True},
|
||||
prefetch=True,
|
||||
translate=True,
|
||||
sanitize='email_outgoing',
|
||||
required=True,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
applicant = self.env['hr.applicant'].browse(
|
||||
self.env.context.get('default_applicant_id') or self.env.context.get('active_id')
|
||||
)
|
||||
if not applicant.exists():
|
||||
raise UserError(_("The applicant does not exist or is not accessible."))
|
||||
|
||||
recruiter_name = applicant.user_id.name or self.env.user.name
|
||||
candidate_name = applicant.partner_name or applicant.candidate_id.partner_name or applicant.display_name
|
||||
position = applicant.job_id.name or applicant.hr_job_recruitment.job_id.name or applicant.display_name
|
||||
finalized_ctc = applicant.finalized_ctc or applicant.salary_expected or applicant.current_ctc or 0.0
|
||||
|
||||
defaults.update({
|
||||
'applicant_id': applicant.id,
|
||||
'position': position,
|
||||
'finalized_ctc': finalized_ctc,
|
||||
'email_from': self.env.user.email or self.env.company.email or '',
|
||||
'email_to': self._get_hr_email_to(),
|
||||
'email_subject': _('Offer Release Request - %s') % candidate_name,
|
||||
'email_body': (
|
||||
f"<p>Dear HR Team,</p>"
|
||||
f"<p>Please release the offer letter for <strong>{candidate_name}</strong>.</p>"
|
||||
f"<p><strong>Position:</strong> {position}<br/>"
|
||||
f"<strong>Finalized CTC:</strong> {finalized_ctc:.2f}<br/>"
|
||||
f"<strong>Requested By:</strong> {recruiter_name}</p>"
|
||||
f"<p>Please review the request and release the offer letter to the applicant.</p>"
|
||||
),
|
||||
})
|
||||
return defaults
|
||||
|
||||
def _get_hr_email_to(self):
|
||||
hr_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_hr_id')
|
||||
hr_manager_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False
|
||||
users = hr_manager_id
|
||||
if not hr_id:
|
||||
group = self.env.ref('hr.group_hr_manager')
|
||||
users = self.env['res.users'].sudo().search([
|
||||
('groups_id', 'in', group.ids),
|
||||
('email', '!=', False),
|
||||
])
|
||||
emails = users.mapped('email')
|
||||
return ','.join(emails) or self.env.company.email or ''
|
||||
|
||||
def _get_default_pay_structure(self, applicant):
|
||||
company = applicant.company_id or self.env.company
|
||||
return self.env['hr.payroll.structure'].search([
|
||||
], limit=1)
|
||||
|
||||
def _get_default_manager(self, applicant):
|
||||
return applicant.user_id.employee_id or self.env.user.employee_id
|
||||
|
||||
def action_submit_request(self):
|
||||
self.ensure_one()
|
||||
applicant = self.applicant_id
|
||||
pay_structure = self._get_default_pay_structure(applicant)
|
||||
if not pay_structure:
|
||||
raise UserError(_("Please configure at least one salary structure before requesting an offer release."))
|
||||
|
||||
applicant.finalized_ctc = self.finalized_ctc
|
||||
offer_letter = self.env['offer.letter'].create({
|
||||
'candidate_id': applicant.id,
|
||||
'position': self.position,
|
||||
'salary': self.finalized_ctc,
|
||||
'joining_date': fields.Date.today(),
|
||||
'pay_struct_id': pay_structure.id,
|
||||
'manager_id': self._get_default_manager(applicant).id,
|
||||
'requested_by_id': self.env.user.id,
|
||||
'request_date': fields.Datetime.now(),
|
||||
'state': 'requested',
|
||||
})
|
||||
offer_letter.get_paydetailed_lines()
|
||||
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
offer_url = f"{base_url}/web#id={offer_letter.id}&model=offer.letter&view_type=form"
|
||||
body_html = (
|
||||
f"{self.email_body}"
|
||||
f"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>"
|
||||
)
|
||||
|
||||
mail = self.env['mail.mail'].create({
|
||||
'email_from': self.email_from,
|
||||
'email_to': self.email_to,
|
||||
'email_cc': self.email_cc,
|
||||
'subject': self.email_subject,
|
||||
'body_html': body_html,
|
||||
'auto_delete': False,
|
||||
'model': 'offer.letter',
|
||||
'res_id': offer_letter.id,
|
||||
})
|
||||
mail.send()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_offer_release_request_wizard_form" model="ir.ui.view">
|
||||
<field name="name">offer.release.request.wizard.form</field>
|
||||
<field name="model">offer.release.request.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Request Offer Release">
|
||||
<group>
|
||||
<field name="applicant_id" readonly="1"/>
|
||||
<field name="position" readonly="1"/>
|
||||
<field name="finalized_ctc"/>
|
||||
</group>
|
||||
<group string="Email Details">
|
||||
<field name="email_from" placeholder="Sender email"/>
|
||||
<field name="email_to" placeholder="Recipient email(s)"/>
|
||||
<field name="email_cc" placeholder="Comma-separated CC recipients"/>
|
||||
<field name="email_subject" placeholder="Email subject"/>
|
||||
</group>
|
||||
<group string="Mail Body">
|
||||
<field name="email_body" widget="html_mail" class="oe-bordered-editor"
|
||||
options="{'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_submit_request" type="object" string="Submit Request" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
# Task Document: Project by Module
|
||||
|
||||
## Requirement
|
||||
|
||||
Add a **Project by Module** submenu in the Odoo **Project** menu.
|
||||
|
||||
The menu should show **Project records grouped by Module**, so users can easily view projects module-wise.
|
||||
|
||||
## Understanding
|
||||
|
||||
Initially, the addon already had a `Module` field on `project.task`:
|
||||
|
||||
```python
|
||||
model_id = fields.Many2one('project.module.source', string="Module")
|
||||
```
|
||||
|
||||
But the requirement is to show **projects by module**, not tasks by module. Since `project.project` did not have a module field, a new field was added on the Project model.
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
The implementation adds:
|
||||
|
||||
- A `module_id` field on `project.project`
|
||||
- A Module field on the Project form
|
||||
- A Project search group-by filter for Module
|
||||
- A new Project menu item named **Project by Module**
|
||||
- A new action that opens `project.project` grouped by `module_id`
|
||||
- Manifest entry to load the new XML file
|
||||
|
||||
## Files Changed
|
||||
|
||||
### 1. `models/project.py`
|
||||
|
||||
Added a new Many2one field on Project:
|
||||
|
||||
```python
|
||||
module_id = fields.Many2one('project.module.source', string="Module")
|
||||
```
|
||||
|
||||
Location:
|
||||
|
||||
```python
|
||||
sequence_name = fields.Char("Project Number", copy=False, readonly=True)
|
||||
module_id = fields.Many2one('project.module.source', string="Module")
|
||||
task_sequence_id = fields.Many2one(
|
||||
'ir.sequence',
|
||||
string="Task Sequence",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Sequence for tasks of this project"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. `view/project_by_module.xml`
|
||||
|
||||
Created a new XML file to handle form view, search view, action, and menu.
|
||||
|
||||
Full code:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="project_project_form_module_inherit" model="ir.ui.view">
|
||||
<field name="name">project.project.form.module.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='project_lead']" position="after">
|
||||
<field name="module_id" options="{'no_open': True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_project_search_project_by_module" model="ir.ui.view">
|
||||
<field name="name">project.project.search.project.by.module</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_project_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//search" position="inside">
|
||||
<filter name="group_by_module" string="Module" context="{'group_by': 'module_id'}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_by_module" model="ir.actions.act_window">
|
||||
<field name="name">Project by Module</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="view_mode">kanban,list,form,calendar,activity</field>
|
||||
<field name="search_view_id" ref="project.view_project_project_filter"/>
|
||||
<field name="context">{'search_default_group_by_module': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No projects found.
|
||||
</p>
|
||||
<p>
|
||||
Create projects and set their Module to review projects module-wise.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_project_by_module"
|
||||
name="Project by Module"
|
||||
parent="project.menu_main_pm"
|
||||
action="action_project_by_module"
|
||||
sequence="25"/>
|
||||
</data>
|
||||
</odoo>
|
||||
```
|
||||
|
||||
### 3. `__manifest__.py`
|
||||
|
||||
Added the new XML file in the data list:
|
||||
|
||||
```python
|
||||
'view/project_by_module.xml',
|
||||
```
|
||||
|
||||
Location:
|
||||
|
||||
```python
|
||||
'view/project_task.xml',
|
||||
'view/project.xml',
|
||||
'view/project_portfolio.xml',
|
||||
'view/project_by_module.xml',
|
||||
'view/timesheets.xml',
|
||||
```
|
||||
|
||||
## Expected Result
|
||||
|
||||
After upgrading the module:
|
||||
|
||||
1. Open a Project record.
|
||||
2. Set the **Module** field.
|
||||
3. Go to **Project > Project by Module**.
|
||||
4. Odoo opens Project records grouped module-wise.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
PMT
|
||||
Project A
|
||||
Project B
|
||||
|
||||
Finance
|
||||
Project C
|
||||
|
||||
ATS
|
||||
Project D
|
||||
```
|
||||
|
||||
## Module Upgrade Command
|
||||
|
||||
Use the appropriate database name:
|
||||
|
||||
```bash
|
||||
odoo-bin -d your_database -u project_task_timesheet_extended
|
||||
```
|
||||
|
||||
## Validation Done
|
||||
|
||||
- XML syntax checked successfully.
|
||||
- Python syntax checked successfully using `compile()`.
|
||||
- `py_compile` could not write bytecode because Windows denied write access to `models/__pycache__`, but the Python syntax itself is valid.
|
||||
|
||||
## Additional Update: Portfolio on Project Card and Module on Task
|
||||
|
||||
After the initial Project by Module work, two display changes were added.
|
||||
|
||||
### 1. Portfolio visible on Project Kanban card
|
||||
|
||||
Project records already have `portfolio_id`. The project kanban card was extended to show the portfolio name under the project title/metadata area.
|
||||
|
||||
Code added in `view/project_by_module.xml`:
|
||||
|
||||
```xml
|
||||
<record id="project_project_kanban_portfolio_inherit" model="ir.ui.view">
|
||||
<field name="name">project.project.kanban.portfolio.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban/templates" position="before">
|
||||
<field name="portfolio_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//span[@name='partner_name']" position="after">
|
||||
<span name="portfolio_name" class="text-muted d-flex align-items-baseline" t-if="record.portfolio_id.value">
|
||||
<span class="fa fa-folder-open me-2" aria-label="Portfolio" title="Portfolio"></span>
|
||||
<field class="text-truncate" name="portfolio_id"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
### 2. Task Module comes from Project Module
|
||||
|
||||
The task module field now comes from the task's selected project, so the task automatically shows which module it belongs to.
|
||||
|
||||
Code changed in `models/project_task.py`:
|
||||
|
||||
```python
|
||||
model_id = fields.Many2one(
|
||||
'project.module.source',
|
||||
string="Module",
|
||||
related='project_id.module_id',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
```
|
||||
|
||||
The task form also displays the Module near the task title area and keeps the regular Module field read-only.
|
||||
|
||||
Code added in `view/project_by_module.xml`:
|
||||
|
||||
```xml
|
||||
<record id="project_task_form_project_module_display" model="ir.ui.view">
|
||||
<field name="name">project.task.form.project.module.display</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project_task_timesheet_extended.project_task_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="inside">
|
||||
<div class="text-muted mt-1" invisible="not model_id">
|
||||
<span>Module: </span>
|
||||
<field name="model_id" readonly="1" nolabel="1" options="{'no_open': True}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='model_id']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
<attribute name="options">{'no_open': True}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Project kanban cards show their Portfolio when assigned.
|
||||
- Task form shows the Module from the parent Project.
|
||||
- Users only need to set Module on Project; related tasks display it automatically.
|
||||
|
||||
### 3. Task ID and Module visible on Task Kanban card
|
||||
|
||||
The task kanban card was extended to show the generated task sequence and the module directly under the task title.
|
||||
|
||||
Code added in `view/project_task.xml`:
|
||||
|
||||
```xml
|
||||
<record id="project_task_kanban_task_id_module" model="ir.ui.view">
|
||||
<field name="name">project.task.kanban.task.id.module</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='subtask_count']" position="after">
|
||||
<field name="sequence_name"/>
|
||||
<field name="model_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']//field[@name='name']" position="after">
|
||||
<div class="text-muted mt-1 small" t-if="record.sequence_name.raw_value">
|
||||
<span>Task ID: </span>
|
||||
<field name="sequence_name"/>
|
||||
</div>
|
||||
<div class="text-muted small" t-if="record.model_id.raw_value">
|
||||
<span>Module: </span>
|
||||
<field name="model_id"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
Expected behavior on task kanban cards:
|
||||
|
||||
```text
|
||||
project task
|
||||
Task ID: PROJ-012/TASK-001
|
||||
Module: PMT
|
||||
```
|
||||
|
|
@ -14,4 +14,9 @@ class ResConfigSettings(models.TransientModel):
|
|||
requisition_finance_manager = fields.Many2one('res.users',config_parameter='requisitions.requisition_finance_manager_id', string='Requisition Finance Manager',
|
||||
domain=lambda self: [
|
||||
('groups_id', 'in',
|
||||
self.env.ref('account.group_account_manager').id)])
|
||||
self.env.ref('account.group_account_manager').id)])
|
||||
requisition_manager = fields.Many2one('res.users',config_parameter='requisitions.requisition_manager', string='Requisition Managers',
|
||||
domain=lambda self: [
|
||||
('groups_id', 'in',
|
||||
self.env.ref('hr_recruitment.group_hr_recruitment_manager').id)]
|
||||
)
|
||||
|
|
@ -17,7 +17,11 @@
|
|||
id="requisition_finance_access_control">
|
||||
<field name="requisition_finance_manager" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
|
||||
<setting string="Requisition Manager Access"
|
||||
help="Select the Manager responsible for Requisitions."
|
||||
id="requisition_manager_access_control">
|
||||
<field name="requisition_manager" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="universal_attachment_preview.PopupPreview">
|
||||
<Dialog title="props.filename" size="'lg'">
|
||||
<p class="text-muted mt-2">
|
||||
<t t-esc="props.mimetype"/>
|
||||
</p>
|
||||
<Dialog title="props.filename + ' (' + props.mimetype + ')'" size="'lg'">
|
||||
<!-- <p class="text-muted mt-2">-->
|
||||
<!-- <t t-esc="props.mimetype"/>-->
|
||||
<!-- </p>-->
|
||||
|
||||
<t t-if="props.mimetype.endsWith('pdf')">
|
||||
<iframe t-att-src="props.url"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
from . import models, wizards
|
||||
|
||||
|
||||
def post_init_remove_duplicate_skills(env):
|
||||
|
||||
# =====================================================
|
||||
# STEP 1 : Ensure every skill type has levels
|
||||
# =====================================================
|
||||
|
||||
skill_types = env['hr.skill.type'].search([])
|
||||
|
||||
for skill_type in skill_types:
|
||||
|
||||
# -------------------------------------------------
|
||||
# Create level if no levels exist
|
||||
# -------------------------------------------------
|
||||
|
||||
if not skill_type.skill_level_ids:
|
||||
|
||||
env['hr.skill.level'].create({
|
||||
'name': 'Beginner',
|
||||
'level_progress': 10,
|
||||
'default_level': True,
|
||||
'skill_type_id': skill_type.id,
|
||||
})
|
||||
|
||||
# -------------------------------------------------
|
||||
# Ensure one default level exists
|
||||
# -------------------------------------------------
|
||||
|
||||
elif not skill_type.skill_level_ids.filtered('default_level'):
|
||||
|
||||
skill_type.skill_level_ids[0].default_level = True
|
||||
|
||||
# =====================================================
|
||||
# STEP 2 : Fix invalid candidate skill records
|
||||
# =====================================================
|
||||
|
||||
candidate_skill_lines = env['hr.candidate.skill'].search([])
|
||||
|
||||
for line in candidate_skill_lines:
|
||||
|
||||
# -------------------------------------------------
|
||||
# Skip invalid records
|
||||
# -------------------------------------------------
|
||||
|
||||
if not line.skill_id:
|
||||
continue
|
||||
|
||||
# -------------------------------------------------
|
||||
# Fix wrong skill type
|
||||
# -------------------------------------------------
|
||||
|
||||
correct_skill_type = line.skill_id.skill_type_id
|
||||
|
||||
# -------------------------------------------------
|
||||
# Ensure skill type has levels
|
||||
# -------------------------------------------------
|
||||
|
||||
levels = correct_skill_type.skill_level_ids
|
||||
|
||||
if not levels:
|
||||
|
||||
level = env['hr.skill.level'].create({
|
||||
'name': 'Beginner',
|
||||
'level_progress': 10,
|
||||
'default_level': True,
|
||||
'skill_type_id': correct_skill_type.id,
|
||||
})
|
||||
|
||||
levels = level
|
||||
|
||||
# -------------------------------------------------
|
||||
# Ensure default level exists
|
||||
# -------------------------------------------------
|
||||
|
||||
default_level = levels.filtered(
|
||||
lambda l: l.default_level
|
||||
)[:1]
|
||||
|
||||
if not default_level:
|
||||
|
||||
default_level = levels[0]
|
||||
default_level.default_level = True
|
||||
|
||||
# -------------------------------------------------
|
||||
# Update all fields together
|
||||
# -------------------------------------------------
|
||||
|
||||
line.write({
|
||||
'skill_type_id': correct_skill_type.id,
|
||||
'skill_level_id': default_level.id,
|
||||
})
|
||||
|
||||
# =====================================================
|
||||
# STEP 3 : Remove duplicate skills
|
||||
# =====================================================
|
||||
|
||||
skills = env['hr.skill'].search([
|
||||
('active', 'in', [True, False])
|
||||
])
|
||||
|
||||
processed_skills = {}
|
||||
|
||||
for skill in skills:
|
||||
|
||||
normalized_name = skill.name.strip().lower()
|
||||
|
||||
# -------------------------------------------------
|
||||
# Keep first occurrence
|
||||
# -------------------------------------------------
|
||||
|
||||
if normalized_name not in processed_skills:
|
||||
|
||||
processed_skills[normalized_name] = skill
|
||||
continue
|
||||
|
||||
original_skill = processed_skills[normalized_name]
|
||||
|
||||
duplicate_skill = skill
|
||||
|
||||
# -------------------------------------------------
|
||||
# Ensure original skill type has levels
|
||||
# -------------------------------------------------
|
||||
|
||||
levels = original_skill.skill_type_id.skill_level_ids
|
||||
|
||||
if not levels:
|
||||
|
||||
level = env['hr.skill.level'].create({
|
||||
'name': 'Beginner',
|
||||
'level_progress': 10,
|
||||
'default_level': True,
|
||||
'skill_type_id': original_skill.skill_type_id.id,
|
||||
})
|
||||
|
||||
levels = level
|
||||
|
||||
# -------------------------------------------------
|
||||
# Ensure default level exists
|
||||
# -------------------------------------------------
|
||||
|
||||
default_level = levels.filtered(
|
||||
lambda l: l.default_level
|
||||
)[:1]
|
||||
|
||||
if not default_level:
|
||||
|
||||
default_level = levels[0]
|
||||
default_level.default_level = True
|
||||
|
||||
# =================================================
|
||||
# Update hr.candidate.skill lines
|
||||
# =================================================
|
||||
|
||||
duplicate_lines = env['hr.candidate.skill'].search([
|
||||
('skill_id', '=', duplicate_skill.id)
|
||||
])
|
||||
|
||||
for line in duplicate_lines:
|
||||
|
||||
# ---------------------------------------------
|
||||
# Update all fields together
|
||||
# ---------------------------------------------
|
||||
|
||||
line.write({
|
||||
'skill_id': original_skill.id,
|
||||
'skill_type_id': original_skill.skill_type_id.id,
|
||||
'skill_level_id': default_level.id,
|
||||
})
|
||||
|
||||
# =================================================
|
||||
# Update quick_skill_ids in hr.candidate
|
||||
# =================================================
|
||||
|
||||
candidates = env['hr.candidate'].search([
|
||||
('quick_skill_ids', 'in', duplicate_skill.id)
|
||||
])
|
||||
|
||||
for candidate in candidates:
|
||||
|
||||
updated_skill_ids = []
|
||||
|
||||
for skill_id in candidate.quick_skill_ids.ids:
|
||||
|
||||
if skill_id == duplicate_skill.id:
|
||||
updated_skill_ids.append(original_skill.id)
|
||||
else:
|
||||
updated_skill_ids.append(skill_id)
|
||||
|
||||
# Remove duplicate ids
|
||||
updated_skill_ids = list(set(updated_skill_ids))
|
||||
|
||||
candidate.write({
|
||||
'quick_skill_ids': [(6, 0, updated_skill_ids)]
|
||||
})
|
||||
|
||||
# =================================================
|
||||
# Archive duplicate skill
|
||||
# =================================================
|
||||
|
||||
duplicate_skill.active = False
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Form Chages',
|
||||
'category': 'Website/Website',
|
||||
'sequence': 310,
|
||||
'version': '1.1',
|
||||
'summary': 'Changes of the form',
|
||||
'description': "This module contains the changes in the job portal, candidates, appliant form",
|
||||
'depends': ['hr_recruitment_extended', 'website_hr_recruitment','website_hr_recruitment_extended','survey','hr_recruitment'],
|
||||
'data': [
|
||||
'data/mail_template_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/hr_applicant_form.xml',
|
||||
'views/hr_candidate_form.xml',
|
||||
'views/job_portal_changes.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': {
|
||||
'web_portal_form_custom/static/src/css/candidate_card.css'
|
||||
},
|
||||
},
|
||||
'post_init_hook': 'post_init_remove_duplicate_skills',
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mail_template_applicant_interview_invite" model="mail.template">
|
||||
<field name="name">Applicant: Interview</field>
|
||||
<field name="model_id" ref="model_survey_user_input" />
|
||||
<field name="subject">Participate to {{ object.survey_id.display_name }} interview</field>
|
||||
<field name="email_to">{{ (object.partner_id.email_formatted or object.email) }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <t t-out="object.partner_id.name or 'applicant'">[applicant name]</t><br/><br/>
|
||||
<t>
|
||||
You've progressed through the recruitment process and we would like you to answer some questions.
|
||||
</t>
|
||||
<div style="margin: 16px 0px 16px 0px;">
|
||||
<a t-att-href="(object.get_start_url())"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
<t>
|
||||
Start the written interview
|
||||
</t>
|
||||
</a>
|
||||
</div>
|
||||
<t t-if="object.deadline">
|
||||
Please answer the interview for <t t-out="format_date(object.deadline)">[deadline date]</t>.<br/><br/>
|
||||
</t>
|
||||
<t>
|
||||
We wish you good luck! Thank you in advance for your participation.
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import application_candidate_changes
|
||||
from . import survey_line
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
from odoo import api, fields, models,_
|
||||
|
||||
|
||||
class ApplicantCandidate(models.Model):
|
||||
_inherit = 'hr.candidate'
|
||||
|
||||
quick_skill_ids = fields.Many2many(
|
||||
'hr.skill',
|
||||
'hr_candidate_quick_skill_rel',
|
||||
'candidate_id',
|
||||
'skill_id',
|
||||
compute='_compute_quick_skill_ids',
|
||||
inverse='_inverse_quick_skill_ids',
|
||||
store=True,
|
||||
domain=[('active', '=', True)],
|
||||
string="Skills",
|
||||
)
|
||||
|
||||
priority = fields.Selection(
|
||||
selection_add=[
|
||||
('4', 'Outstanding'),
|
||||
('5', 'Top Priority')
|
||||
]
|
||||
)
|
||||
resume = fields.Binary(
|
||||
string="Resume",
|
||||
attachment=True
|
||||
)
|
||||
|
||||
resume_name = fields.Char(
|
||||
string="Resume Name"
|
||||
)
|
||||
|
||||
resume_type = fields.Char(
|
||||
string="Resume Type"
|
||||
)
|
||||
|
||||
def action_open_resume_candidate(self):
|
||||
self.ensure_one()
|
||||
|
||||
if not self.resume:
|
||||
return
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/hr.candidate/{self.id}/resume/{self.resume_name}?download=false',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
@api.onchange('resume_name')
|
||||
def _onchange_resume_name(self):
|
||||
for rec in self:
|
||||
if rec.resume_name:
|
||||
ext = rec.resume_name.split('.')[-1].lower()
|
||||
|
||||
if ext == 'pdf':
|
||||
rec.resume_type = 'pdf'
|
||||
|
||||
elif ext in ['png', 'jpg', 'jpeg', 'gif']:
|
||||
rec.resume_type = 'image'
|
||||
|
||||
else:
|
||||
rec.resume_type = 'other'
|
||||
|
||||
@api.depends('candidate_skill_ids.skill_id')
|
||||
def _compute_quick_skill_ids(self):
|
||||
for rec in self:
|
||||
rec.quick_skill_ids = rec.candidate_skill_ids.mapped('skill_id')
|
||||
|
||||
def _inverse_quick_skill_ids(self):
|
||||
candidate_skill_model = self.env['hr.candidate.skill']
|
||||
for rec in self:
|
||||
desired_skills = rec.quick_skill_ids
|
||||
existing_lines = rec.candidate_skill_ids.filtered('skill_id')
|
||||
existing_skills = existing_lines.mapped('skill_id')
|
||||
|
||||
lines_to_remove = existing_lines.filtered(lambda line: line.skill_id not in desired_skills)
|
||||
if lines_to_remove:
|
||||
rec.candidate_skill_ids -= lines_to_remove
|
||||
|
||||
skills_to_add = desired_skills - existing_skills
|
||||
for skill in skills_to_add:
|
||||
skill_type = skill.skill_type_id
|
||||
if not skill_type:
|
||||
continue
|
||||
default_level = rec._get_default_skill_level(skill_type)
|
||||
if not default_level:
|
||||
continue
|
||||
candidate_skill_model.create({
|
||||
'candidate_id': rec.id,
|
||||
'skill_id': skill.id,
|
||||
'skill_type_id': skill_type.id,
|
||||
'skill_level_id': default_level.id,
|
||||
})
|
||||
|
||||
rec._cleanup_duplicate_candidate_skill_lines()
|
||||
|
||||
def _cleanup_duplicate_candidate_skill_lines(self):
|
||||
candidate_skill_model = self.env['hr.candidate.skill']
|
||||
for rec in self:
|
||||
seen_names = set()
|
||||
duplicate_lines = candidate_skill_model
|
||||
for line in rec.candidate_skill_ids.filtered(lambda l: l.skill_id and l.skill_id.name):
|
||||
skill_name = line.skill_id.name.strip().lower()
|
||||
if skill_name in seen_names:
|
||||
duplicate_lines += line
|
||||
continue
|
||||
seen_names.add(skill_name)
|
||||
if duplicate_lines:
|
||||
rec.candidate_skill_ids -= duplicate_lines
|
||||
|
||||
def _get_default_skill_level(self, skill_type):
|
||||
skill_type.ensure_one()
|
||||
default_level = skill_type.skill_level_ids.filtered('default_level')[:1]
|
||||
if default_level:
|
||||
return default_level
|
||||
if skill_type.skill_level_ids:
|
||||
skill_type.skill_level_ids[0].default_level = True
|
||||
return skill_type.skill_level_ids[0]
|
||||
return self.env['hr.skill.level'].create({
|
||||
'name': 'Beginner',
|
||||
'level_progress': 10,
|
||||
'default_level': True,
|
||||
'skill_type_id': skill_type.id,
|
||||
})
|
||||
|
||||
|
||||
class CandidateApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
|
||||
quick_skill_ids = fields.Many2many(
|
||||
related='candidate_id.quick_skill_ids',
|
||||
readonly=False,
|
||||
string="Skills"
|
||||
)
|
||||
|
||||
priority = fields.Selection(
|
||||
selection_add=[
|
||||
('4', 'Outstanding'),
|
||||
('5', 'Top Priority')
|
||||
]
|
||||
)
|
||||
applicant_history_ids = fields.One2many(
|
||||
related='candidate_id.applicant_ids',
|
||||
string='Applications',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
survey_line_ids = fields.One2many(
|
||||
'applicant.survey.line',
|
||||
'applicant_id',
|
||||
string="Online Tests"
|
||||
)
|
||||
|
||||
certificate_ids = fields.One2many(
|
||||
'applicant.certificate',
|
||||
'applicant_id',
|
||||
string='Certificates'
|
||||
)
|
||||
|
||||
def action_open_survey_wizard(self):
|
||||
return {
|
||||
'name': _('Assign Online Test'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'assign.survey.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_applicant_id': self.id,
|
||||
}
|
||||
}
|
||||
|
||||
def action_open_resume(self):
|
||||
self.ensure_one()
|
||||
|
||||
if not self.resume:
|
||||
return
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f'/web/content/hr.applicant/{self.id}/resume/{self.resume_name}?download=false',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_open_resume_applicant(self):
|
||||
pass
|
||||
|
||||
|
||||
class HRSkill(models.Model):
|
||||
_inherit = 'hr.skill'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
|
||||
|
||||
class ApplicantCertificate(models.Model):
|
||||
_name = 'applicant.certificate'
|
||||
_description = 'Applicant Certificate'
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
'hr.applicant',
|
||||
string="Applicant",
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
name = fields.Char(
|
||||
string="Certificate Name",
|
||||
required=True
|
||||
)
|
||||
|
||||
certificate_file = fields.Binary(
|
||||
string="Certificate File"
|
||||
)
|
||||
|
||||
certificate_filename = fields.Char(
|
||||
string="Filename"
|
||||
)
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import werkzeug
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
|
||||
class ApplicantSurveyLine(models.Model):
|
||||
_name = 'applicant.survey.line'
|
||||
_description = 'Applicant Survey Line'
|
||||
_order = 'id desc'
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
'hr.applicant',
|
||||
string="Applicant",
|
||||
ondelete='cascade'
|
||||
)
|
||||
|
||||
survey_id = fields.Many2one(
|
||||
'survey.survey',
|
||||
string="Online Test",
|
||||
required=True
|
||||
)
|
||||
|
||||
survey_user_input_id = fields.Many2one(
|
||||
'survey.user_input',
|
||||
string="Survey Answer"
|
||||
)
|
||||
|
||||
survey_start_url = fields.Char(
|
||||
string="Survey URL"
|
||||
)
|
||||
|
||||
survey_state = fields.Selection([
|
||||
('new', 'New'),
|
||||
('in_progress', 'In Progress'),
|
||||
('done', 'Completed')], string='Status', default='new', compute="_compute_survey_state",
|
||||
store=True, readonly=True)
|
||||
|
||||
survey_score = fields.Float(
|
||||
string="Score",
|
||||
compute="_compute_survey_score",
|
||||
store=True
|
||||
)
|
||||
response_ids = fields.One2many('survey.user_input', 'applicant_survey_line', string="Responses")
|
||||
|
||||
@api.depends('survey_user_input_id.state')
|
||||
def _compute_survey_state(self):
|
||||
for rec in self:
|
||||
rec.survey_state = rec.survey_user_input_id.state or False
|
||||
|
||||
@api.depends('response_ids.user_input_line_ids.answer_score','survey_user_input_id.scoring_percentage')
|
||||
def _compute_survey_score(self):
|
||||
for rec in self:
|
||||
# if rec.response_ids:
|
||||
# latest_response = rec.response_ids.sorted('id', reverse=True)[:1]
|
||||
# rec.survey_score = sum(
|
||||
# latest_response.user_input_line_ids.mapped('answer_score')
|
||||
# ) if latest_response else 0.0
|
||||
# else:
|
||||
rec.survey_score = rec.survey_user_input_id.scoring_percentage if rec.survey_user_input_id else 0.0
|
||||
|
||||
def _sync_latest_response(self):
|
||||
for line in self:
|
||||
latest_response = line.response_ids.sorted(lambda response: (response.create_date, response.id))[-1:]
|
||||
if not latest_response:
|
||||
continue
|
||||
|
||||
line.write({
|
||||
'survey_user_input_id': latest_response.id,
|
||||
'survey_start_url': werkzeug.urls.url_join(
|
||||
line.survey_id.get_base_url(),
|
||||
latest_response.get_start_url(),
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
class AssignSurveyWizard(models.TransientModel):
|
||||
_name = 'assign.survey.wizard'
|
||||
_description = 'Assign Survey Wizard'
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
'hr.applicant',
|
||||
string="Applicant"
|
||||
)
|
||||
|
||||
survey_id = fields.Many2one(
|
||||
'survey.survey',
|
||||
string="Survey",
|
||||
required=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def action_assign_survey(self):
|
||||
self.ensure_one()
|
||||
|
||||
self.survey_id.check_validity()
|
||||
template = self.env.ref('web_portal_form_custom.mail_template_applicant_interview_invite',
|
||||
raise_if_not_found=False)
|
||||
local_context = dict(
|
||||
default_applicant_id=self.applicant_id.id,
|
||||
default_partner_ids=self.applicant_id.partner_id.ids,
|
||||
default_survey_id=self.survey_id.id,
|
||||
default_use_template=bool(template),
|
||||
default_template_id=template and template.id or False,
|
||||
default_email_layout_xmlid='mail.mail_notification_light',
|
||||
default_deadline=fields.Datetime.now() + timedelta(days=15)
|
||||
)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _("Send an interview"),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'survey.invite',
|
||||
'target': 'new',
|
||||
'context': local_context,
|
||||
}
|
||||
|
||||
|
||||
class SurveyUserInput(models.Model):
|
||||
_inherit = "survey.user_input"
|
||||
|
||||
applicant_survey_line = fields.Many2one('applicant.survey.line', string="Applicant Survey")
|
||||
applicant_id = fields.Many2one('hr.applicant', related='applicant_survey_line.applicant_id', store=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records.mapped('applicant_survey_line')._sync_latest_response()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
previous_lines = self.mapped('applicant_survey_line')
|
||||
result = super().write(vals)
|
||||
if 'applicant_survey_line' in vals:
|
||||
(previous_lines | self.mapped('applicant_survey_line'))._sync_latest_response()
|
||||
return result
|
||||
|
||||
def _mark_done(self):
|
||||
odoobot = self.env.ref('base.partner_root')
|
||||
for user_input in self:
|
||||
if user_input.applicant_id:
|
||||
body = _('The applicant "%s" has finished the survey.', user_input.applicant_id.partner_name)
|
||||
user_input.applicant_id.message_post(body=body, author_id=odoobot.id)
|
||||
return super()._mark_done()
|
||||
|
||||
|
||||
class SurveySurvey(models.Model):
|
||||
_inherit = 'survey.survey'
|
||||
|
||||
def _create_answer(self, user=False, partner=False, email=False, test_entry=False, check_attempts=True, **additional_vals):
|
||||
applicant_survey_line = False
|
||||
invite_token = additional_vals.get('invite_token')
|
||||
|
||||
if invite_token:
|
||||
previous_answer = self.env['survey.user_input'].search([
|
||||
('survey_id', 'in', self.ids),
|
||||
('invite_token', '=', invite_token),
|
||||
('applicant_survey_line', '!=', False),
|
||||
], order='id desc', limit=1)
|
||||
applicant_survey_line = previous_answer.applicant_survey_line
|
||||
|
||||
if not applicant_survey_line:
|
||||
search_domain = [('survey_id', 'in', self.ids)]
|
||||
if partner:
|
||||
search_domain.append(('partner_id', '=', partner.id))
|
||||
elif email:
|
||||
search_domain.append(('email', '=', email))
|
||||
else:
|
||||
search_domain = []
|
||||
|
||||
if search_domain:
|
||||
previous_answer = self.env['survey.user_input'].search(search_domain, order='id desc', limit=1)
|
||||
applicant_survey_line = previous_answer.applicant_survey_line
|
||||
|
||||
if applicant_survey_line and not additional_vals.get('applicant_survey_line'):
|
||||
additional_vals['applicant_survey_line'] = applicant_survey_line.id
|
||||
|
||||
return super()._create_answer(
|
||||
user=user,
|
||||
partner=partner,
|
||||
email=email,
|
||||
test_entry=test_entry,
|
||||
check_attempts=check_attempts,
|
||||
**additional_vals,
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_assign_survey_wizard,assign.survey.wizard,model_assign_survey_wizard,,1,1,1,1
|
||||
access_applicant_survey_line_user,access.applicant.survey.line.user,model_applicant_survey_line,base.group_user,1,1,1,1
|
||||
access_applicant_certificate,applicant.certificate,model_applicant_certificate,,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
.o_form_view .o_group.row.align-items-start {
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 0px !important;
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
.o_form_view h1 {
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 0px !important;
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
.o_form_view .o_inner_group {
|
||||
margin-top: 0px !important;
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue