Recruitment changes
This commit is contained in:
parent
90211776a1
commit
604d556501
|
|
@ -1,2 +1,3 @@
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizards
|
from . import wizards
|
||||||
|
from . import controllers
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
'report/report_action.xml',
|
'report/report_action.xml',
|
||||||
'report/it_tax_template.xml',
|
'report/it_tax_template.xml',
|
||||||
'views/it_tax_menu_and_wizard_view.xml',
|
'views/it_tax_menu_and_wizard_view.xml',
|
||||||
|
'views/employee_payslip_download_wizard_views.xml',
|
||||||
'wizards/hr_tds_calculation.xml',
|
'wizards/hr_tds_calculation.xml',
|
||||||
'wizards/children_education_costing.xml',
|
'wizards/children_education_costing.xml',
|
||||||
'wizards/employee_life_insurance.xml',
|
'wizards/employee_life_insurance.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 payroll_periods
|
||||||
from . import investment_types
|
from . import investment_types
|
||||||
from . import investment_costings
|
|
||||||
from . import emp_it_declaration
|
from . import emp_it_declaration
|
||||||
|
from . import investment_costings
|
||||||
from . import slab_master
|
from . import slab_master
|
||||||
from . import it_tax_statement
|
from . import it_tax_statement
|
||||||
from . import it_tax_statement_wiz
|
from . import it_tax_statement_wiz
|
||||||
|
from . import employee_payslip_download_wiz
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,39 @@
|
||||||
from odoo import models, fields, api
|
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):
|
class EmpITDeclaration(models.Model):
|
||||||
|
|
@ -38,6 +73,12 @@ class EmpITDeclaration(models.Model):
|
||||||
('new', 'New Regime'),
|
('new', 'New Regime'),
|
||||||
('old', 'Old Regime')
|
('old', 'Old Regime')
|
||||||
], string="Tax Regime", required=True, default='new')
|
], 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')
|
total_investment = fields.Float(string='Total Investment')
|
||||||
|
|
||||||
|
|
@ -51,6 +92,28 @@ class EmpITDeclaration(models.Model):
|
||||||
)
|
)
|
||||||
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
|
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')
|
@api.depends('costing_details_generated','investment_costing_ids')
|
||||||
def _compute_investment_costing(self):
|
def _compute_investment_costing(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -346,3 +409,155 @@ class EmpITDeclaration(models.Model):
|
||||||
rec._ensure_investment_costing_records()
|
rec._ensure_investment_costing_records()
|
||||||
rec._update_investment_amounts()
|
rec._update_investment_amounts()
|
||||||
rec.costing_details_generated = True
|
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',
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import re
|
||||||
|
|
||||||
class investmentCostings(models.Model):
|
class investmentCostings(models.Model):
|
||||||
_name = 'investment.costings'
|
_name = 'investment.costings'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_id'
|
_rec_name = 'investment_type_id'
|
||||||
|
|
||||||
investment_type_id = fields.Many2one('it.investment.type')
|
investment_type_id = fields.Many2one('it.investment.type')
|
||||||
|
|
@ -25,6 +26,7 @@ class investmentCostings(models.Model):
|
||||||
|
|
||||||
class pastEmpcostingType(models.Model):
|
class pastEmpcostingType(models.Model):
|
||||||
_name = 'past_employment.costing.type'
|
_name = 'past_employment.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,6 +106,7 @@ class pastEmpcostingType(models.Model):
|
||||||
|
|
||||||
class us80cCostingType(models.Model):
|
class us80cCostingType(models.Model):
|
||||||
_name = 'us80c.costing.type'
|
_name = 'us80c.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -140,6 +143,7 @@ class us80cCostingType(models.Model):
|
||||||
}
|
}
|
||||||
class us80dCostingType(models.Model):
|
class us80dCostingType(models.Model):
|
||||||
_name = 'us80d.costing.type'
|
_name = 'us80d.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -157,6 +161,7 @@ class us80dCostingType(models.Model):
|
||||||
|
|
||||||
class us10CostingType(models.Model):
|
class us10CostingType(models.Model):
|
||||||
_name = 'us10.costing.type'
|
_name = 'us10.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -173,6 +178,7 @@ class us10CostingType(models.Model):
|
||||||
|
|
||||||
class us80gCostingType(models.Model):
|
class us80gCostingType(models.Model):
|
||||||
_name = 'us80g.costing.type'
|
_name = 'us80g.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -189,6 +195,7 @@ class us80gCostingType(models.Model):
|
||||||
|
|
||||||
class chapterViaCostingType(models.Model):
|
class chapterViaCostingType(models.Model):
|
||||||
_name = 'chapter.via.costing.type'
|
_name = 'chapter.via.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -205,6 +212,7 @@ class chapterViaCostingType(models.Model):
|
||||||
|
|
||||||
class us17CostingType(models.Model):
|
class us17CostingType(models.Model):
|
||||||
_name = 'us17.costing.type'
|
_name = 'us17.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -219,6 +227,7 @@ class us17CostingType(models.Model):
|
||||||
|
|
||||||
class OtherILCostingType(models.Model):
|
class OtherILCostingType(models.Model):
|
||||||
_name = 'other.il.costing.type'
|
_name = 'other.il.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -256,6 +265,7 @@ class OtherILCostingType(models.Model):
|
||||||
|
|
||||||
class OtherDeclarationCostingType(models.Model):
|
class OtherDeclarationCostingType(models.Model):
|
||||||
_name = 'other.declaration.costing.type'
|
_name = 'other.declaration.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -271,6 +281,7 @@ class OtherDeclarationCostingType(models.Model):
|
||||||
|
|
||||||
class HouseRentDeclaration(models.Model):
|
class HouseRentDeclaration(models.Model):
|
||||||
_name = 'house.rent.declaration'
|
_name = 'house.rent.declaration'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'House Rent Declaration'
|
_description = 'House Rent Declaration'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<report
|
<record id="action_report_it_tax_statement" model="ir.actions.report">
|
||||||
id="action_report_it_tax_statement"
|
<field name="name">IT Declaration Submission</field>
|
||||||
model="emp.it.declaration"
|
<field name="model">emp.it.declaration</field>
|
||||||
string="IT Tax Statement"
|
<field name="report_type">qweb-pdf</field>
|
||||||
report_type="qweb-pdf"
|
<field name="report_name">employee_it_declaration.report_it_tax_statement</field>
|
||||||
name="your_module_name.report_it_tax_statement"
|
<field name="report_file">employee_it_declaration.report_it_tax_statement</field>
|
||||||
file="your_module_name.report_it_tax_statement"
|
<field name="print_report_name">'IT Declaration - %s - %s' % (object.employee_id.name or '', object.period_id.name or '')</field>
|
||||||
print_report_name="'IT_Tax_Statement_%s' % (object.employee_id.name)"
|
<field name="binding_model_id" eval="False"/>
|
||||||
/>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ access_house_rent_declaration_user,access.house.rent.declaration.user,model_hous
|
||||||
|
|
||||||
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
|
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_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_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_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1
|
||||||
|
|
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
|
|
||||||
|
<field name="employee_id"/>
|
||||||
<field name="period_id"/>
|
<field name="period_id"/>
|
||||||
<field name="total_investment"/>
|
<field name="total_investment"/>
|
||||||
<field name="tax_regime"/>
|
<field name="tax_regime"/>
|
||||||
|
<field name="state"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -20,18 +22,40 @@
|
||||||
<field name="model">emp.it.declaration</field>
|
<field name="model">emp.it.declaration</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="IT Declaration">
|
<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>
|
<sheet>
|
||||||
<div class="oe_title mb24">
|
<div class="oe_title mb24">
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated"/>
|
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated or state == 'submitted'"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="period_id" readonly="costing_details_generated"/>
|
<field name="period_id" readonly="costing_details_generated or state == 'submitted'"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="total_investment"/>
|
<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="costing_details_generated" invisible="1" force_save="1"/>
|
||||||
<field name="house_rent_costing_id"/>
|
<field name="house_rent_costing_id"/>
|
||||||
<field name="show_past_employment" invisible="1"/>
|
<field name="show_past_employment" invisible="1"/>
|
||||||
|
|
@ -49,9 +73,9 @@
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<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/>
|
<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 -->
|
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
|
||||||
|
|
||||||
<group invisible="not costing_details_generated">
|
<group invisible="not costing_details_generated">
|
||||||
|
|
@ -71,7 +95,7 @@
|
||||||
<page string="Total Investment Costing">
|
<page string="Total Investment Costing">
|
||||||
<group>
|
<group>
|
||||||
<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">
|
<list editable="bottom" create="0" delete="0" edit="0">
|
||||||
<field name="investment_type_id"/>
|
<field name="investment_type_id"/>
|
||||||
<field name="amount"/>
|
<field name="amount"/>
|
||||||
|
|
@ -102,7 +126,7 @@
|
||||||
<!-- </page>-->
|
<!-- </page>-->
|
||||||
<page name="past_employment_costings" string="PAST EMPLOYMENT" invisible="not show_past_employment">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -112,7 +136,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -126,7 +150,7 @@
|
||||||
|
|
||||||
<page name="us_80c_costings" string="US 80C" invisible="not show_us_80c">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -142,7 +166,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -162,10 +186,10 @@
|
||||||
</page>
|
</page>
|
||||||
<page name="us_80d_costings" string="US 80D" invisible="not show_us_80d">
|
<page name="us_80d_costings" string="US 80D" invisible="not show_us_80d">
|
||||||
<group>
|
<group>
|
||||||
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated"/>
|
<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"/>
|
<field name="us80d_health_checkup" readonly="state == 'submitted'"/>
|
||||||
</group>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -175,7 +199,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -185,7 +209,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -196,7 +220,7 @@
|
||||||
</list>
|
</list>
|
||||||
|
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -207,7 +231,7 @@
|
||||||
</list>
|
</list>
|
||||||
|
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -218,7 +242,7 @@
|
||||||
</list>
|
</list>
|
||||||
|
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -231,7 +255,7 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="us_10_costing" string="US 10" invisible="not show_us_10">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -241,7 +265,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -253,7 +277,7 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="us_80g_costing" string="US 80G" invisible="not show_us_80g">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -263,7 +287,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -276,7 +300,7 @@
|
||||||
</page>
|
</page>
|
||||||
|
|
||||||
<page name="chapter_via_costings" string="CHAPTER VIA" invisible="not show_chapter_via">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -286,7 +310,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -299,7 +323,7 @@
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
<page name="us_17_costings" string="US 17" invisible="not show_us_17">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -309,7 +333,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -322,7 +346,7 @@
|
||||||
</page>
|
</page>
|
||||||
<page name="house_rent_costings" string="HOUSE RENT" invisible="not show_house_rent">
|
<page name="house_rent_costings" string="HOUSE RENT" invisible="not show_house_rent">
|
||||||
<!-- <field name="house_rent_costing_line_ids"/>-->
|
<!-- <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
|
'default_costing_type': house_rent_costing_id
|
||||||
}">
|
}">
|
||||||
<list string="House Rent Declarations">
|
<list string="House Rent Declarations">
|
||||||
|
|
@ -373,7 +397,7 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS" invisible="not show_other_i_or_l">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -390,7 +414,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -409,7 +433,7 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="other_declaration_costings" string="Other Declarations" invisible="not show_other_declaration">
|
<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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -419,7 +443,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</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">
|
<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="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"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -443,10 +467,13 @@
|
||||||
<field name="res_model">emp.it.declaration</field>
|
<field name="res_model">emp.it.declaration</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</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"
|
<menuitem id="menu_it_declarations" name="IT Declarations"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="hr_payroll.menu_hr_payroll_root"
|
||||||
action="action_emp_it_declaration" sequence="99"/>
|
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>
|
</data>
|
||||||
</odoo>
|
</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="name">Generate Tax Statement</field>
|
||||||
<field name="res_model">it.tax.statement.wizard</field>
|
<field name="res_model">it.tax.statement.wizard</field>
|
||||||
<field name="path">tax-statement</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">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Create a new employment type
|
Create a new employment type
|
||||||
|
|
@ -96,8 +96,9 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
|
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="employee_it_declaration.menu_hr_payroll_emp_root"
|
||||||
action="action_it_tax_statement_wizard" sequence="99"/>
|
groups="base.group_user"
|
||||||
|
action="action_it_tax_statement_wizard" sequence="3"/>
|
||||||
|
|
||||||
<record id="it_statement_paper_format" model="report.paperformat">
|
<record id="it_statement_paper_format" model="report.paperformat">
|
||||||
<field name="name">A4 - statement</field>
|
<field name="name">A4 - statement</field>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,158 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<template id="report_it_tax_statement">
|
<template id="report_it_tax_statement">
|
||||||
<t t-call="web.html_container">
|
<t t-call="web.html_container">
|
||||||
<t t-call="web.external_layout">
|
<t t-foreach="docs" t-as="doc">
|
||||||
<div class="page">
|
<t t-call="web.external_layout">
|
||||||
<h2>IT Tax Statement</h2>
|
<div class="page">
|
||||||
<p><strong>Employee:</strong> <t t-esc="doc.employee_id.name"/></p>
|
<style>
|
||||||
<p><strong>Period:</strong> <t t-esc="doc.period_id.name"/></p>
|
.it-title { text-align: center; margin-bottom: 18px; }
|
||||||
<p><strong>Tax Regime:</strong> <t t-esc="dict(doc._fields['tax_regime'].selection).get(doc.tax_regime)"/></p>
|
.it-title h2 { margin: 0; font-size: 21px; font-weight: 700; }
|
||||||
<table class="table table-sm">
|
.it-title p { margin: 4px 0 0; color: #555; }
|
||||||
<thead>
|
.it-section { margin-top: 18px; }
|
||||||
<tr><th>Section</th><th>Amount</th></tr>
|
.it-section h4 { font-size: 14px; font-weight: 700; border-bottom: 1px solid #999; padding-bottom: 4px; margin-bottom: 8px; }
|
||||||
</thead>
|
.it-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||||
<tbody>
|
.it-table th { background: #f2f2f2; font-weight: 700; }
|
||||||
<t t-foreach="doc.investment_costing_ids" t-as="line">
|
.it-table th, .it-table td { border: 1px solid #ddd; padding: 5px; vertical-align: top; }
|
||||||
<tr>
|
.it-right { text-align: right; }
|
||||||
<td><t t-esc="line.investment_type_id.name"/></td>
|
.it-muted { color: #777; }
|
||||||
<td><t t-esc="line.amount"/></td>
|
</style>
|
||||||
</tr>
|
|
||||||
</t>
|
<div class="it-title">
|
||||||
</tbody>
|
<h2>IT Declaration Submission</h2>
|
||||||
</table>
|
<p>
|
||||||
<p><strong>Total:</strong> <t t-esc="sum(doc.investment_costing_ids.mapped('amount'))"/></p>
|
<span t-esc="doc.employee_id.company_id.name"/>
|
||||||
</div>
|
</p>
|
||||||
</t>
|
</div>
|
||||||
</t>
|
|
||||||
</template>
|
<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>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class ChildrenEducation(models.Model):
|
class ChildrenEducation(models.Model):
|
||||||
_name = "children.education"
|
_name = "children.education"
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = "Children Education"
|
_description = "Children Education"
|
||||||
_rec_name = 'it_declaration_id'
|
_rec_name = 'it_declaration_id'
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ class ChildrenEducation(models.Model):
|
||||||
|
|
||||||
class ChildrenEducationCosting(models.Model):
|
class ChildrenEducationCosting(models.Model):
|
||||||
_name = 'children.education.costing'
|
_name = 'children.education.costing'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = "Children Education Costing"
|
_description = "Children Education Costing"
|
||||||
|
|
||||||
child_id = fields.Char('Child ID')
|
child_id = fields.Char('Child ID')
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api, _
|
||||||
|
|
||||||
class US80CInsuranceLine(models.Model):
|
class US80CInsuranceLine(models.Model):
|
||||||
_name = 'us80c.insurance.line'
|
_name = 'us80c.insurance.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'US80C Insurance Line'
|
_description = 'US80C Insurance Line'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
@ -30,6 +31,7 @@ class US80CInsuranceLine(models.Model):
|
||||||
|
|
||||||
class EmployeeLifeInsurance(models.Model):
|
class EmployeeLifeInsurance(models.Model):
|
||||||
_name = 'employee.life.insurance'
|
_name = 'employee.life.insurance'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Employee Life Insurance'
|
_description = 'Employee Life Insurance'
|
||||||
|
|
||||||
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
|
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import math
|
||||||
|
|
||||||
class LetoutHouseProperty(models.Model):
|
class LetoutHouseProperty(models.Model):
|
||||||
_name = 'letout.house.property'
|
_name = 'letout.house.property'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Letout House Property Details'
|
_description = 'Letout House Property Details'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class NSCDeclarationLine(models.Model):
|
class NSCDeclarationLine(models.Model):
|
||||||
_name = 'nsc.declaration.line'
|
_name = 'nsc.declaration.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Declaration Line'
|
_description = 'NSC Declaration Line'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
||||||
|
|
@ -19,6 +20,7 @@ class NSCDeclarationLine(models.Model):
|
||||||
|
|
||||||
class NSCEntry(models.Model):
|
class NSCEntry(models.Model):
|
||||||
_name = 'nsc.entry'
|
_name = 'nsc.entry'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Entry'
|
_description = 'NSC Entry'
|
||||||
|
|
||||||
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
|
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class NSCInterestLine(models.Model):
|
class NSCInterestLine(models.Model):
|
||||||
_name = 'nsc.interest.line'
|
_name = 'nsc.interest.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Interest Line'
|
_description = 'NSC Interest Line'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ class NSCInterestLine(models.Model):
|
||||||
|
|
||||||
class NSCEntry(models.Model):
|
class NSCEntry(models.Model):
|
||||||
_name = 'nsc.interest.entry'
|
_name = 'nsc.interest.entry'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Entry'
|
_description = 'NSC Entry'
|
||||||
|
|
||||||
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
|
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class SelfOccupiedProperty(models.Model):
|
class SelfOccupiedProperty(models.Model):
|
||||||
_name = 'self.occupied.property'
|
_name = 'self.occupied.property'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Self Occupied House Property Details'
|
_description = 'Self Occupied House Property Details'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class website_hr_recruitment_applications_extended(website_hr_recruitment_applic
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
if not applicant.exists():
|
if not applicant.exists():
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
if applicant and applicant.send_post_onboarding_form:
|
if applicant:
|
||||||
if applicant.post_onboarding_form_status == 'done':
|
if applicant.post_onboarding_form_status == 'done':
|
||||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
return request.render("hr_recruitment_extended.thank_you_template", {
|
||||||
'applicant': applicant
|
'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"
|
return f"Error: Applicant with ID {applicant_id} not found"
|
||||||
|
|
||||||
# Business logic check
|
# 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"
|
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
|
||||||
|
|
||||||
# Get the template
|
# Get the template
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from odoo import fields, api, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,8 +31,7 @@ class HREmployee(models.Model):
|
||||||
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
||||||
})
|
})
|
||||||
rec.applicant_id = application.id
|
rec.applicant_id = application.id
|
||||||
rec.sudo().applicant_id.send_post_onboarding_form = True
|
return rec.sudo().applicant_id.send_jod_form_to_employee()
|
||||||
return rec.sudo().applicant_id.send_post_onboarding_form_to_candidate()
|
|
||||||
|
|
||||||
|
|
||||||
class PostOnboardingAttachmentWizard(models.TransientModel):
|
class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
|
|
@ -44,11 +43,12 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
def _onchange_template_id(self):
|
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:
|
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')
|
model = self.env.context.get('active_model')
|
||||||
|
if model == 'applicant.request.forms':
|
||||||
if model == 'hr.applicant':
|
|
||||||
applicant = self.env['hr.applicant'].browse(record_id)
|
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:
|
else:
|
||||||
if model == 'hr.employee':
|
if model == 'hr.employee':
|
||||||
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
||||||
|
|
@ -74,14 +74,30 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
context = self.env.context
|
context = self.env.context
|
||||||
active_id = context.get('active_id')
|
active_id = context.get('applicant_id')
|
||||||
model = context.get('active_model')
|
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)
|
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:
|
else:
|
||||||
if model == 'hr.employee':
|
applicant = self.env['hr.applicant'].browse(active_id)
|
||||||
applicant = self.env['hr.employee'].browse(active_id).applicant_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]
|
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')
|
lambda a: a.attachment_type == 'previous_employer').mapped('name')
|
||||||
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
||||||
|
|
||||||
# Prepare context for the template
|
|
||||||
email_context = {
|
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,
|
'personal_docs': personal_docs,
|
||||||
'education_docs': education_docs,
|
'education_docs': education_docs,
|
||||||
'previous_employer_docs': previous_employer_docs,
|
'previous_employer_docs': previous_employer_docs,
|
||||||
'other_docs': other_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_values = {
|
||||||
'email_from': rec.email_from,
|
'email_from': rec.email_from,
|
||||||
'email_to': rec.email_to,
|
'email_to': rec.email_to,
|
||||||
'email_cc': rec.email_cc,
|
'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)],
|
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
||||||
|
|
||||||
}
|
}
|
||||||
# Use 'with_context' to override the email template fields dynamically
|
|
||||||
if rec.send_mail:
|
if rec.send_mail:
|
||||||
template.sudo().with_context(default_body_html=rec.email_body,
|
template.sudo().with_context(default_body_html=rec.email_body,
|
||||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
**email_context).send_mail(applicant.id, email_values=email_values,
|
||||||
|
|
@ -116,7 +139,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
base_url = self.get_base_url()
|
base_url = self.get_base_url()
|
||||||
|
|
||||||
if rec.is_pre_onboarding_attachment_request:
|
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:
|
else:
|
||||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||||
applicant.joining_form_link = '%s/FTPROTECH/JoiningForm/%s'%(base_url,applicant.id)
|
applicant.joining_form_link = '%s/FTPROTECH/JoiningForm/%s'%(base_url,applicant.id)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
<field name="model">hr.applicant</field>
|
<field name="model">hr.applicant</field>
|
||||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//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"/>
|
<field name="joining_form_link" force_save="1" readonly="1"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@
|
||||||
'views/res_config_settings.xml',
|
'views/res_config_settings.xml',
|
||||||
'views/hr_employee.xml',
|
'views/hr_employee.xml',
|
||||||
'views/bank_details.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')
|
last_working_day = fields.Date(string='Last Working Day')
|
||||||
ctc = fields.Char(string='CTC')
|
ctc = fields.Char(string='CTC')
|
||||||
employee_id = fields.Many2one('hr.employee')
|
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 json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
|
from calendar import monthrange
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
|
||||||
|
|
@ -232,6 +233,29 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"degree": {"type": "string", "description": "Highest degree or main qualification"},
|
"degree": {"type": "string", "description": "Highest degree or main qualification"},
|
||||||
"skills": {"type": "list", "description": "All explicit technical and functional skills"},
|
"skills": {"type": "list", "description": "All explicit technical and functional skills"},
|
||||||
"summary": {"type": "string", "description": "Short professional summary from the resume"},
|
"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):
|
def _get_jd_required_fields(self):
|
||||||
|
|
@ -255,7 +279,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"Return precise contact details, experience, location, and skills. "
|
"Return precise contact details, experience, location, and skills. "
|
||||||
"Do not guess missing values. "
|
"Do not guess missing values. "
|
||||||
"Normalize skills into clean individual names. "
|
"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"
|
"Do not consider certifications, responsibilities, Non Technical Stuff as skills"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -283,12 +310,16 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
if self.update_existing_candidates:
|
if self.update_existing_candidates:
|
||||||
candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals))
|
candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals))
|
||||||
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
||||||
|
if self.target_model == "candidate":
|
||||||
|
self._sync_candidate_resume_histories(candidate, parsed_data)
|
||||||
message = _("Matched existing candidate: %s") % candidate.display_name
|
message = _("Matched existing candidate: %s") % candidate.display_name
|
||||||
return candidate, "updated", message
|
return candidate, "updated", message
|
||||||
|
|
||||||
self._ensure_resume_creation_allowed(parsed_data, line.file_name)
|
self._ensure_resume_creation_allowed(parsed_data, line.file_name)
|
||||||
candidate = self.env["hr.candidate"].create(candidate_vals)
|
candidate = self.env["hr.candidate"].create(candidate_vals)
|
||||||
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
|
||||||
|
if self.target_model == "candidate":
|
||||||
|
self._sync_candidate_resume_histories(candidate, parsed_data)
|
||||||
message = _("Created candidate: %s") % candidate.display_name
|
message = _("Created candidate: %s") % candidate.display_name
|
||||||
return candidate, "created", message
|
return candidate, "created", message
|
||||||
|
|
||||||
|
|
@ -298,6 +329,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
("hr_job_recruitment", "=", self.job_recruitment_id.id),
|
("hr_job_recruitment", "=", self.job_recruitment_id.id),
|
||||||
], limit=1)
|
], limit=1)
|
||||||
if existing_applicant:
|
if existing_applicant:
|
||||||
|
self._sync_applicant_resume_histories(existing_applicant, parsed_data)
|
||||||
return existing_applicant, "existing", _("Existing application reused for this job request.")
|
return existing_applicant, "existing", _("Existing application reused for this job request.")
|
||||||
|
|
||||||
applicant_vals = {
|
applicant_vals = {
|
||||||
|
|
@ -319,6 +351,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
applicant_vals["total_exp_type"] = "year"
|
applicant_vals["total_exp_type"] = "year"
|
||||||
applicant_vals = {key: value for key, value in applicant_vals.items() if value not in (False, None, "")}
|
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)
|
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
|
return applicant, "created", _("Created application for job request %s.") % self.job_recruitment_id.display_name
|
||||||
|
|
||||||
def _find_existing_candidate(self, parsed_data):
|
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["total_experience_years"] = self._guess_total_experience(extracted_text)
|
||||||
|
|
||||||
data["skills"] = self._merge_resume_skills(data.get("skills") or [], 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
|
return data
|
||||||
|
|
||||||
def _post_process_jd_data(self, parsed_data, extracted_text):
|
def _post_process_jd_data(self, parsed_data, extracted_text):
|
||||||
|
|
@ -691,6 +727,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"resume_name": line.file_name,
|
"resume_name": line.file_name,
|
||||||
"resume_type": resume_mimetype,
|
"resume_type": resume_mimetype,
|
||||||
"type_id": degree_id,
|
"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, "")}
|
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]
|
update_vals[field_name] = values[field_name]
|
||||||
return update_vals
|
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):
|
def _sync_candidate_skills(self, candidate, skills):
|
||||||
if not skills:
|
if not skills:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,17 @@
|
||||||
'wizards/applicant_refuse_reason.xml',
|
'wizards/applicant_refuse_reason.xml',
|
||||||
'wizards/ats_invite_mail_template_wizard.xml',
|
'wizards/ats_invite_mail_template_wizard.xml',
|
||||||
'wizards/client_submission_mail_template_wizard.xml',
|
'wizards/client_submission_mail_template_wizard.xml',
|
||||||
|
'wizards/applicant_stage_comment_wizard.xml',
|
||||||
# 'views/resume_pearser.xml',
|
# 'views/resume_pearser.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'hr_recruitment_extended/static/src/img/pdf_icon.png',
|
'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/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/recruitment_match_panel.scss',
|
||||||
|
'hr_recruitment_extended/static/src/scss/hr_applicant_hold.scss',
|
||||||
],
|
],
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,58 @@
|
||||||
|
|
||||||
import warnings
|
|
||||||
from datetime import datetime
|
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 import http
|
||||||
from odoo.addons.website_hr_recruitment.controllers.main import WebsiteHrRecruitment
|
|
||||||
from odoo.osv.expression import AND
|
|
||||||
from odoo.http import request
|
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
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class website_hr_recruitment_applications(http.Controller):
|
class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
@http.route(['/hr_recruitment/second_application_form/<int:applicant_id>'], type='http', auth="public", website=True)
|
def _get_request_form(self, applicant_id, applicant_request_id, token=None, form_type=None, require_token=False):
|
||||||
def second_application_form(self, applicant_id, **kwargs):
|
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."""
|
"""Renders the website form for applicants to submit additional details."""
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant_request_id = request.env['applicant.request.forms'].sudo().browse(applicant_request_id)
|
||||||
if not applicant.exists():
|
if not applicant_request_id.exists():
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
if applicant and applicant.send_second_application_form:
|
if applicant_request_id :
|
||||||
if applicant.second_application_form_status == 'done':
|
if applicant_request_id.status == 'done':
|
||||||
return request.render("hr_recruitment_extended.thank_you_template")
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
else:
|
else:
|
||||||
return request.render("hr_recruitment_extended.applicant_form_template", {
|
return request.render("hr_recruitment_extended.applicant_form_template", {
|
||||||
'applicant': applicant
|
'applicant': applicant_request_id.applicant_id,
|
||||||
|
'applicant_request_id': applicant_request_id
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
return request.not_found()
|
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)
|
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
|
# Get the applicant
|
||||||
candidate_image_base64 = kwargs.pop('candidate_image_base64')
|
candidate_image_base64 = kwargs.pop('candidate_image_base64')
|
||||||
candidate_image = kwargs.pop('candidate_image')
|
candidate_image = kwargs.pop('candidate_image')
|
||||||
|
|
@ -62,10 +71,11 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
kwargs['total_exp_type'] = 'year'
|
kwargs['total_exp_type'] = 'year'
|
||||||
|
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
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
|
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")
|
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:
|
if template and applicant.user_id.email:
|
||||||
template.sudo().send_mail(applicant.id, force_send=True)
|
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
|
# Redirect to a Thank You page
|
||||||
return request.render("hr_recruitment_extended.thank_you_template")
|
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)
|
website=True)
|
||||||
def doc_request_form(self, applicant_id, **kwargs):
|
def doc_request_form(self, applicant_id, applicant_request_id, **kwargs):
|
||||||
"""Renders the website form for applicants to submit additional details."""
|
request_token = kwargs.get('token')
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant_request = self._get_request_form(
|
||||||
if not applicant.exists():
|
applicant_id,
|
||||||
return request.not_found()
|
applicant_request_id,
|
||||||
if applicant:
|
token=request_token,
|
||||||
if applicant.doc_requests_form_status == 'done':
|
form_type='documents_request',
|
||||||
return request.render("hr_recruitment_extended.thank_you_template")
|
require_token=True,
|
||||||
else:
|
)
|
||||||
return request.render("hr_recruitment_extended.doc_request_form_template", {
|
if not applicant_request:
|
||||||
'applicant': applicant
|
raise request.not_found()
|
||||||
})
|
if self._is_submitted_for_token(applicant_request, request_token):
|
||||||
else:
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
return request.not_found()
|
|
||||||
|
|
||||||
@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)
|
methods=['POST'], website=True, csrf=False)
|
||||||
def process_applicant_doc_submission_form(self, applicant_id, **post):
|
def process_applicant_doc_submission_form(self, applicant_id, applicant_request_id, **post):
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
request_token = post.get('request_token')
|
||||||
if not applicant.exists():
|
applicant_request = self._get_request_form(
|
||||||
return request.not_found() # Return 404 if applicant doesn't exist
|
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")
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
|
|
||||||
applicant_data = {
|
applicant_data = {
|
||||||
'applicant_id': int(post.get('applicant_id', 0)),
|
'applicant_id': int(post.get('applicant_id', 0)),
|
||||||
'candidate_image': post.get('candidate_image_base64', ''),
|
'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}
|
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 = post.get('attachments_data_json', '[]')
|
||||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
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.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")
|
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)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
if not applicant.exists():
|
if not applicant.exists():
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
if applicant and applicant.send_post_onboarding_form:
|
if applicant:
|
||||||
if applicant.post_onboarding_form_status == 'done':
|
if applicant.post_onboarding_form_status == 'done':
|
||||||
return request.render("hr_recruitment_extended.thank_you_template")
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
else:
|
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}
|
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 = post.get('family_data_json', '[]')
|
||||||
family_data = json.loads(family_data_json) if family_data_json else []
|
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', ''),
|
'contact_no': member.get('contact', ''),
|
||||||
'dob': datetime.strptime(member.get('dob'), '%Y-%m-%d').date() if member.get('dob') else None,
|
'dob': datetime.strptime(member.get('dob'), '%Y-%m-%d').date() if member.get('dob') else None,
|
||||||
'location': member.get('location', ''),
|
'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 = post.get('education_data_json', '[]')
|
||||||
education_data = json.loads(education_data_json) if education_data_json else []
|
education_data = json.loads(education_data_json) if education_data_json else []
|
||||||
if education_data:
|
if education_data:
|
||||||
|
|
@ -262,28 +273,13 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
}) for company in employer_data
|
}) for company in employer_data
|
||||||
]
|
]
|
||||||
|
|
||||||
#attachments
|
|
||||||
attachments_data_json = post.get('attachments_data_json', '[]')
|
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||||
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
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.write(applicant_data)
|
||||||
|
applicant.replace_joining_attachments(attachments_data)
|
||||||
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
template = request.env.ref('hr_recruitment_extended.email_template_post_onboarding_form_user_submit',
|
||||||
raise_if_not_found=False)
|
raise_if_not_found=False)
|
||||||
# Get HR managers with HR department
|
|
||||||
group = request.env.ref('hr.group_hr_manager')
|
group = request.env.ref('hr.group_hr_manager')
|
||||||
users = request.env['res.users'].sudo().search([
|
users = request.env['res.users'].sudo().search([
|
||||||
('groups_id', 'in', group.ids),
|
('groups_id', 'in', group.ids),
|
||||||
|
|
@ -292,27 +288,20 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
('employee_id.department_id.name', '=', 'Human Resource')
|
('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])
|
email_cc = ','.join([user.email for user in users])
|
||||||
|
|
||||||
# Prepare email values
|
|
||||||
email_values = {
|
email_values = {
|
||||||
'email_from': applicant.email_from,
|
'email_from': applicant.email_from,
|
||||||
'email_to': 'hr@ftprotech.com',
|
'email_to': 'hr@ftprotech.com',
|
||||||
'email_cc': email_cc
|
'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(
|
template.sudo().send_mail(
|
||||||
applicant.id,
|
applicant.id,
|
||||||
email_values=email_values,
|
email_values=email_values,
|
||||||
force_send=True
|
force_send=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Render thank you page
|
|
||||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
return request.render("hr_recruitment_extended.thank_you_template", {
|
||||||
'applicant': applicant
|
'applicant': applicant
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
<!-- employee.recruitment.attachments-->
|
|
||||||
<record model="ir.attachment" id="employee_recruitment_attachments_preview">
|
<record model="ir.attachment" id="employee_recruitment_attachments_preview">
|
||||||
<field name="name">Attachment Preview</field>
|
<field name="name">Attachment Preview</field>
|
||||||
<field name="type">binary</field>
|
<field name="type">binary</field>
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,10 @@
|
||||||
Request to employees to provide salary expectations, experience, and current offers.
|
Request to employees to provide salary expectations, experience, and current offers.
|
||||||
</field>
|
</field>
|
||||||
<field name="body_html" type="html">
|
<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="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="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;">
|
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
|
||||||
<p>Dear
|
<p>Dear
|
||||||
|
|
@ -143,7 +144,7 @@
|
||||||
<field name="name">Applicant Form Submission Notification</field>
|
<field name="name">Applicant Form Submission Notification</field>
|
||||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
<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_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="subject">New Submission: Applicant Salary & Experience Form</field>
|
||||||
<field name="description">
|
<field name="description">
|
||||||
Notification sent to recruiter when an applicant submits the form.
|
Notification sent to recruiter when an applicant submits the form.
|
||||||
|
|
@ -202,7 +203,7 @@
|
||||||
</field>
|
</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
|
<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;">
|
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
|
||||||
<p>Dear
|
<p>Dear
|
||||||
<strong>
|
<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')">
|
<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>
|
<p>Please ensure to provide soft copies of the required documents:</p>
|
||||||
|
|
||||||
<!-- Personal Documents -->
|
|
||||||
<t t-if="ctx.get('personal_docs')">
|
<t t-if="ctx.get('personal_docs')">
|
||||||
<strong>Personal Documents:</strong>
|
<strong>Personal Documents:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -228,7 +228,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- Education Documents -->
|
|
||||||
<t t-if="ctx.get('education_docs')">
|
<t t-if="ctx.get('education_docs')">
|
||||||
<strong>Education Documents:</strong>
|
<strong>Education Documents:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -238,7 +237,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- Previous Employer Documents -->
|
|
||||||
<t t-if="ctx.get('previous_employer_docs')">
|
<t t-if="ctx.get('previous_employer_docs')">
|
||||||
<strong>Previous Employer Documents:</strong>
|
<strong>Previous Employer Documents:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -248,7 +246,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- Additional Documents -->
|
|
||||||
<t t-if="ctx.get('other_docs')">
|
<t t-if="ctx.get('other_docs')">
|
||||||
<strong>Additional Documents:</strong>
|
<strong>Additional Documents:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
@ -261,8 +258,8 @@
|
||||||
|
|
||||||
<p>Please upload your documents via the following link:</p>
|
<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="ctx.get('applicant_request_form_url')"/>
|
||||||
<t t-set="upload_url" t-value="base_url + '/FTPROTECH/DocRequests/%s' % object.id"/>
|
<t t-esc="upload_url"/>
|
||||||
|
|
||||||
<p style="text-align: center; margin-top: 20px;">
|
<p style="text-align: center; margin-top: 20px;">
|
||||||
<a t-att-href="upload_url" target="_blank"
|
<a t-att-href="upload_url" target="_blank"
|
||||||
|
|
@ -635,29 +632,217 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="application_client_submission_email_template" model="mail.template">
|
<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="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||||
<field name="email_from">{{ user.email_formatted }}</field>
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
<field name="email_to">{{ object.hr_job_recruitment.requested_by.email }}</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">
|
<field name="description">
|
||||||
Submitting the Applicant Details to Client.
|
Share applicant and job requisition details by email.
|
||||||
</field>
|
</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
<t t-set="applicant_name"
|
||||||
Dear <t t-esc="ctx['client_name']">Sir/Madam</t>,
|
t-value="object.candidate_id.partner_name or object.partner_name or object.display_name"/>
|
||||||
<br/>
|
<t t-set="job_name">
|
||||||
<br/>
|
<t t-if="object.hr_job_recruitment.job_id">
|
||||||
Submitting new applicant.
|
<t t-set="job_name" t-value="object.hr_job_recruitment.job_id.display_name"/>
|
||||||
<br/>
|
</t>
|
||||||
Kindly review the Applicant.
|
<t t-elif="object.job_id">
|
||||||
<br/>
|
<t t-set="job_name" t-value="object.job_id.name"/>
|
||||||
<br/>
|
</t>
|
||||||
Regards,
|
<t t-else="">
|
||||||
<br/>
|
<t t-set="job_name" t-value="''"/>
|
||||||
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
|
</t>
|
||||||
</p>
|
</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>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from . import hr_recruitment
|
from . import hr_recruitment
|
||||||
from . import hr_job_recruitment
|
from . import hr_job_recruitment
|
||||||
from . import stages
|
from . import stages
|
||||||
|
from . import applicant_request_forms
|
||||||
|
from . import hr_applicant_stage_comment
|
||||||
from . import hr_applicant
|
from . import hr_applicant
|
||||||
from . import hr_job
|
from . import hr_job
|
||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import candidate_experience
|
from . import candidate_experience
|
||||||
from . import hr_employee_education_employer_family
|
from . import hr_employee_education_employer_family
|
||||||
# from . import resume_pearser
|
|
||||||
from . import recruitment_attachments
|
from . import recruitment_attachments
|
||||||
from . import hr_recruitment_source
|
from . import hr_recruitment_source
|
||||||
from . import requisitions
|
from . import requisitions
|
||||||
|
|
|
||||||
|
|
@ -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,11 +1,8 @@
|
||||||
from email.policy import default
|
|
||||||
|
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
import warnings
|
|
||||||
from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
|
from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,12 +31,29 @@ class HRApplicant(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
|
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
|
||||||
submitted_to_client = fields.Boolean(string="Submitted_to_client", default=False, readonly=True, tracking=True)
|
submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True)
|
||||||
client_submission_date = fields.Datetime(string="Submission Date")
|
client_submission_date = fields.Datetime(string="Submission Date", tracking=True)
|
||||||
submitted_stage = fields.Many2one('hr.recruitment.stage')
|
submitted_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True)
|
||||||
refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage")
|
refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage")
|
||||||
refused_comments = fields.Text(string='Reject Comments')
|
refused_comments = fields.Text(string='Reject Comments')
|
||||||
is_on_hold = fields.Boolean(string="Is On Hold", default=False)
|
is_on_hold = fields.Boolean(string="Is On Hold", default=False)
|
||||||
|
hold_state = fields.Selection(
|
||||||
|
[('hold', '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):
|
def hold_unhold_button(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -47,12 +61,66 @@ class HRApplicant(models.Model):
|
||||||
rec.is_on_hold = False
|
rec.is_on_hold = False
|
||||||
else:
|
else:
|
||||||
rec.is_on_hold = True
|
rec.is_on_hold = True
|
||||||
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||||
|
|
||||||
def action_toggle_chatter_visibility(self):
|
def action_toggle_chatter_visibility(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.hide_chatter_suggestion = not record.hide_chatter_suggestion
|
record.hide_chatter_suggestion = not record.hide_chatter_suggestion
|
||||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||||
|
|
||||||
|
@api.depends('stage_comment_ids')
|
||||||
|
def _compute_stage_comment_count(self):
|
||||||
|
for applicant in self:
|
||||||
|
applicant.stage_comment_count = len(applicant.stage_comment_ids)
|
||||||
|
|
||||||
|
@api.depends('stage_comment_ids.comment', 'stage_comment_ids.stage_id', 'stage_comment_ids.user_id', 'stage_comment_ids.comment_date')
|
||||||
|
def _compute_stage_comment_tooltips(self):
|
||||||
|
for applicant in self:
|
||||||
|
grouped_comments = {}
|
||||||
|
for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()):
|
||||||
|
if not comment.stage_id:
|
||||||
|
continue
|
||||||
|
line = '%s: %s' % (comment.user_id.name, comment.comment)
|
||||||
|
grouped_comments.setdefault(comment.stage_id.id, []).append(line)
|
||||||
|
applicant.stage_comment_tooltips = {
|
||||||
|
stage_id: '\n'.join(comments[-4:])
|
||||||
|
for stage_id, comments in grouped_comments.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_stage_comment_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Stage Comments'),
|
||||||
|
'res_model': 'applicant.stage.comment.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {
|
||||||
|
'active_id': self.id,
|
||||||
|
'default_applicant_id': self.id,
|
||||||
|
'default_stage_id': self.recruitment_stage_id.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.onchange('submitted_to_client')
|
||||||
|
def _onchange_submitted_to_client(self):
|
||||||
|
for applicant in self:
|
||||||
|
if applicant.submitted_to_client:
|
||||||
|
applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now()
|
||||||
|
applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id
|
||||||
|
else:
|
||||||
|
applicant.client_submission_date = False
|
||||||
|
applicant.submitted_stage = False
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('submitted_to_client'):
|
||||||
|
vals.setdefault('client_submission_date', fields.Datetime.now())
|
||||||
|
vals.setdefault('submitted_stage', vals.get('recruitment_stage_id'))
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
@api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
|
@api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
|
||||||
def _compute_skill_match_percentages(self):
|
def _compute_skill_match_percentages(self):
|
||||||
for applicant in self:
|
for applicant in self:
|
||||||
|
|
@ -82,13 +150,38 @@ class HRApplicant(models.Model):
|
||||||
search_domain = []
|
search_domain = []
|
||||||
if job_recruitment_id:
|
if job_recruitment_id:
|
||||||
search_domain = [('job_recruitment_ids', '=', job_recruitment_id)] + search_domain
|
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)
|
stage_ids = stages.sudo()._search(search_domain, order=stages._order)
|
||||||
return stages.browse(stage_ids)
|
return stages.browse(stage_ids)
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
|
if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1:
|
||||||
|
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
|
# user_id change: update date_open
|
||||||
res = super().write(vals)
|
res = super().write(vals)
|
||||||
if vals.get('user_id'):
|
if vals.get('user_id'):
|
||||||
|
|
@ -150,16 +243,15 @@ class HRApplicant(models.Model):
|
||||||
copy=False, index=True,
|
copy=False, index=True,
|
||||||
group_expand='_read_group_recruitment_stage_ids')
|
group_expand='_read_group_recruitment_stage_ids')
|
||||||
stage_color = fields.Char(related="recruitment_stage_id.stage_color")
|
stage_color = fields.Char(related="recruitment_stage_id.stage_color")
|
||||||
|
request_form_ids = fields.One2many(
|
||||||
send_second_application_form = fields.Boolean(related='recruitment_stage_id.second_application_form')
|
'applicant.request.forms',
|
||||||
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
'applicant_id',
|
||||||
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
|
string='Request Forms'
|
||||||
|
)
|
||||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
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_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_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')
|
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")
|
employee_code = fields.Char(related="employee_id.employee_id")
|
||||||
|
|
||||||
recruitment_attachments = fields.Many2many(
|
recruitment_attachments = fields.Many2many(
|
||||||
|
|
@ -193,37 +285,50 @@ class HRApplicant(models.Model):
|
||||||
|
|
||||||
def preview_resume(self):
|
def preview_resume(self):
|
||||||
pass
|
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):
|
def replace_joining_attachments(self, attachments_data):
|
||||||
for rec in self:
|
self.ensure_one()
|
||||||
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:
|
latest_attachments = {}
|
||||||
warnings.warn(
|
for attachment in attachments_data or []:
|
||||||
"Max no of submissions for this JD has been reached",
|
attachment_id = attachment.get('attachment_rec_id')
|
||||||
DeprecationWarning,
|
file_content = attachment.get('file_content')
|
||||||
)
|
if not attachment_id or not file_content:
|
||||||
return {
|
continue
|
||||||
'type': 'ir.actions.act_window',
|
latest_attachments[int(attachment_id)] = {
|
||||||
'name': 'Submission',
|
'name': attachment.get('file_name', ''),
|
||||||
'res_model': 'client.submission.mails.template.wizard',
|
'recruitment_attachment_id': int(attachment_id),
|
||||||
'view_mode': 'form',
|
'file': file_content,
|
||||||
'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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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):
|
def submit_for_approval(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -231,7 +336,6 @@ class HRApplicant(models.Model):
|
||||||
if not manager_id:
|
if not manager_id:
|
||||||
raise ValidationError(_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
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')
|
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))
|
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
||||||
render_ctx = dict(recruitment_manager=manager_id)
|
render_ctx = dict(recruitment_manager=manager_id)
|
||||||
mail_template.with_context(render_ctx).send_mail(
|
mail_template.with_context(render_ctx).send_mail(
|
||||||
|
|
@ -247,7 +351,6 @@ class HRApplicant(models.Model):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
_("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')
|
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))
|
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
||||||
render_ctx = dict(recruitment_manager=manager_id)
|
render_ctx = dict(recruitment_manager=manager_id)
|
||||||
mail_template.with_context(render_ctx).send_mail(
|
mail_template.with_context(render_ctx).send_mail(
|
||||||
|
|
@ -280,7 +383,7 @@ class HRApplicant(models.Model):
|
||||||
template.send_mail(applicant.id, force_send=True)
|
template.send_mail(applicant.id, force_send=True)
|
||||||
applicant.second_application_form_status = 'email_sent_to_candidate'
|
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:
|
for rec in self:
|
||||||
if not rec.employee_id:
|
if not rec.employee_id:
|
||||||
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
|
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 '',
|
||||||
|
)
|
||||||
|
|
@ -13,6 +13,8 @@ class HRJobRecruitment(models.Model):
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_inherits = {'hr.job': 'job_id'}
|
_inherits = {'hr.job': 'job_id'}
|
||||||
_rec_name = 'recruitment_sequence'
|
_rec_name = 'recruitment_sequence'
|
||||||
|
_order = 'id desc'
|
||||||
|
|
||||||
|
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||||
|
|
@ -148,7 +150,6 @@ class HRJobRecruitment(models.Model):
|
||||||
target_from = fields.Date(string="This is the date in which we starting the recruitment process",
|
target_from = fields.Date(string="This is the date in which we starting the recruitment process",
|
||||||
default=fields.Date.today, tracking=True)
|
default=fields.Date.today, tracking=True)
|
||||||
target_to = fields.Date(string='This is the target end date', 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)
|
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)
|
department_id = fields.Many2one('hr.department', string='Department', check_company=True, tracking=True)
|
||||||
description = fields.Html(string='Job Description', sanitize_attributes=False)
|
description = fields.Html(string='Job Description', sanitize_attributes=False)
|
||||||
|
|
@ -159,7 +160,6 @@ class HRJobRecruitment(models.Model):
|
||||||
help='Number of employees currently occupying this job position.')
|
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)
|
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)
|
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",
|
user_id = fields.Many2one('res.users', "Recruiter",
|
||||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||||
default=lambda self: self.env.user,
|
default=lambda self: self.env.user,
|
||||||
|
|
@ -392,6 +392,22 @@ class HRJobRecruitment(models.Model):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
@api.onchange('requested_by')
|
||||||
def _onchange_requested_by(self):
|
def _onchange_requested_by(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
|
||||||
|
|
@ -161,71 +161,6 @@ class HrCandidate(models.Model):
|
||||||
employee.write({
|
employee.write({
|
||||||
'image_1920': self.candidate_image})
|
'image_1920': self.candidate_image})
|
||||||
return action
|
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
|
# #authotentication Details
|
||||||
# pan_no = fields.Char(string='PAN No',tracking=True)
|
# pan_no = fields.Char(string='PAN No',tracking=True)
|
||||||
# identification_id = fields.Char(string='Aadhar 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
|
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.")
|
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
|
# rec.hired = hired_applicants
|
||||||
# else:
|
# else:
|
||||||
# rec.hired = False
|
# rec.hired = False
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ class RecruitmentStage(models.Model):
|
||||||
job_recruitment_ids = fields.Many2many(
|
job_recruitment_ids = fields.Many2many(
|
||||||
'hr.job.recruitment', string='Job Specific',
|
'hr.job.recruitment', string='Job Specific',
|
||||||
help='Specific jobs that use this stage. Other jobs will not use this stage.')
|
help='Specific jobs that use this stage. Other jobs will not use this stage.')
|
||||||
second_application_form = fields.Boolean(default=False)
|
# second_application_form = fields.Boolean(default=False)
|
||||||
post_onboarding_form = fields.Boolean(default=False)
|
# post_onboarding_form = fields.Boolean(default=False)
|
||||||
require_approval = 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')
|
stage_color = fields.Char('Stage Color', default='#FFFFFF', help="Choose a color for the recruitment stage", widget='color')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,12 @@ hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_rec
|
||||||
access_hr_recruitment_stage_hr,hr.recruitment.stage.hr,hr_recruitment.model_hr_recruitment_stage,hr.group_hr_manager,1,0,0,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_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_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_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} "/>
|
<value eval=" {'noupdate': True} "/>
|
||||||
</function>
|
</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">
|
<record id="hr_recruitment.hr_applicant_interviewer_rule" model="ir.rule">
|
||||||
<field name="name">Applicant Interviewer</field>
|
<field name="name">Applicant Interviewer</field>
|
||||||
<field name="domain_force">[
|
<field name="domain_force">[
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,14 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
||||||
addUploadedFileRow(attachmentId, file, base64String) {
|
addUploadedFileRow(attachmentId, file, base64String) {
|
||||||
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
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 fileId = `${attachmentId}-${Date.now()}`;
|
||||||
|
|
||||||
const fileRecord = {
|
const fileRecord = {
|
||||||
attachment_rec_id : attachmentId,
|
attachment_rec_id: parseInt(attachmentId, 10),
|
||||||
id: fileId, // Unique file ID
|
id: fileId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
base64: base64String,
|
base64: base64String,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
|
|
@ -43,36 +41,33 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
||||||
|
|
||||||
this.uploadedFiles[attachmentId].push(fileRecord);
|
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||||
|
|
||||||
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
|
||||||
const previewImageId = `preview_image_${fileId}`;
|
|
||||||
const fileNameInputId = `file_name_input_${fileId}`;
|
const fileNameInputId = `file_name_input_${fileId}`;
|
||||||
|
const previewWrapperId = `preview_wrapper_${fileId}`;
|
||||||
let previewContent = '';
|
let previewContent = '';
|
||||||
let previewClickHandler = '';
|
let previewClickHandler = '';
|
||||||
|
|
||||||
// Check if the file is an image or PDF and set preview content accordingly
|
|
||||||
if (file.type.startsWith('image/')) {
|
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;" />
|
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||||
</div>`;
|
</div>`;
|
||||||
previewClickHandler = () => {
|
previewClickHandler = () => {
|
||||||
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||||
this.$('#modal_attachment_photo_preview').show();
|
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');
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
};
|
};
|
||||||
} else if (file.type === 'application/pdf') {
|
} 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>
|
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||||
</div>`;
|
</div>`;
|
||||||
previewClickHandler = () => {
|
previewClickHandler = () => {
|
||||||
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||||
this.$('#modal_attachment_pdf_preview').show();
|
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');
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append new row to the table with a preview and buttons
|
|
||||||
tableBody.append(`
|
tableBody.append(`
|
||||||
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -96,10 +91,7 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
||||||
|
|
||||||
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||||
|
|
||||||
// Attach click handler for preview (image or PDF)
|
|
||||||
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
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);
|
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 attachmentId = $(button).data('attachment-id');
|
||||||
const fileId = $(button).data('file-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);
|
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||||
|
|
||||||
if (fileIndex !== -1) {
|
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();
|
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||||
|
|
||||||
// Hide table if no files left
|
|
||||||
if (this.uploadedFiles[attachmentId].length === 0) {
|
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||||
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||||
}
|
}
|
||||||
|
|
@ -130,15 +119,15 @@ publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
||||||
const attachmentId = $(input).data('attachment-id');
|
const attachmentId = $(input).data('attachment-id');
|
||||||
|
|
||||||
if (input.files.length > 0) {
|
if (input.files.length > 0) {
|
||||||
Array.from(input.files).forEach((file) => {
|
const file = input.files[input.files.length - 1];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const base64String = e.target.result.split(',')[1];
|
const base64String = e.target.result.split(',')[1];
|
||||||
this.addUploadedFileRow(attachmentId, file, base64String);
|
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
input.value = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUploadNewFile(ev) {
|
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_code" placeholder = "E1" required="1" width="30%"/>
|
||||||
<field name="experience_from" required="1" placeholder="0" />
|
<field name="experience_from" required="1" placeholder="0" />
|
||||||
<field name="experience_to" required="1" placeholder="2" />
|
<field name="experience_to" required="1" placeholder="2" />
|
||||||
<!-- <field name="active"/>-->
|
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,21 @@
|
||||||
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
|
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
|
||||||
<field name="model">hr.applicant</field>
|
<field name="model">hr.applicant</field>
|
||||||
<field name="arch" type="xml">
|
<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">
|
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||||
<attribute name="column_invisible">1</attribute>
|
<attribute name="column_invisible">1</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='stage_id']" position="after">
|
<xpath expr="//field[@name='stage_id']" position="after">
|
||||||
<field name="recruitment_stage_id"/>
|
<field name="recruitment_stage_id"/>
|
||||||
|
<field name="hold_state" widget="badge" decoration-muted="hold_state == 'hold'" optional="show" nolabel="1"/>
|
||||||
|
<field name="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="primary_skill_match_percentage" optional="hide"/>
|
||||||
<field name="secondary_skill_match_percentage" optional="hide"/>
|
<field name="secondary_skill_match_percentage" optional="hide"/>
|
||||||
<field name="overall_skill_match_percentage" optional="hide"/>
|
<field name="overall_skill_match_percentage" optional="hide"/>
|
||||||
|
|
@ -23,6 +33,7 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//button[@name='archive_applicant']" position="attributes">
|
<xpath expr="//button[@name='archive_applicant']" position="attributes">
|
||||||
<attribute name="string">Reject</attribute>
|
<attribute name="string">Reject</attribute>
|
||||||
|
<attribute name="invisible">not active or is_on_hold</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//button[@name='archive_applicant']" position="before">
|
<xpath expr="//button[@name='archive_applicant']" position="before">
|
||||||
<field name="is_on_hold" invisible="1" force_save="1"/>
|
<field name="is_on_hold" invisible="1" force_save="1"/>
|
||||||
|
|
@ -33,21 +44,34 @@
|
||||||
groups="hr_recruitment.group_hr_recruitment_user"
|
groups="hr_recruitment.group_hr_recruitment_user"
|
||||||
invisible="application_status in ['refused'] or not is_on_hold"/>
|
invisible="application_status in ['refused'] or not is_on_hold"/>
|
||||||
<button string="Submit" name="submit_for_approval" type="object" class="oe_stat_button"
|
<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"
|
<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"/>
|
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"
|
groups="hr_recruitment.group_hr_recruitment_user"
|
||||||
invisible="submitted_to_client or application_status in ['refused']"/>
|
invisible="not id"/>
|
||||||
|
|
||||||
</xpath>
|
</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">
|
<xpath expr="//field[@name='kanban_state']" position="after">
|
||||||
<div class="o_employee_avatar m-0 p-0">
|
<div class="o_employee_avatar m-0 p-0">
|
||||||
<field name="candidate_image" widget="image" class="oe_avatar m-0"
|
<field name="candidate_image" widget="image" class="oe_avatar m-0"
|
||||||
options="{"zoom": true, "preview_image":"candidate_image"}"/>
|
options="{"zoom": true, "preview_image":"candidate_image"}"/>
|
||||||
|
|
||||||
</div>
|
</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"/>
|
<widget name="web_ribbon" title="Awaiting Approval" bg_color="text-bg-warning" invisible="not approval_required or not application_submitted"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
|
|
@ -60,14 +84,18 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='job_id']" position="after">
|
<xpath expr="//field[@name='job_id']" position="after">
|
||||||
<field name="employee_id" invisible="1"/>
|
<field name="employee_id" invisible="1"/>
|
||||||
<field name="send_second_application_form"/>
|
</xpath>
|
||||||
<field name="second_application_form_status" readonly="not send_second_application_form"/>
|
<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="send_post_onboarding_form"/>
|
<field name="stage_comment_count" widget="statinfo" string="Stage Notes"/>
|
||||||
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
</button>
|
||||||
<field name="doc_requests_form_status" readonly="1"/>
|
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//page[@name='application_details']/group[1]" position="after">
|
<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">
|
<group string="Skill Matching">
|
||||||
<field name="primary_skill_match_percentage" readonly="1"/>
|
<field name="primary_skill_match_percentage" readonly="1"/>
|
||||||
<field name="secondary_skill_match_percentage" readonly="1"/>
|
<field name="secondary_skill_match_percentage" readonly="1"/>
|
||||||
|
|
@ -79,25 +107,14 @@
|
||||||
<attribute name="invisible">1</attribute>
|
<attribute name="invisible">1</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='stage_id']" position="before">
|
<xpath expr="//field[@name='stage_id']" position="before">
|
||||||
|
<field name="stage_comment_tooltips" invisible="1"/>
|
||||||
<field name="recruitment_stage_id" widget="statusbar_duration"
|
<field name="recruitment_stage_id" widget="statusbar_duration"
|
||||||
options="{'clickable': '1', 'fold_field': 'fold'}" invisible="not active and not employee_id"
|
options="{'clickable': '1', 'fold_field': 'fold'}" invisible="not active and not employee_id"
|
||||||
readonly="approval_required or is_on_hold" force_save="1"/>
|
readonly="approval_required or is_on_hold" force_save="1"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- <xpath expr="//form" position="after">-->
|
|
||||||
<!-- </xpath>-->
|
|
||||||
<xpath expr="//header" position="inside">
|
<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>
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//page[@name='application_details']" position="after">
|
||||||
<page name="Attachments" id="attachment_ids_page">
|
<page name="Attachments" id="attachment_ids_page">
|
||||||
<field name="recruitment_attachments" widget="many2many_tags"/>
|
<field name="recruitment_attachments" widget="many2many_tags"/>
|
||||||
|
|
||||||
|
|
@ -107,7 +124,7 @@
|
||||||
name="action_validate_attachments"
|
name="action_validate_attachments"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="attachments_validation_status == 'pending'"
|
invisible="attachments_validation_status == 'pending' or not employee_id"
|
||||||
style="width: 100%;">
|
style="width: 100%;">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data (attachments)
|
Click here to save & Update Employee Data (attachments)
|
||||||
|
|
@ -121,7 +138,7 @@
|
||||||
name="action_validate_attachments"
|
name="action_validate_attachments"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="attachments_validation_status == 'validated'"
|
invisible="attachments_validation_status == 'validated' or not employee_id"
|
||||||
style="width: 100%;">
|
style="width: 100%;">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data (attachments)
|
Click here to save, Validate and Update into Employee Data (attachments)
|
||||||
|
|
@ -161,6 +178,27 @@
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</xpath>
|
</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">
|
<xpath expr="//chatter" position="attributes">
|
||||||
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
@ -181,10 +219,13 @@
|
||||||
<xpath expr="//search/field[@name='job_id']" position="after">
|
<xpath expr="//search/field[@name='job_id']" position="after">
|
||||||
<field name="hr_job_recruitment"/>
|
<field name="hr_job_recruitment"/>
|
||||||
<field name="recruitment_stage_id" domain="[]"/>
|
<field name="recruitment_stage_id" domain="[]"/>
|
||||||
|
<field name="submitted_stage" domain="[]"/>
|
||||||
<field name="approval_required"/>
|
<field name="approval_required"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//filter[@name='my_applications']" position="after">
|
<xpath expr="//filter[@name='my_applications']" position="after">
|
||||||
<filter name="to_approve" string="To Approve" domain="[('approval_required', '=', True),('application_submitted', '=', True)]"/>
|
<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>
|
||||||
<xpath expr="//search/group" position="inside">
|
<xpath expr="//search/group" position="inside">
|
||||||
<filter string="Job Recruitment" name="job_recruitment" domain="[]"
|
<filter string="Job Recruitment" name="job_recruitment" domain="[]"
|
||||||
|
|
@ -192,6 +233,10 @@
|
||||||
|
|
||||||
<filter string="Job Recruitment Stage" name="job_recruitment_stage" domain="[]"
|
<filter string="Job Recruitment Stage" name="job_recruitment_stage" domain="[]"
|
||||||
context="{'group_by': 'recruitment_stage_id'}"/>
|
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>
|
||||||
<xpath expr="//search/group/filter[@name='stage']" position="attributes">
|
<xpath expr="//search/group/filter[@name='stage']" position="attributes">
|
||||||
<attribute name="invisible">1</attribute>
|
<attribute name="invisible">1</attribute>
|
||||||
|
|
@ -207,6 +252,43 @@
|
||||||
<xpath expr="//kanban" position="attributes">
|
<xpath expr="//kanban" position="attributes">
|
||||||
<attribute name="default_group_by">recruitment_stage_id</attribute>
|
<attribute name="default_group_by">recruitment_stage_id</attribute>
|
||||||
</xpath>
|
</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>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
|
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
|
||||||
|
|
@ -225,19 +307,22 @@
|
||||||
<field name="user_id"/>
|
<field name="user_id"/>
|
||||||
<field name="active"/>
|
<field name="active"/>
|
||||||
<field name="application_status"/>
|
<field name="application_status"/>
|
||||||
|
<field name="submitted_to_client"/>
|
||||||
<field name="is_on_hold" invisible="1"/>
|
<field name="is_on_hold" invisible="1"/>
|
||||||
<field name="company_id"
|
<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 -->
|
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"}'/>
|
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger"}'/>
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="menu">
|
<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
|
Interview
|
||||||
</a>
|
</a>
|
||||||
<a role="menuitem" name="archive_applicant" type="object" class="dropdown-item">Reject</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">Hold</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">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>
|
||||||
<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
|
Unarchive
|
||||||
</a>
|
</a>
|
||||||
<t t-if="widget.deletable">
|
<t t-if="widget.deletable">
|
||||||
|
|
@ -245,54 +330,57 @@
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
<t t-name="card">
|
<t t-name="card">
|
||||||
<widget name="web_ribbon" title="HOLD" bg_color="text-bg-success" invisible="not is_on_hold"/>
|
<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="Hired" bg_color="text-bg-success" invisible="not date_closed"/>
|
<widget name="web_ribbon" title="HOLD" bg_color="text-bg-secondary" invisible="not is_on_hold"/>
|
||||||
<widget name="web_ribbon" title="Refused" bg_color="text-bg-danger"
|
<widget name="web_ribbon" title="Hired" bg_color="text-bg-success" invisible="not date_closed"/>
|
||||||
invisible="application_status != 'refused'"/>
|
<widget name="web_ribbon" title="Refused" bg_color="text-bg-danger"
|
||||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
|
invisible="application_status != 'refused'"/>
|
||||||
invisible="application_status != 'archived'"/>
|
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
|
||||||
<div class="d-flex align-items-baseline gap-1 ms-2">
|
invisible="application_status != 'archived'"/>
|
||||||
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
|
<div class="d-flex align-items-baseline gap-1 ms-2">
|
||||||
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
|
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
|
||||||
</div>
|
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
|
||||||
<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>
|
</div>
|
||||||
<div class="col-5">
|
<div class="ms-2 mb-2" invisible="not is_on_hold">
|
||||||
<!-- Refused information, visible when status is 'refused' -->
|
<span class="badge text-bg-secondary">On Hold</span>
|
||||||
<div invisible="application_status != 'refused'">
|
</div>
|
||||||
<sheet>
|
<div class="ms-2 mb-2" invisible="not submitted_to_client">
|
||||||
<!-- Refused Stage -->
|
<span class="badge text-bg-info">Submitted to Client</span>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="refuse_reason_id"><strong>Refuse Details</strong></label>
|
<div class="row g-0 mt-0 mt-sm-3 ms-2">
|
||||||
<field name="refuse_reason_id" readonly="1" force_save="1"
|
<div class="col-7">
|
||||||
invisible="application_status != 'refused'"/>
|
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
</div>
|
<field name="applicant_properties" widget="properties"/>
|
||||||
|
</div>
|
||||||
<!-- Refuse Date -->
|
<div class="col-5">
|
||||||
<div class="form-group">
|
<div invisible="application_status != 'refused'">
|
||||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1"
|
<sheet>
|
||||||
invisible="application_status != 'refused'" widget="date"/>
|
<div class="form-group">
|
||||||
</div>
|
<label for="refuse_reason_id"><strong>Refuse Details</strong></label>
|
||||||
</sheet>
|
<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>
|
||||||
</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>
|
</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>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
</kanban>
|
</kanban>
|
||||||
|
|
@ -428,6 +516,17 @@
|
||||||
<field name="orientation">Portrait</field>
|
<field name="orientation">Portrait</field>
|
||||||
</record>
|
</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">
|
<record id="action_download_joining_form" model="ir.actions.report">
|
||||||
<field name="name">Download Joining Form</field>
|
<field name="name">Download Joining Form</field>
|
||||||
<field name="model">hr.applicant</field>
|
<field name="model">hr.applicant</field>
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
name="action_validate_personal_details"
|
name="action_validate_personal_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="personal_details_status == 'pending' or not employee_id">
|
invisible="not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data
|
Click here to save & Update Employee Data
|
||||||
<field name="personal_details_status"
|
<field name="contact_details_status"
|
||||||
widget="badge"
|
widget="badge"
|
||||||
options="{'pending': 'danger', 'validated': 'success'}"/>
|
options="{'pending': 'danger', 'validated': 'success'}"/>
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
name="action_validate_personal_details"
|
name="action_validate_personal_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="personal_details_status == 'validated' or not employee_id">
|
invisible="not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data
|
Click here to save, Validate and Update into Employee Data
|
||||||
<field name="personal_details_status"
|
<field name="personal_details_status"
|
||||||
|
|
@ -110,13 +110,12 @@
|
||||||
</div>
|
</div>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
<group string="Bank Details">
|
||||||
<group string="Bank Details" invisible="not employee_id">
|
|
||||||
<button string="Validate/update"
|
<button string="Validate/update"
|
||||||
name="action_validate_bank_details"
|
name="action_validate_bank_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="bank_details_status == 'pending'">
|
invisible="bank_details_status == 'pending' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data
|
Click here to save & Update Employee Data
|
||||||
<field name="bank_details_status"
|
<field name="bank_details_status"
|
||||||
|
|
@ -131,7 +130,7 @@
|
||||||
name="action_validate_bank_details"
|
name="action_validate_bank_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="bank_details_status == 'validated'">
|
invisible="bank_details_status == 'validated' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data
|
Click here to save, Validate and Update into Employee Data
|
||||||
<field name="bank_details_status"
|
<field name="bank_details_status"
|
||||||
|
|
@ -149,13 +148,12 @@
|
||||||
<field name="bank_ifsc_code"/>
|
<field name="bank_ifsc_code"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
<group string="Passport Details">
|
||||||
<group string="Passport Details" invisible="not employee_id">
|
|
||||||
<button string="Validate/update"
|
<button string="Validate/update"
|
||||||
name="action_validate_passport_details"
|
name="action_validate_passport_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="passport_details_status == 'pending'">
|
invisible="passport_details_status == 'pending' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data
|
Click here to save & Update Employee Data
|
||||||
<field name="passport_details_status"
|
<field name="passport_details_status"
|
||||||
|
|
@ -170,7 +168,7 @@
|
||||||
name="action_validate_passport_details"
|
name="action_validate_passport_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="passport_details_status == 'validated'">
|
invisible="passport_details_status == 'validated' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data
|
Click here to save, Validate and Update into Employee Data
|
||||||
<field name="passport_details_status"
|
<field name="passport_details_status"
|
||||||
|
|
@ -187,13 +185,12 @@
|
||||||
<field name="passport_issued_location" string="Issued Location"/>
|
<field name="passport_issued_location" string="Issued Location"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
<group string="Authentication Details">
|
||||||
<group string="Authentication Details" invisible="not employee_id">
|
|
||||||
<button string="Validate/update"
|
<button string="Validate/update"
|
||||||
name="action_validate_authentication_details"
|
name="action_validate_authentication_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="authentication_details_status == 'pending'">
|
invisible="authentication_details_status == 'pending' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data
|
Click here to save & Update Employee Data
|
||||||
<field name="authentication_details_status"
|
<field name="authentication_details_status"
|
||||||
|
|
@ -208,7 +205,7 @@
|
||||||
name="action_validate_authentication_details"
|
name="action_validate_authentication_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="authentication_details_status == 'validated'">
|
invisible="authentication_details_status == 'validated' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data
|
Click here to save, Validate and Update into Employee Data
|
||||||
<field name="authentication_details_status"
|
<field name="authentication_details_status"
|
||||||
|
|
@ -226,13 +223,12 @@
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
<group invisible="not employee_id">
|
|
||||||
<button string="Validate/update"
|
<button string="Validate/update"
|
||||||
name="action_validate_family_education_employer_details"
|
name="action_validate_family_education_employer_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
invisible="family_education_employer_details_status == 'pending'">
|
invisible="family_education_employer_details_status == 'pending' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save & Update Employee Data (Education, Employer, Family Details)
|
Click here to save & Update Employee Data (Education, Employer, Family Details)
|
||||||
<field name="family_education_employer_details_status"
|
<field name="family_education_employer_details_status"
|
||||||
|
|
@ -247,7 +243,7 @@
|
||||||
name="action_validate_family_education_employer_details"
|
name="action_validate_family_education_employer_details"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
invisible="family_education_employer_details_status == 'validated'">
|
invisible="family_education_employer_details_status == 'validated' or not employee_id">
|
||||||
<div>
|
<div>
|
||||||
Click here to save, Validate and Update into Employee Data (Education, Employer, Family Details)
|
Click here to save, Validate and Update into Employee Data (Education, Employer, Family Details)
|
||||||
<field name="family_education_employer_details_status"
|
<field name="family_education_employer_details_status"
|
||||||
|
|
@ -262,6 +258,9 @@
|
||||||
<list string="Employer Details">
|
<list string="Employer Details">
|
||||||
<field name="company_name"/>
|
<field name="company_name"/>
|
||||||
<field name="designation"/>
|
<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="date_of_joining"/>
|
||||||
<field name="last_working_day"/>
|
<field name="last_working_day"/>
|
||||||
<field name="ctc"/>
|
<field name="ctc"/>
|
||||||
|
|
@ -286,7 +285,7 @@
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group string="Education History" colspan="2">
|
<group string="Education Education" colspan="2">
|
||||||
<field mode="list" nolabel="1" name="education_history" class="mt-2">
|
<field mode="list" nolabel="1" name="education_history" class="mt-2">
|
||||||
<list string="Education Details">
|
<list string="Education Details">
|
||||||
<field name="education_type"/>
|
<field name="education_type"/>
|
||||||
|
|
@ -350,6 +349,8 @@
|
||||||
<list string="Employer Details">
|
<list string="Employer Details">
|
||||||
<field name="company_name"/>
|
<field name="company_name"/>
|
||||||
<field name="designation"/>
|
<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="date_of_joining"/>
|
||||||
<field name="last_working_day"/>
|
<field name="last_working_day"/>
|
||||||
<field name="ctc"/>
|
<field name="ctc"/>
|
||||||
|
|
@ -386,7 +387,6 @@
|
||||||
<field name="start_year"/>
|
<field name="start_year"/>
|
||||||
<field name="end_year"/>
|
<field name="end_year"/>
|
||||||
<field name="marks_or_grade"/>
|
<field name="marks_or_grade"/>
|
||||||
<!-- <field name="attachments"/>-->
|
|
||||||
<field name="employee_id" column_invisible="1"/>
|
<field name="employee_id" column_invisible="1"/>
|
||||||
</list>
|
</list>
|
||||||
<form string="Education Details">
|
<form string="Education Details">
|
||||||
|
|
|
||||||
|
|
@ -43,23 +43,11 @@
|
||||||
<field name="name">hr.job.recruitment.form</field>
|
<field name="name">hr.job.recruitment.form</field>
|
||||||
<field name="model">hr.job.recruitment</field>
|
<field name="model">hr.job.recruitment</field>
|
||||||
<field name="arch" type="xml">
|
<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">
|
<form string="Job" js_class="recruitment_form_view">
|
||||||
<header>
|
<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="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'}"/>
|
<field name="recruitment_status" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||||
</header> <!-- inherited in other module -->
|
</header> <!-- inherited in other module -->
|
||||||
<field name="active" invisible="1"/>
|
<field name="active" invisible="1"/>
|
||||||
|
|
@ -446,11 +434,11 @@
|
||||||
|
|
||||||
|
|
||||||
<record id="action_hr_job_recruitment_awaiting_published" model="ir.actions.act_window">
|
<record id="action_hr_job_recruitment_awaiting_published" model="ir.actions.act_window">
|
||||||
<field name="name">UnPublished Recruitments</field>
|
<field name="name">JD</field>
|
||||||
<field name="res_model">hr.job.recruitment</field>
|
<field name="res_model">hr.job.recruitment</field>
|
||||||
<field name="view_mode">kanban,list,form,search</field>
|
<field name="view_mode">kanban,list,form,search</field>
|
||||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
<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">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Ready to recruit more efficiently?
|
Ready to recruit more efficiently?
|
||||||
|
|
@ -461,21 +449,21 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_hr_job_recruitment_published" model="ir.actions.act_window">
|
<!-- <record id="action_hr_job_recruitment_published" model="ir.actions.act_window">-->
|
||||||
<field name="name">Published Recruitments</field>
|
<!-- <field name="name">Published</field>-->
|
||||||
<field name="res_model">hr.job.recruitment</field>
|
<!-- <field name="res_model">hr.job.recruitment</field>-->
|
||||||
<field name="view_mode">kanban,list,form,search</field>
|
<!-- <field name="view_mode">kanban,list,form,search</field>-->
|
||||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
<!-- <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="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">
|
<!-- <field name="help" type="html">-->
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<!-- <p class="o_view_nocontent_smiling_face">-->
|
||||||
Ready to recruit more efficiently?
|
<!-- Ready to recruit more efficiently?-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
<p>
|
<!-- <p>-->
|
||||||
Let's create a job position Recruitment Requests.
|
<!-- Let's create a job position Recruitment Requests.-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
</field>
|
<!-- </field>-->
|
||||||
</record>
|
<!-- </record>-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -489,23 +477,24 @@
|
||||||
<menuitem name="JD"
|
<menuitem name="JD"
|
||||||
id="menu_hr_job_descriptions"
|
id="menu_hr_job_descriptions"
|
||||||
parent="hr_recruitment.menu_hr_recruitment_root"
|
parent="hr_recruitment.menu_hr_recruitment_root"
|
||||||
|
action="action_hr_job_recruitment_awaiting_published"
|
||||||
sequence="1"
|
sequence="1"
|
||||||
groups="base.group_user"/>
|
groups="base.group_user"/>
|
||||||
|
|
||||||
<menuitem
|
<!-- <menuitem-->
|
||||||
name="Awaiting Publication"
|
<!-- name="Awaiting Publication"-->
|
||||||
id="menu_hr_job_recruitment_awaiting_publication"
|
<!-- id="menu_hr_job_recruitment_awaiting_publication"-->
|
||||||
parent="menu_hr_job_descriptions"
|
<!-- parent="menu_hr_job_descriptions"-->
|
||||||
action="action_hr_job_recruitment_awaiting_published"
|
<!-- action="action_hr_job_recruitment_awaiting_published"-->
|
||||||
sequence="1"
|
<!-- sequence="1"-->
|
||||||
groups="base.group_user"/>
|
<!-- groups="base.group_user"/>-->
|
||||||
<menuitem
|
<!-- <menuitem-->
|
||||||
name="Published"
|
<!-- name="Published"-->
|
||||||
id="menu_hr_job_recruitment_published"
|
<!-- id="menu_hr_job_recruitment_published"-->
|
||||||
parent="menu_hr_job_descriptions"
|
<!-- parent="menu_hr_job_descriptions"-->
|
||||||
action="action_hr_job_recruitment_published"
|
<!-- action="action_hr_job_recruitment_published"-->
|
||||||
sequence="2"
|
<!-- sequence="2"-->
|
||||||
groups="base.group_user"/>
|
<!-- groups="base.group_user"/>-->
|
||||||
|
|
||||||
<menuitem
|
<menuitem
|
||||||
name="Job Positions"
|
name="Job Positions"
|
||||||
|
|
@ -514,15 +503,5 @@
|
||||||
action="hr_recruitment.action_hr_job"
|
action="hr_recruitment.action_hr_job"
|
||||||
sequence="0"
|
sequence="0"
|
||||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
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>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@
|
||||||
<attribute name="class" add=""/>
|
<attribute name="class" add=""/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//kanban" position="attributes">
|
<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>
|
<attribute name="action">%(hr_recruitment_extended.action_hr_job_recruitment_requests)d</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='no_of_recruitment']" position="attributes">
|
<xpath expr="//field[@name='no_of_recruitment']" position="attributes">
|
||||||
|
|
@ -124,41 +123,11 @@
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//notebook" position="inside">
|
||||||
<page string="All Recruitments" name="hr_job_recruitments_page">
|
<page string="All Recruitments" name="hr_job_recruitments_page">
|
||||||
<field name="hr_job_recruitments"/>
|
<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>
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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">
|
<record model="ir.ui.view" id="hr_recruitment_hr_applicant_view_form_extend">
|
||||||
<field name="name">hr.applicant.view.form.extended</field>
|
<field name="name">hr.applicant.view.form.extended</field>
|
||||||
<field name="model">hr.applicant</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>
|
||||||
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
|
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
|
||||||
<field name="current_ctc"/>
|
<field name="current_ctc" invisible="1"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//page[@name='application_details']" position="inside">
|
<xpath expr="//page[@name='application_details']" position="inside">
|
||||||
<group>
|
<group>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<template id="applicant_form_template" name="Second Application Form">
|
<template id="applicant_form_template" name="Second Application Form">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<t t-set="applicant" t-value="applicant"/>
|
<t t-set="applicant" t-value="applicant"/>
|
||||||
|
<t t-set="applicant_request_id" t-value="applicant_request_id"/>
|
||||||
|
|
||||||
<section class="container mt-5">
|
<section class="container mt-5">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
|
@ -13,7 +14,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<form id="hr_recruitment_second_form_applicant"
|
<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">
|
method="post">
|
||||||
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
||||||
<input type="hidden" name="candidate_image_base64"/>
|
<input type="hidden" name="candidate_image_base64"/>
|
||||||
|
|
@ -1863,24 +1864,21 @@
|
||||||
|
|
||||||
<template id="doc_request_form_template" name="FTPROTECH Doc Request Form">
|
<template id="doc_request_form_template" name="FTPROTECH Doc Request Form">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
|
<t t-set="applicant_request_id" t-value="applicant_request_id"/>
|
||||||
<section class="container mt-5">
|
<section class="container mt-5">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="card shadow-lg p-4">
|
<div class="card shadow-lg p-4">
|
||||||
<form id="doc_request_form"
|
<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">
|
enctype="multipart/form-data">
|
||||||
<div>
|
<div>
|
||||||
<!-- Upload or Capture Photo -->
|
|
||||||
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
<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="candidate_image_base64"/>
|
||||||
<input type="hidden" name="attachments_data_json" id="attachments_data_json"/>
|
<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">
|
<div class="mb-3 text-center">
|
||||||
<!-- Image Preview with Label Click -->
|
|
||||||
<label for="candidate_image" style="cursor: pointer;">
|
<label for="candidate_image" style="cursor: pointer;">
|
||||||
<img id="photo_preview"
|
<img id="photo_preview"
|
||||||
t-att-src="'data:image/png;base64,' + (applicant.candidate_image.decode() if applicant.candidate_image else '')"
|
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;"/>
|
style="display: flex; align-items: center; justify-content: center; width: 150px; height: 150px; object-fit: cover; border: 2px solid #ddd; overflow: hidden;"/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Hidden File Input -->
|
|
||||||
<input type="file" class="d-none" name="candidate_image" id="candidate_image"
|
<input type="file" class="d-none" name="candidate_image" id="candidate_image"
|
||||||
accept="image/*"/>
|
accept="image/*"/>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<button type="button" class="btn btn-sm btn-primary"
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
onclick="document.getElementById('candidate_image').click();">
|
onclick="document.getElementById('candidate_image').click();">
|
||||||
|
|
@ -1908,7 +1904,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Preview Modal -->
|
|
||||||
<div class="modal fade" id="photoPreviewModal" tabindex="-1"
|
<div class="modal fade" id="photoPreviewModal" tabindex="-1"
|
||||||
aria-labelledby="photoPreviewModalLabel" aria-hidden="true">
|
aria-labelledby="photoPreviewModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
|
@ -1956,13 +1951,11 @@
|
||||||
<input type="file"
|
<input type="file"
|
||||||
class="form-control d-none attachment-input"
|
class="form-control d-none attachment-input"
|
||||||
t-att-data-attachment-id="attachment.id"
|
t-att-data-attachment-id="attachment.id"
|
||||||
multiple="multiple"
|
|
||||||
accept="image/*,application/pdf"/>
|
accept="image/*,application/pdf"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table to display uploaded files -->
|
|
||||||
<div t-att-id="'preview_table_container_%s' % attachment.id"
|
<div t-att-id="'preview_table_container_%s' % attachment.id"
|
||||||
class="uploaded-files-preview d-none">
|
class="uploaded-files-preview d-none">
|
||||||
<table class="table table-bordered table-striped">
|
<table class="table table-bordered table-striped">
|
||||||
|
|
@ -1981,7 +1974,6 @@
|
||||||
<p>No attachments required.</p>
|
<p>No attachments required.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Preview Modal -->
|
|
||||||
<div class="modal fade" id="attachmentPreviewModal" tabindex="-1"
|
<div class="modal fade" id="attachmentPreviewModal" tabindex="-1"
|
||||||
aria-labelledby="attachmentPreviewModalLabel" aria-hidden="true">
|
aria-labelledby="attachmentPreviewModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
|
@ -1993,12 +1985,10 @@
|
||||||
aria-label="Close"></button>
|
aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<!-- Image Preview -->
|
|
||||||
<img id="modal_attachment_photo_preview" src=""
|
<img id="modal_attachment_photo_preview" src=""
|
||||||
class="img-fluid rounded shadow"
|
class="img-fluid rounded shadow"
|
||||||
style="max-width: 100%; display: none;"/>
|
style="max-width: 100%; display: none;"/>
|
||||||
|
|
||||||
<!-- PDF Preview -->
|
|
||||||
<iframe id="modal_attachment_pdf_preview" src=""
|
<iframe id="modal_attachment_pdf_preview" src=""
|
||||||
width="100%" height="500px"
|
width="100%" height="500px"
|
||||||
style="border: none; display: none;"></iframe>
|
style="border: none; display: none;"></iframe>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
<field name="model">recruitment.requisition</field>
|
<field name="model">recruitment.requisition</field>
|
||||||
<field name="inherit_id" ref="requisitions.view_requisition_form"/>
|
<field name="inherit_id" ref="requisitions.view_requisition_form"/>
|
||||||
<field name="arch" type="xml">
|
<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">
|
<xpath expr="//field[@name='job_id']" position="attributes">
|
||||||
<attribute name="invisible">0</attribute>
|
<attribute name="invisible">0</attribute>
|
||||||
<attribute name="readonly">state not in ['draft']</attribute>
|
<attribute name="readonly">state not in ['draft']</attribute>
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@
|
||||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='category_id']" position="after">
|
<xpath expr="//field[@name='category_id']" position="after">
|
||||||
<!-- <group>-->
|
|
||||||
<field name="contact_type"/>
|
<field name="contact_type"/>
|
||||||
<!-- </group>-->
|
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='job_ids']" position="after">
|
<xpath expr="//field[@name='job_ids']" position="after">
|
||||||
<field name="job_recruitment_ids" string="Job Recruitment Ids" widget="many2many_tags"/>
|
<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="require_approval" string="Approval Required"/>
|
||||||
<field name="stage_color" widget="color" string="Select Stage Color"/>
|
<field name="stage_color" widget="color" string="Select Stage Color"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ from . import post_onboarding_attachment_wizard
|
||||||
from . import applicant_refuse_reason
|
from . import applicant_refuse_reason
|
||||||
from . import ats_invite_mail_template_wizard
|
from . import ats_invite_mail_template_wizard
|
||||||
from . import client_submission_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
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
'recruitment.attachments',
|
'recruitment.attachments',
|
||||||
string='Attachments to Request'
|
string='Attachments to Request'
|
||||||
)
|
)
|
||||||
|
request_form_id = fields.Many2one('applicant.request.forms')
|
||||||
attachment_ids = fields.Many2many('ir.attachment')
|
attachment_ids = fields.Many2many('ir.attachment')
|
||||||
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
|
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
|
||||||
template_id = fields.Many2one('mail.template', string='Email Template',compute='_compute_template_id')
|
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')
|
@api.onchange('template_id')
|
||||||
def _onchange_template_id(self):
|
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:
|
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)
|
applicant = self.env['hr.applicant'].browse(record_id)
|
||||||
|
|
||||||
if record_id:
|
if record_id:
|
||||||
|
|
@ -47,7 +48,6 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
if not record.exists():
|
if not record.exists():
|
||||||
raise UserError("The record does not exist or is not accessible.")
|
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)
|
email_template = self.env['mail.template'].sudo().browse(self.template_id.id)
|
||||||
|
|
||||||
if not email_template:
|
if not email_template:
|
||||||
|
|
@ -55,7 +55,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
|
|
||||||
self.email_from = self.env.company.email
|
self.email_from = self.env.company.email
|
||||||
self.email_to = applicant.email_from
|
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
|
self.email_subject = email_template.subject
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|
@ -72,8 +72,21 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
context = self.env.context
|
context = self.env.context
|
||||||
active_id = context.get('active_id')
|
active_id = context.get('applicant_id')
|
||||||
applicant = self.env['hr.applicant'].browse(active_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]
|
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')
|
lambda a: a.attachment_type == 'previous_employer').mapped('name')
|
||||||
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
||||||
|
|
||||||
|
print(request_upload_url)
|
||||||
# Prepare context for the template
|
|
||||||
email_context = {
|
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,
|
'personal_docs': personal_docs,
|
||||||
'education_docs': education_docs,
|
'education_docs': education_docs,
|
||||||
'previous_employer_docs': previous_employer_docs,
|
'previous_employer_docs': previous_employer_docs,
|
||||||
'other_docs': other_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_values = {
|
||||||
'email_from': rec.email_from,
|
'email_from': rec.email_from,
|
||||||
'email_to': rec.email_to,
|
'email_to': rec.email_to,
|
||||||
'email_cc': rec.email_cc,
|
'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)],
|
'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,
|
template.sudo().with_context(default_body_html=rec.email_body,
|
||||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
**email_context).send_mail(applicant.id, email_values=email_values,
|
||||||
force_send=True)
|
force_send=True)
|
||||||
|
|
||||||
|
|
||||||
if rec.is_pre_onboarding_attachment_request:
|
if not rec.is_pre_onboarding_attachment_request:
|
||||||
applicant.doc_requests_form_status = 'email_sent_to_candidate'
|
|
||||||
else:
|
|
||||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/data.xml',
|
'data/data.xml',
|
||||||
'views/masters.xml',
|
'views/masters.xml',
|
||||||
# 'views/groups.xml',
|
'views/groups.xml',
|
||||||
'views/login.xml',
|
'views/login.xml',
|
||||||
'views/menu_access_control_views.xml',
|
'views/menu_access_control_views.xml',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class CustomMasterLogin(Home):
|
||||||
response = super(CustomMasterLogin, self).web_login(*args, **kw)
|
response = super(CustomMasterLogin, self).web_login(*args, **kw)
|
||||||
|
|
||||||
if response.is_qweb:
|
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().clear_caches()
|
||||||
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,3 +152,22 @@ class IrUiMenu(models.Model):
|
||||||
parent = parent.parent_id
|
parent = parent.parent_id
|
||||||
|
|
||||||
return visible
|
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">
|
<record id="base.action_res_groups" model="ir.actions.act_window">
|
||||||
<field name="name">Roles</field>
|
<field name="name">Roles</field>
|
||||||
<field name="res_model">res.groups</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="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-->
|
<!-- <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-->
|
<!-- them access and rights to specific applications and tasks in the system. You can create custom groups or-->
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<!-- </field>-->
|
<!-- </field>-->
|
||||||
</record>
|
</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>
|
</data>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -27,9 +27,13 @@ export class ModuleSelector extends Component {
|
||||||
const masters = await this.orm.searchRead(
|
const masters = await this.orm.searchRead(
|
||||||
"master.control",
|
"master.control",
|
||||||
[["user_ids", "in", [user.userId]]],
|
[["user_ids", "in", [user.userId]]],
|
||||||
["name", "code"]
|
["name", "code", "sequence", "id"],
|
||||||
|
{
|
||||||
|
order: "sequence ASC, id ASC",
|
||||||
|
offset: 0,
|
||||||
|
limit: false
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.state.masters = masters;
|
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',
|
'author': 'Raman Marikanti',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'depends': ['base', 'hr_recruitment','hr_payroll','hr_ftp'],
|
'depends': ['base', 'hr_recruitment', 'hr_payroll', 'hr_recruitment_extended'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'data/mail_template.xml',
|
||||||
'views/offer_letter_views.xml',
|
'views/offer_letter_views.xml',
|
||||||
|
'views/hr_applicant_offer_views.xml',
|
||||||
|
'views/offer_response_templates.xml',
|
||||||
# 'views/templates.xml',
|
# 'views/templates.xml',
|
||||||
'views/menu_views.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_report.xml',
|
||||||
'report/offer_letter_template.xml',
|
'report/offer_letter_template.xml',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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(
|
employee_id = fields.Char(
|
||||||
string='Employee ID',
|
string='Employee ID',
|
||||||
|
|
@ -68,20 +70,20 @@ class OfferLetter(models.Model):
|
||||||
default=lambda self: self._default_terms()
|
default=lambda self: self._default_terms()
|
||||||
)
|
)
|
||||||
state = fields.Selection([
|
state = fields.Selection([
|
||||||
('draft', 'Draft'),
|
('requested', 'Requested'),
|
||||||
('sent', 'Sent'),
|
('sent', 'Sent'),
|
||||||
('accepted', 'Accepted'),
|
('accepted', 'Accepted'),
|
||||||
('rejected', 'Rejected'),
|
('rejected', 'Rejected'),
|
||||||
('expired', 'Expired')],
|
('expired', 'Expired')],
|
||||||
string='Status',
|
string='Status',
|
||||||
default='draft',
|
default='requested',
|
||||||
tracking=True
|
tracking=True
|
||||||
)
|
)
|
||||||
sent_date = fields.Datetime(string='Sent Date')
|
sent_date = fields.Datetime(string='Sent Date')
|
||||||
response_date = fields.Datetime(string='Response Date')
|
response_date = fields.Datetime(string='Response Date')
|
||||||
pay_struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
pay_struct_id = fields.Many2one('hr.payroll.structure', string="Salary Structure", required=True)
|
||||||
manager_id = fields.Many2one('hr.employee', string='Manager')
|
manager_id = fields.Many2one('hr.employee', string='Manager')
|
||||||
|
rejection_reason = fields.Char()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _default_terms(self):
|
def _default_terms(self):
|
||||||
|
|
@ -95,13 +97,41 @@ class OfferLetter(models.Model):
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
if vals.get('name', _('New')) == _('New'):
|
if vals.get('name', _('New')) == _('New'):
|
||||||
vals['name'] = self.env['ir.sequence'].next_by_code('offer.letter') or _('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)
|
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):
|
def action_send_offer(self):
|
||||||
self.ensure_one()
|
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()})
|
self.write({'state': 'sent', 'sent_date': fields.Datetime.now()})
|
||||||
# template.send_mail(self.id, force_send=True)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_accept_offer(self):
|
def action_accept_offer(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
|
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_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_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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<odoo>
|
<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>
|
</odoo>
|
||||||
|
|
@ -20,22 +20,26 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<header>
|
<header>
|
||||||
<button name="action_send_offer" type="object" string="Send Offer" class="oe_highlight" invisble="state != 'draft'"/>
|
<button name="action_open_send_offer_wizard" type="object" string="Send Offer" class="oe_highlight"
|
||||||
<button name="action_accept_offer" type="object" string="Accept Offer" invisble="state != 'sent'" class="oe_highlight"/>
|
invisible="state != 'requested'" groups="hr.group_hr_manager"/>
|
||||||
<button name="action_reject_offer" type="object" string="Reject Offer" invisble="state != 'sent'" class="oe_danger"/>
|
<button name="action_accept_offer" type="object" string="Accept Offer" invisible="state != 'sent'" class="oe_highlight"/>
|
||||||
<button name="get_paydetailed_lines" type="object" string="Get Data" invisble="state != 'sent'" class="oe_danger"/>
|
<!-- <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"/>
|
<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>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="name" readonly="state != 'draft'"/>
|
<field name="name" readonly="state != 'requested'"/>
|
||||||
<field name="candidate_id"/>
|
<field name="candidate_id"/>
|
||||||
|
<field name="requested_by_id" readonly="1"/>
|
||||||
|
<field name="request_date" readonly="1"/>
|
||||||
<field name="manager_id"/>
|
<field name="manager_id"/>
|
||||||
<field name="position"/>
|
<field name="position"/>
|
||||||
<field name="salary"/>
|
<field name="salary"/>
|
||||||
<field name="mi"/>
|
<field name="mi"/>
|
||||||
|
<field name="rejection_reason" readonly="state != 'rejected'"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="currency_id"/>
|
<field name="currency_id"/>
|
||||||
|
|
@ -43,6 +47,8 @@
|
||||||
<field name="contract_type"/>
|
<field name="contract_type"/>
|
||||||
<field name="probation_period"/>
|
<field name="probation_period"/>
|
||||||
<field name="pay_struct_id"/>
|
<field name="pay_struct_id"/>
|
||||||
|
<field name="sent_date" readonly="1"/>
|
||||||
|
<field name="response_date" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -15,3 +15,8 @@ class ResConfigSettings(models.TransientModel):
|
||||||
domain=lambda self: [
|
domain=lambda self: [
|
||||||
('groups_id', 'in',
|
('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">
|
id="requisition_finance_access_control">
|
||||||
<field name="requisition_finance_manager" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
<field name="requisition_finance_manager" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||||
</setting>
|
</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>
|
</block>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
<t t-name="universal_attachment_preview.PopupPreview">
|
<t t-name="universal_attachment_preview.PopupPreview">
|
||||||
<Dialog title="props.filename" size="'lg'">
|
<Dialog title="props.filename + ' (' + props.mimetype + ')'" size="'lg'">
|
||||||
<p class="text-muted mt-2">
|
<!-- <p class="text-muted mt-2">-->
|
||||||
<t t-esc="props.mimetype"/>
|
<!-- <t t-esc="props.mimetype"/>-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
|
|
||||||
<t t-if="props.mimetype.endsWith('pdf')">
|
<t t-if="props.mimetype.endsWith('pdf')">
|
||||||
<iframe t-att-src="props.url"
|
<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